Rspecテストで同時アクセスによる排他テストを行う
商品購入に同時にアクセスがあった場合、在庫数がマイナスにならないようにテストしておきたい。
そんなテストをしたいタイミングが多々あるのではないでしょうか?
その際テーブルロックを利用して排他制御を行っていた場合、何も考えずにrspecでThreadを使って同時にロジックを実行させても上手く動きません。
これはrspecはテストの開始時にTransactionを張ってしまうため、Thread内の別プロセスからはメインプロセスで作った未コミットのモデル群にアクセスできないためです。
そこでrspec開始時にtransactionを張らないように修正する必要があります。
前提
今回はDatabaseCleanerを利用しています。
環境
Service | Version |
---|---|
Ruby | 3.2.2 |
Ruby on Rails | 7.0.6 |
rspec | 3.12.0 |
database_cleaner | 2.0.2 |
mariadb | 11.1.2 |
対応
rspecのデータ削除の仕組みであるuse_transactional_fixtures
を非活性化し、特定条件下ではDatabaseCleanerのtransactionも張らないようにします。
spec/rails_helper.rb
- config.use_transactional_fixtures = true + config.use_transactional_fixtures = false config.before(:suite) do DatabaseCleaner.strategy = :transaction DatabaseCleaner.clean_with(:truncation) end config.before(:each) do |example| + if example.metadata[:disable_database_cleaner] + DatabaseCleaner.strategy = nil + else DatabaseCleaner.strategy = :transaction DatabaseCleaner.start + end end config.after(:each) do |example| + if example.metadata[:disable_database_cleaner] + # do nothing + else DatabaseCleaner.clean + end end config.after(:all) do DatabaseCleaner.clean_with(:truncation) end
利用方法
上記設定を読むと、disabled_database_cleaner
を設定するとstrategyとしてtransaction
を利用しないようにしていることが分かります。
具体的な使い方としては下記のようになります。
RSpec.describe Order, type: :model do describe "self.create_from_cart!" do context "parallel test", disable_database_cleaner: true do let!(:product) { create(:product, :with_inventory) } let!(:cart_items) { create_list(:cart_item, 100, product: product, quantity: 1) } it "should work on multiple threads" do product.product_inventory.update!(amount: 50) cart_items.map do |cart_item| Thread.new(cart_item.cart) do |cart| ActiveRecord::Base.connection_pool.with_connection do Order.create_from_cart!(cart) rescue nil end end end.each {|t| t.join } expect(product.product_inventory.reload.amount).to eq(0) expect(Order.count).to eq(50) end end end end
context "parallel test", disable_database_cleaner: true do
の部分でtransactionを張らないよう設定をしています。
コード自体の説明はしませんが、create_from_cart!
内ではproduct_inventory
をSELECT FOR UPDATE
でコールし、排他制御を行っています。
所感
最近はやりのChatGTPに解決方法を聞いたりしたけど、全然的を射た回答はくれないし、「transactionが怪しいのでは?」と気づいたあたりで再度聞いても動きもしないコードを示してきたりで逆に時間がかかってしまった。。。
上司としての才能がないということが証明されてしまったようで辛い。。。