我有一段代码,我将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
,我也会这样做
看起来这里有几件事情在发生。
首先,问题的核心可能在于几个方面:
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查询,如果抛出异常,它将全部回滚到您的数据库而不是外部服务上)
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