如何测试锁机制

问题描述 投票:2回答:2

我有一段代码,我将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,我也会这样做

ruby-on-rails ruby ruby-on-rails-5
2个回答
1
投票

看起来这里有几件事情在发生。

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

  1. 数据库清理策略:您正在使用DatabaseCleaner gem,它提供三种清理策略:截断,事务和删除。我的猜测是,如果你使用Transaction策略,锁永远不会被释放,因为第一个事务永远不会被提交,第二个线程只是等待它被释放。
  2. 数据库池配置:另一种可能的理论是测试数据库的连接池太小。这意味着您的一个(或两个)线程正在等待获取数据库连接。通常会为此配置超时,如果设置了一个超时,您应该看到如下所示的异常:could not obtain a connection from the pool within 5.000 seconds。为了解决这个问题,在你的database.ymltest调整你的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查询,如果抛出异常,它将全部回滚到您的数据库而不是外部服务上)


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
© www.soinside.com 2019 - 2024. All rights reserved.