当我写一个API时,虽然我不是一个核心的TDD实践者,但我确实喜欢写规范,尤其是测试整个堆栈的请求规范。
将它们添加到API中是很快速的,而且与带有UI的应用程序相比,产生了相当好的结果,在那里你必须使用chrome-cli或phantomjs来接近这个水平,但代价是痛苦的缓慢的执行时间(是的,即使你将它们优化到地狱,并得到五分钟的运行时间,在我看来它们仍然是缓慢的)。
总之,为了了解一些情况,我写Ruby API已经有一段时间了,但使用的是经典的CRuby虚拟机/解释器,这次我们换成了JRuby(感谢Max ),因为我们需要渲染极其快速的JSON响应(即202状态代码)--所以从ActiveRecord的CRUD操作到业务逻辑的处理都是使用Concurrent-ruby与Futures和ThreadPools(在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 这里是,即改进向关闭的线程池发帖时的行为