2018年8月の記事一覧

そうだ スピードチェック入稿のテストを自動化しよう

ラクスルのサーバサイドエンジニア 加藤です。

今回はスピードチェック入稿(テスト対象が印刷用ファイルの場合)の自動テストについて書いてみたいと思います。

再びスピードチェック入稿とは

チラシや名刺などの印刷をインターネットで注文したことのある方はいらっしゃいますか。

印刷したいものを選んで購入し、自分で用意した印刷データをアップロードして待つと、チラシや名刺などが届きます。通常の通販サイトと違うのは、自分で印刷したいデータを用意し、アップロードする作業です。さらに入稿データはラクスルでチェックし、印刷に適した形式に変換されます。

ラクスルでは、昨年8月末にそれまでオペレータが手動で行ってきたこの工程を自動化、スピードチェック入稿をリリースしました。それまで、1日以上かかってしまうこともあったデータチェックが、その場で完了。印刷物のお届けまでの日数が想定よりかかってしまうということもなくなり、多くのお客様に使われるようになりました。

リリースまでの過程は、スピードチェック入稿リリース秘話でご紹介させていただきました。

スピードチェック入稿のその後

さて、スピードチェック入稿は、その後もどんどん進化してきました。

1. 対応商品の拡大
最初は、チラシ・フライヤーと名刺だけだった対応商品に、ポスター/ポストカード/カード/チラシ折加工/折パンフレットが加わりました。

2. 入稿ファイル形式の追加
入稿ファイル形式も最初はPDFのみでしたが、ai/jpg/png/tiffでも入稿できるようになりました。また、ラクスルで提供しているWEBブラウザから利用できる無料のデザインソフト「オンラインデザイン」で作成していただいたデータも入稿することができます。

3. UXの改善
面裏の入稿原稿向き、折の開き向きなどを実際の印刷物により近い形でご確認していただくために、仕上がりプレビューが生まれました。実際に、ひっくり返したり、開いたり閉じたりして仕上がりを確認することができます。

折加工商品の仕上がりプレビュー

さて、様々な機能追加があると、大変なのがテストです。

ファイルの変換が入るために、テストを自動化しようにも、通常のWebテストのフレームワークだけでは対応できません。悩んでいるうちにも開発は進み、リリースのたびにプロジェクトメンバーがかなりの時間をかけて、データを手動でチェックしてテストをしていました。また、テストの実施もれが発生してしまうことがありました。これでは、チームとして自信を持って開発・リリースすることができません。

テストを自動化するぞ!

なんとかしなきゃとディスカッションしていたところ、「正常リリースで生成された印刷データと変換方法に変更を加えた後に生成した印刷データを、画像に変換して比較したらいいのではないか」というアイデアと、実際にImageMagicのcompareコマンドで検証結果がチーム内でシェアされました。

印刷データ画像

差分なし

差分あり

変換方法を変えて差分ができた場合は、差分箇所が赤く表示されます。これなら、一目で問題に気づけそうです!

このアイデアをとっかかりに、さらに印刷データのファイルに関してテストしたい項目を整理してみました。

1. ファイルの規格、各BOX(*)のサイズ、カラーが注文情報と比較して正しいこと
2. 問題のあるファイルに対して正しくエラーが返されていること
3. 変更前後の印刷データの画像差分がないこと、もしくは差分が妥当であると確認できること

* ドキュメントの用紙サイズ、印刷用の裁ち落としや仕上がりサイズの定義

そして、実際にテストを開発に組み込んでいくために、非エンジニアでも気軽にテストを実施したいと考えました。それぞれテストファイルが30個だとしても、この3項目に対してテストすると3倍の90項目。テストが大変だという理由で改善が進まないのは残念です。

なるべく早く問題に気づけるようにする

早速、設計に取りかかりました。

自動テストの場合は、社内ツールでユーザは自分も含め同じプロジェクトのメンバーです。要件はユーザ向けの機能と違って自由に決められますが、今回は開発がスムーズに進むようにするためなので「開発のなるべく早い段階で問題に気づけるような仕組みを作る」を指針にしました。

スピードチェック入稿のシステムは大きく3つのレイヤーに分かれています。

