如何在JVM下使用concurrent-ruby编写请求规范的危险性

65 阅读4分钟

当我写一个API时,虽然我不是一个核心的TDD实践者,但我确实喜欢写规范,尤其是测试整个堆栈的请求规范。

将它们添加到API中是很快速的,而且与带有UI的应用程序相比,产生了相当好的结果,在那里你必须使用chrome-cli或phantomjs来接近这个水平,但代价是痛苦的缓慢的执行时间(是的,即使你将它们优化到地狱,并得到五分钟的运行时间,在我看来它们仍然是缓慢的)。

总之,为了了解一些情况,我写Ruby API已经有一段时间了,但使用的是经典的CRuby虚拟机/解释器,这次我们换成了JRuby(感谢Max ),因为我们需要渲染极其快速的JSON响应(即202状态代码)--所以从ActiveRecord的CRUD操作到业务逻辑的处理都是使用Concurrent-rubyFuturesThreadPools(在JRuby中,他们使用Java的本地实现- 太棒了,因为可以玩STM和所有这些好东西 - 如果需要的话)。

下一站:问题的核心:在后台运行所有这些并行的请求规范,会破坏我们大部分的规范,因为RSpec不知道任何并行运行的东西,以及并行运行的代码固有的非确定性本质。

最初的修复方法是添加一个辅助工具,基本上是强制关闭ThreadPool,直到它完成为止--这 "确保 "了所有的并行任务(比如创建记录)在我们到达规范中的期望部分之前完成。一切都很好,除了它没有像预期那样工作。

这就是最初的实现:

def wait_for_thread_pool!(sleep_time = nil)
  thread_pool_executor = executor.instance
  pool = thread_pool_executor.executor

  # shutdown the pool and wait as long as it takes
  pool.shutdown
  pool.wait_for_termination

  # we want a fresh thread pool for the next test
  thread_pool_executor.send(:initialize)
end

这很好解释--它发出一个ThreadPool关闭,等待所有线程完成它们的工作--问题是什么?在使用RSpec随机规范执行的10%的情况下,它失败了。

就目前而言,下一个最好的解决方法是给库打补丁(更准确地说,是给我们的适配器打补丁),以便按顺序运行所有的东西--特别是要记住concurrent-ruby是一个外部测试的库,问题应该是RSpec、DatabaseCleaner和concurrent-ruby之间奇怪的交互。

无论如何,在我能够开始挖掘所有这些依赖关系,看看实际问题是什么之前,这个问题已经解决了。

# run everything sequantial for now
def disable_parallelism!(context: nil)
  case context
  when :context_a
    future.instance_eval do
      def execute(executor: nil, &block)
        block.call
      end
    end
  when :context_b
    future.class_eval do
      def add_observer(observer)
        @observer = observer
      end

      def initialize(opts = {}, &block)
        @block_to_call = block
      end

      def execute
        proc_response = @block_to_call.call
        @observer.update(time.now, proc_response)
      end
    end
  end
end

我通常避免打这样的猴子补丁--但这也是猴子补丁可以给你一些喘息的时间,让你在调试原因的漫长过程中保持清醒。

pool.shutdown
pool.wait_for_termination

它没有正确地完成它的工作,可以作为一个同步(双关语)的任务继续进行。

注意:请阅读下面的更新部分,了解这个问题是如何解决的--为了完全避免我们可以使用的猴子修补法。

def disable_parallelism!
  CustomExecutor.instance.executor = Concurrent::ImmediateExecutor.new
end

关于规格/覆盖率--下一步是改善单元测试领域的代码覆盖率,以确保所有的小部分都能正确工作。

总结

JRuby+concurrent-ruby是一个福音--如上所述,有一些缺点,我将在以后的文章中详细介绍,但当你真的需要快速响应时,它真的会带来回报。另外,concurrent-ruby将JRuby的水平提高到可以与Elixir以及Erlang的并发模型竞争的地步(是的,它有Actors,但目前还在边缘分支中)。它的抽象是一流的,它们消除了处理Threads的很多痛苦,但有一些小的注意事项。

更新

r/ruby 讨论之后 ,需要进行一些更新:

一个同胞redditor -i_know_sherman 建议concurrent-ruby的ImmediateExecutor,这是一个特殊的执行器,基本上是按顺序运行一切。

一个执行器服务,在当前线程上运行所有操作,必要时进行阻塞。操作是按照收到的顺序进行的,没有两个操作可以同时进行。

这里有一个小提示:除了在使用Futures时,将选项dup_on_deref 设为true ,它才能正常工作。

来自moomaka 的另一个建议--这次是为了保持并行性。

问题可能是,有时队列中的任务需要在同一个执行器上排队等待其他任务,而这些任务被拒绝。可以通过将执行器的:fallback_policy设置为:caller_runs来解决这个问题,这样就可以在调用者线程中立即运行被拒绝的任务,使所有的事情都能完成。

这个功能是在 concurrent-ruby 中实现的,jrochkind 这里是,即改进向关闭的线程池发帖时的行为