如何测试锁机制

时间:2019-03-25 13:39:04

标签: ruby-on-rails ruby ruby-on-rails-5

我有一段代码,将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)之前的断点并调用该函数(可以正常工作)
  • rspec示例中的断点并调试operationparams(都很好)
还有什么想法?

修改

这是我当前的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,我也会这么做

2 个答案:

答案 0 :(得分:1)

似乎这里发生了几件事。

首先,问题的核心可能在于两个方面:

  1. 数据库清理策略:您使用的DatabaseCleaner gem提供了三种清理策略:截断,事务和删除。我的猜测是,如果您使用Transaction策略,则永远不会释放该锁,因为永远不会提交第一个事务,而第二个线程只是等待释放它。
  2. 数据库池配置:另一个可能的理论是测试数据库的连接池太小。这意味着您的一个(或两个)线程正在等待获取数据库连接。通常,为此配置了一个超时,如果设置了超时,您应该会看到一个类似于以下内容的异常: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