・フロント:データチェック用のAPIを呼び出し結果を描画
・データチェック用のAPI: フロントから受け取ったリクエストを受けてジョブをキューイング
・バックエンドジョブ:データチェックファイル変換を実施

シンプルにするために、今回の3項目のテストの対象はバックエンドジョブに絞りました。

システム構成

先ほどの指針にしたがって、1. ファイルの規格のテストに関しては、QA環境でスピードデータチェック入稿が使われると必ずジョブ内で自動で実行され、問題が発見されればslackに通知されるようにしました。

 

ファイルアップロード後の処理の流れ

上記の処理の流れの中で、サムネイルの生成が終わったらステータスは終了にして、フロントが描画できるような状態にしつつ、データチェックエンジンのチェック用のスクリプトが走るような作りです。

2. エラーのテスト3. 画像比較テスト に関しては、実行するとしばらくQA環境のジョブを占有してしまうので、実行タイミングをコントロールできるように、手動でslackから実行できるようにしました。

実行するとテストケースのYAMLから、上記の処理のファイル検証〜サムネイル生成を行うジョブをまとめて生成、結果チェックまたは画像比較が順次実行されます。画像比較テストの場合はテストケースが何十個もあるため、期待画像を一括に生成するコマンドも用意。テストケースの更新の手間も軽減しました。

テストの結果も、既存ツールに表示しようかと思いましたが、もう少し要件があったため、以下の画像のように、Datacheck APIの管理画面に表示しています。

画像比較テストの結果一覧(差分がない場合)

 

画像比較テストの結果一覧(差分がある場合)

これで1日〜2日かかっていたテスト作業が、30分以内で実施できるようになりました!
さらにミスもなくなり自信を持ってリリースすることができるように。ファイルの処理で処理フローに変更が必要になって試行錯誤することがあっても、すぐに確認できると開発効率は上がりますし、様々な改善のアイデアも湧いてきます。

自動テストの開発後、スピードチェック入稿当初からやりたかった複雑な処理変更や細かい改善を無事にリリースすることができました。

自動テストを使い続ける

自動テストは、テスト自体のメンテナンスも必要ですね。

今回は同じリポジトリにテストケースの定義を含めたため、通常のWebテストフレームワークと同じように機能修正のPull Requestに含めてテストの変更、追加を行なっています。

自動テストに関するREADMEも用意しメンバーにシェア。エンジニアだけでなく、データチェックのエンジンの設定メンバーも、テストケースの変更・追加を行なって、テストを最新の状態に保っています。

まとめ

スピードチェック入稿の印刷用ファイルを対象にテストを自動化しました。

「開発のなるべく早い段階で問題に気づけるような仕組みを作る」を指針に3つの項目のテストが短時間に実施可能に。これにより、すぐにミスに気づけるようになり、開発効率も向上しました。

テストの自動化は後回しになってきましたが、スピードチェック入稿が進化していく上で重要な施策だったと思っていますし、早速その効果を実感することができました。

ラクスルでは、絶賛エンジニア募集中です。

「仕組みを変えれば世界はもっとよくなる」

ユーザ価値をさらに向上させつつ、より社内外のサービスと繋がっていくプラットホームを開発していきたいエンジニアを募集しています。是非一度オフィスに遊びにきてください!

運用中のRailsプロジェクトをなめらかに複数DB化した話

エンジニアの二串です。

ラクスルのRailsのプロジェクトではデータベースとして Amazon Aurora(MySQL) を採用しています。そしてO/Rマッパーとして当然ActiveRecordを使っています。

さて、ActiveRecordを使っていて悩むのが複数DBの接続ですがみなさんはどうしていますか? ActiveRecordは標準では1つのデータベースにしか接続できないので、複数の異なるデータベースサーバや、マスター・リードレプリカの接続を切り替えるには工夫が必要です。しかし、世の中には素晴らしい方々がいて複数DB切り替えを可能にするgemが提供されています。

今回はそれらgemの中からSwitchPointを採用し、運用中のRailsプロジェクトになめらかに導入したときの経緯と方針、さらに具体設定を紹介します。

確認したバージョンは以下のとおりです

  • Rails 5.1.5
  • Ruby 2.4.2
  • switch_point 0.8.0
  • arproxy 0.2.3

最初からまとめ

