我有一段代码,将BankAccountTransaction
导入到BankAccount
bank_account.with_lock do
transactions.each do |transaction|
import(bank_account, transaction)
end
end
它可以正常工作,但是我需要为其写一个RSpec案例,这样我可以100%确保不会两次导入事务。
我写了以下助手
module ConcurrencyHelper
def make_concurrent_calls(function, concurrent_calls: 2)
threads = Array.new(concurrent_calls) do
thread = Thread.new { function.call }
thread.abort_on_exception = true
thread
end
threads.each(&:join)
end
end
我在RSpec上称呼它
context 'when importing the same transaction twice' do
subject(:concurrent_calls) { make_concurrent_calls(operation) }
let!(:operation) { -> { described_class.call(params) } }
let(:filename) { 'single-transaction-response.xml' }
it 'creates only one transaction' do
expect { concurrent_calls }.to change(BankaccountTransaction, :count).by(1)
end
end
但是什么也没发生,这时测试服卡住了,没有抛出任何错误或类似的东西。
我在实例化线程并尝试调用该函数后立即放置了一个调试点(byebug
),它运行正常,但是当我加入线程时,什么也没发生。
到目前为止我尝试过的事情
threads.each(&:join)
之前的断点并调用该函数(可以正常工作)operation
和params
(都很好)修改
这是我当前的DatabaseCleaner配置
RSpec.configure do |config|
config.before(:suite) do
DatabaseCleaner.clean_with(:deletion)
end
config.before do
DatabaseCleaner.strategy = :transaction
end
config.before(:each, js: true) do
DatabaseCleaner.strategy = :deletion
end
config.before do
DatabaseCleaner.start
end
config.after do
DatabaseCleaner.clean
end
end
我仍然没有尝试将策略修改为:deleteion
,我也会这么做
答案 0 :(得分:1)
似乎这里发生了几件事。
首先,问题的核心可能在于两个方面:
Transaction
策略,则永远不会释放该锁,因为永远不会提交第一个事务,而第二个线程只是等待释放它。could not obtain a connection from the pool within 5.000 seconds
。要解决此问题,请在database.yml
下的test
中调整pool
setting。您可以通过查看以下内容检查设置的值:irb(main):001:0> ActiveRecord::Base.connection.pool.size
=> 5
irb(main):001:0> ActiveRecord::Base.connection.pool.checkout_timeout
=> 5
第二,在您提供的代码中,除非import
修改其导入的交易或银行帐户,否则好像with_lock
并不会实际上阻止多次上传。他们按顺序运行。
您可能需要执行以下操作:
bank_account.with_lock do
unimported_transactions.each do |transaction|
import(bank_account, transaction)
transaction.mark_as_imported!
end
end
此外,如果导入正在发出某种外部请求,则应注意部分失败和回滚。 (with_lock
将所有SQL查询包装在数据库事务中,并且如果引发异常,则所有回滚都将回滚到您的数据库上,而不回滚到外部服务上)
答案 1 :(得分:0)
with_lock
是Rails的实现,我们不需要测试。您可以使用mock并检查代码是否调用with_lock
。这里唯一的技巧是确保导入事务(即with_lock
中的代码被执行)。 RSpec将提供您可以调用的块。以下是您如何执行此操作的摘要-可以在here中找到完整的工作实现。
describe "#import_transactions" do
it "runs with lock" do
# Test if with_lock is getting called
expect(subject).to receive(:with_lock) do |*_args, &block|
# block is provided to with_lock method
# execute the block and test if it creates transactions
expect { block.call }
.to change { BankAccountTransaction.count }.from(0).to(2)
end
ImportService.new.import_transactions(subject, transactions)
end
end