はじめに
この記事では Parallel
(parallel gem)でオプションとして in_processes
を指定して並行処理する中のメソッドをRSpecでモックするために辿り着いた方法を記載します。
経緯詳細
とあるメールを送信するRake Taskを書く機会がありました。
対象が多かったため、Parallel
を利用して並行処理を行うことにしました。
簡略化してめっちゃざっくり書くとこんな感じ
# frozen_string_literal: true
class SampleMailer
def send(id)
puts "Send to id: #{id}!" # 実際はメールを送ってる
end
end
namespace :sample do
namespace :parallel do
task exec: :environment do |_|
ids = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] # 実際はRDBから取得してきてる
Parallel.each(ids, in_processes: 2) do |id|
SampleMailer.new.send(id)
end
end
end
end
メールは送りたくないから SampleMailer#send
はモックして、必要な回数実行されたかを確認するRSpecを書こう。ほほーい↓
# frozen_string_literal: true
require 'spec_helper'
require 'rake'
Rspec.describe 'sample:parallel' do
before(:all) do
@rake = Rake::Application.new
Rake.application = @rake
Rake.application.rake_require 'tasks/sample/parallel'
Rake::Task.define_task(:environment)
end
describe 'exec' do
let(:task) { 'sample:parallel:exec' }
let(:sample_mailer_instance) { instance_double(SampleMailer) }
before do
allow(SampleMailer).to receive(:new).and_return(sample_mailer_instance)
allow(sample_mailer_instance).to receive(:send)
end
specify 'SampleMailer#send called 10 times' do
expect(sample_mailer_instance).to receive(:send).exactly(10).times
@rake[task].invoke
end
end
end
いくぜ、実行!
$ rspec spec/lib/tasks/sample/parallel_spec.rb
結果
Failures:
1) sample:parallel exec SampleMailer#send called 10 times
Failure/Error: expect(sample_mailer_instance).to receive(:send).exactly(10).times
(InstanceDouble(SampleMailer) (anonymous)).send(*(any args))
expected: 10 times with any arguments
received: 0 times with any arguments
(中略)
Finished in 0.45351 seconds (files took 8.29 seconds to load)
1 example, 1 failure
一度も実行されていない…だと…
上記例では簡略化していますが実際は他のロジックもあり原因が特定できず詰まること数時間、最終的に Parallel
で in_processes
を利用しているためメモリ共有できていないことが何か悪さしているのではという疑いに辿り着きました。
(本当にそうなのかはわからんが多分そう)
Parallel
部分をただの each
メソッドに変更したところ、テストは正常に通りました。
# (以上略)
namespace :sample do
namespace :parallel do
task exec: :environment do |_|
ids = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
ids.each do |id| # ここ書き換えた
SampleMailer.new.send(id)
end
# (以下略)
また、in_processes
を in_threads
に変更しても同様に成功しました。コード例は省略。
結論
以下のような記事を参考にし、
RSpec内で Parallel#each
メソッドをRuby標準の each
メソッドに上書きするというハックに出ることにしました。
コードとしてはこんな感じ
before do
allow(SampleMailer).to receive(:new).and_return(sample_mailer_instance)
allow(sample_mailer_instance).to receive(:send)
# これを足す
allow(Parallel).to receive(:each) { |ids, &block| ids.each(&block) }
end
なんとかなれーーッ!!
$ rspec spec/lib/tasks/sample/parallel_spec.rb
勝利した…ってコト!?
Finished in 0.37315 seconds (files took 9.12 seconds to load)
1 example, 0 failures
each
ではなく Parallel#each
で in_threads
指定に上書きにした方がベターかもですが、まあこの辺にしておいてやりましょう。
おわりに
以上でした。
並行処理をマルチプロセスで実行した結果を〜というのはこのRSpecのスコープではなく、担保したいところはできているのではないかと思うものの、悪い意味でトリッキーな実装だと思っています。
この方法をめちゃくちゃ推奨する自信もないですが、RSpec内で allow
の構文でメソッドの上書きをできるという選択肢だけでも持ち帰っていただければ幸いです。