記事では以下のことを書いています。

  • 運用中のRailsプロジェクトにSwitchPointを導入し複数DB化しました
  • マスター接続をデフォルトにし、オプショナルでリードレプリカへ接続するようにしました
  • 複数DB化した後も、いままでの素のRails(シングルDB運用)での開発のスピード感を損なわないよう、工夫をしました

導入前の事情

複数DB化の背景を少しだけお伝えしますと、今回のRailsプロジェクトは元々はphpで書かれた管理画面の刷新目的でスタートしたプロジェクトでした(詳しい事情は過去の記事でも触れています)。しばらくの月日の後、プロジェクトがいい感じに育ちまして表側の機能も提供するまでに進化しました。現在では、そうだ、ラクスルを作り直そう!でも触れられているRaksul Platform Projectの1コンポーネントを担っています。

今回の対応はその表側の機能も提供することになったフェーズでの話で、状況は以下のとおりでした。

  • これまで管理画面のみを提供していたので、マスターのみの接続で割り切ってた
  • 今後、raksul.comのパブリックな機能も提供することになった
  • read mostly, write sometimesなサイト特性上、参照系はリードレプリカへ逃したほうがベターだが、マスターに向けてしまっても当面は捌ける規模感
  • とはいえ、マスターの負荷を増加させないようリードレプリカへの接続も用意しておきたい

このような経緯で複数DB化の対応を入れることにしました。

複数DB化によるマイナス面の考慮と導入方針

これまでの経験上、複数DB化することにより以後の開発スピードが落ちたり、運用工数が増えるケースがあることを知っていました。具体的には、

  • 実装工数増加(この save! はマスターに接続して、次はSELECTするからリードレプリカに接続して…と考えたりする工数)
  • コードの見通しが悪くなる(SwitchPointの場合 with_readonly { with_writable { ... } } のようにブロックで囲ってネストするので)
  • テストの実装工数(同上 + FactoryBotで作成する際の考慮等)
  • レプリ遅延の考慮が発生する (INSERT直後にリードレプリカ側でSELECTして空振ったら、マスターにフォールバックして参照する、レプリ遅延を考慮したCI環境にするかどうか、etc..)

等々。

これらのデメリットを受け入れるというのも一つのやり方ですが、そうはしたくありませんでした。なるべく開発スピードを素のRailsでの感覚のまま、これまでのシングルDBオンリーでの開発のスピード感のままに、複数DB化できないだろうか、という点をとても大事にしました。

また、改修にあたっては、既に管理画面向け機能がたくさんあるのでコードの修正範囲は最小にしようとおもいました。

そこで、改修にあたって以下の方針を立てました。

  • gemはSwitchPointを使う
  • マスターDBデフォルト、リードレプリカ参照をオプショナル
  • コントローラのアクション単位で参照を切り替る
  • レプリ遅延を考慮したコードは書かないようにする
  • RAILS_ENV=testではシングルDBで
  • マスター・リードレプリカ接続状況をログに記録する

以下でそれぞれ詳細説明します。

SwitchPoint

SwitchPoint 1択でした。理由は、私の前職のプロジェクトで使っていて問題なく動いているのを知っていたのと、そのときに内部実装を大体把握していたからでした。よって他のgemは検討しませんでした。

マスターDBデフォルト、リードレプリカ参照オプショナル

SwitchPointのREADMEを読み進めると、モデルクラスはマスター、リードレプリカどちらに接続するかを with_readonly {} または with_writable {} ブロックでどちらも指定した例に見られます。この方針で進めると、我々の場合既存の管理画面のコントローラをすべて修正しなければならなくなってしまいます。コードの改修量は最小にしたかったのでこれは避けたい。

そこで、まず、with_readonly {}with_writable {} も指定せずにActiveRecordでクエリを実行した場合はデフォルトでマスターに接続されるようします。そして重いクエリが実行されるページやアクセスが多いページでは都度 .with_readonly {} を使ってリードレプリカへ接続させるようにしました。

# こういう処理が既にあって...
Order.find(params[:id]).update!(order_params)

# 素で SwitchPoint を導入すると以下のような書き換えが必要だが...
order = nil
order = Order.with_readonly { Order.find(params[:id] }
Order.with_writable { order.update!(order_params)

# そうではなくマスター接続をデフォルトにすれば、既存コードを書き換えずに済む
Order.find(params[:id]).update!(order_params)

これを実現するにはいくつか設定が必要です。READMEにはあまり書かれていなかったので内部実装も読んでます。

database.yml

# writable 系は Rails 標準のネーミング development, production, etc とする.
# これは https://github.com/eagletmt/switch_point/issues/16 の通りで、標準のネーミングの設定が無いと
# Rails が起動しないケースに遭遇したため.
development:
  # 接続情報(略)
  database: raksul_development
development_raksul_readonly:
  # 接続情報(略)
  database: raksul_development

test:
  # 接続情報(略)
  database: raksul_test
test_raksul_readonly:
  # 接続情報(略)
  database: raksul_test

production:
  # 接続情報(略)
production_raksul_readonly:
  # 接続情報(略)
  • マスターの接続情報はRails標準のdevelopment,test,productionに定義する
  • リードレプリカの接続情報は#{Rails.env}_raksul_readonly に定義する

switch_pointの設定

SwitchPointでマスターDBへの接続をデフォルトにするには SwitchPoint.writable!(接続シンボル名)をアプリケーション初期化フェースでcallします。

# config/initializers/switch_point.rb

SwitchPoint.configure do |config|
  raksul_config = {
    readonly: :"#{Rails.env}_raksul_readonly",
    writable: :"#{Rails.env}"
  }
  if Rails.env.test?
    # 通常SwitchPointは2本コネクションを作る(readとwrite).
    # しかし、database cleanerでtransaction strategyを使っているとテストがfailする.
    # なぜなら、transactionのbeginはwriteコネクションでcallされ、
    # SELECT は read コネクションで呼ばれるが、
    # FactoryBotで作られたレコードはcommitされるまでSELECTしても参照できないのでテストが成立しない.
    #
    # この問題を解決する2つの方法:
    #   1. RAILS_ENV=testでのみSwitchPointのコネクションを1本にする
    #   2. transaction以外のstragegy(truncation or deletion)に変更する
    #
    # 我々は1を選択した。2の方法ではテストが遅くなったため。
    #
    # 1をやるには、readonlyのコネクション設定を消すことで実現できる.
    # こうするとSwitchPointはwritableコネクションにフォールバックする挙動
    # ref https://github.com/eagletmt/switch_point/blob/v0.8.0/lib/switch_point/proxy.rb#L131
    raksul_config.delete(:readonly)
  end
  config.define_switch_point :raksul, raksul_config
end
# Change @global_model of the proxy for raksul :writable
# ブロック指定なしの場合の挙動を writable にする
SwitchPoint.writable!(:raksul)


# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
 self.abstract_class = true

 # モデルのSwitchPoint組み込み
 use_switch_point :raksul
end

なお、一点database_cleanerでtransaction strategyを使っていたので上のコメントの通りコネクションを1本にする対策を入れました。

これで既存のコードは修正せずに済みます。

ではリードレプリカを参照したい場合には with_readonly { } のブロックを書かなければならないのか? これも次の方法で楽をします。

コントローラのアクション単位でリードレプリカに切り替える

開発ポリシとして、コントローラのアクション単位でリードレプリカへ接続を向けるようにします。

例えば、OrdersControllerの #index, #show, #new, #edit ではDBへの書き込みはなく参照しか発生しない、というケースは良くあることかとおもいます。なので、around_action filter を使って with_readonly {} をcallしてあげます。そうすることで、リードレプリカ参照においても with_readonly {} のブロックを意識することなく filter を設定するだけで済みます。ブロックがないのでコードの見通しが損なわれません。

class ApplicationController < ActionController::Base
  private

  def with_readonly
    ApplicationRecord.with_readonly { yield }
  end
end

class OrdersController < ApplicationController
  around_action :with_readonly, only: %i[index]

  def index
    # アクション内で .with_readonly { .. } を意識しなくて済む
    @orders = Order.all.order(:id)
  end
end

中には参照系のペーシだけど別途行動ログを書き込みたいといった場合もあるかもしれませんが、そういうケースではそのそのアクションではリードレプリカへ接続させずマスターへ接続させる、で割り切ります。

レプリ遅延は考慮しないコードを書く

1つのアクション内でいくつかレコードをSELECTしINSERT or UPDATEする時、理想的には参照はリードレプリカへ、書き込みはマスターへ、と細かく切り替えるのがDB負荷を考えると理想的です。ただ、レプリ遅延により書き込み直後のリードレプリカ側のSELECTが空振って想定してなかったバグが発生したり、またその対策としてマスターへフォールバックさせるコードを書いたり…という経験がある方もいるとおもいます。このような状況では開発工数も運用工数も増えます。

そう、この考慮したくない、そうおもいました。ですので思い切って割り切ることにしました。つまり、書き込みが発生するアクション内ではずっとマスターと接続させておく。こうすることでレプリ遅延しても同じアクション内でレプリ遅延は発生しないので考慮する必要はなくなります。(リダイレクト先のアクションでレプリ遅延がある可能性はありますが)

トラフィック特性上、更新系のアクションをマスターに向けても即座に詰まることはないので、これで良いと思っています。もし、トラフィックが激増して厳密に切り替えないと回らない…という状況になったら対策するとして、そうなったときはつまりサービスが拡大しているということなのでとても嬉しい状況ですね。

RAILS_ENV=testではシングルDBで

上述の通り、レプリ遅延を極力考慮しない割り切りなので、テスト実行においてレプリ遅延が発生するケースをあぶり出すような考慮はしません。ですのでCI環境やローカル環境ではシングルDBなのでレプリケーション設定もしてません。

マスター・リードレプリカとの接続状況をログに記録する

開発時に便利なので、ActiveRecordのクエリログに readonly or writable どちらに接続したかを記録するようにしました。ログの拡張には cookpad/arproxy を使いました。とても便利で感謝しかないです。

設定

# config/initializers/arproxy.rb

if Rails.env.development? || Rails.env.test?
  require 'switch_point_logger_enhancement'

  Arproxy.configure do |config|
    config.adapter = 'mysql2'
    config.use SwitchPointLoggerEnhancement
  end
  Arproxy.enable!
end

# lib/switch_point_logger_enhancement.rb

class SwitchPointLoggerEnhancement < Arproxy::Base
  def execute(sql, name = nil)
    proxy = SwitchPoint::ProxyRepository.checkout(:raksul)
    mode = proxy.mode
    name = "#{name} [#{mode}]"
    super(sql, name)
  end
end

 

ログサンプル

SCHEMA [readonly] (0.8ms)  SHOW FULL FIELDS FROM `tickets`
Ticket Load [readonly] (0.3ms)  SELECT `tickets`.* FROM `tickets` WHERE `tickets`.`staff_id` IN (100, 201, 12, 13, 71, 10)
Staff Load [writable] (0.3ms)  SELECT  `staffs`.* FROM `staffs` WHERE `staffs`.`id` = 2 ORDER BY `staffs`.`id` ASC LIMIT 1
Role Load [writable] (0.4ms)  SELECT `roles`.* FROM `roles` INNER JOIN `abilities` ON `roles`.`id` = `abilities`.`role_id` WHERE `abilities`.`staff_id` = 2

[readonly] [writable] の箇所です。

まとめ

運用中のRailsプロジェクトにSwitchPointを導入し複数DB化しました。

マスター接続デフォルト、リードレプリカ接続をオプショナルとすることで、既存コードの改修を最小に押さえて導入の敷居を下げつつ、リードレプリカを参照させるときはコントローラのアクション単位で制御するようにしました。

また、レプリ遅延の考慮は極力意識しなくて済むよう、マスター、リードレプリカの接続を混ぜるアクションを実装しない割り切りをしました。

これらにより、複数DB化した後も、素のRails(シングルDB運用)と同じレベルの開発スピード感を維持することができました。

実際、本番導入からしばらく立ちますが問題なく運用できており、また開発もこれまでどおりのスピード感で進めれています。

ラクスルではエンジニアを積極採用しています

いろいろ詳しく書きましたが、複数DB化にあたっての検討は私一人で行ったわけではなく、自社のサービス傾向や開発運用スタイル似合うかどうかを、周りにいるRailsに詳しいエンジニアにも相談しながら、進めていきました。1人で決めるより話し合って決めるラクスルのスタイルはとてもやりやすく感じています。

そういうわけでして、ラクスルでは私達と開発指針を議論しながら開発したいエンジニアを絶賛募集しています。是非一度オフィスに遊びにきてください!