用Ruby on Rails开发各种类型的Web应用确实是很棒的选择,但是这些Web应用所在的问题领域中,你可能经常会遇到一些复杂精密的计算或者长时间运行的后台任务。但是由于你的Web应用被限制在 HTTP协议的request/response模型下,这可能就会造成一些问题。你知道应该如何运行漫长的后台任务而不让你的Web服务器超时么?你又知道该如何把这些任务的进度告诉用户么?
作者写了一个叫做BackgrounDRb的Rails插件用来解决上面的问题。在Ruby的标准库中已经预制了DRb(Distributed Ruby),为使用TCP/IP或Unix sockets通过网络存取Ruby对象提供了一个简单的API。BackgrounDRb提供了一个框架方便在Rails以外的独立线程中运行后台任务,从而摆脱了request/response模型。而且使用DRb你可以在Rails中使用钩子函数为用户提供任务进度或者状态更新。
BackgrounDRb服务端程序通过发布一个MiddleMan对象来管理你所有的woker类,其中包括一个由{job_key => running_worker_object}键值对组成的@jobs和一个由{job_key => timestamp}键值对组成的@timestamps两个hash,MiddleMan对象是DRb服务器和你的Rails应用之间的一个接口。下面的图表简单说明了BackgrounDRb和Rails应用之间的关系。
下面是通过插件提供的worker generator脚本生成的一个worker类。
$ script/generate worker Foo
class FooWorker < BackgrounDRb::Rails
def do_work(args)
# This method is called in its own new thread when you
# call new worker. args is set to :args
end
end
当FooWorker对象在Rails中通过MiddleMan初始化以后,do_work方法会自动运行在它自己的线程中。由于do_work在自己的线程中运行,所以Rails不需要等待do_work完成就可以继续执行。
使用BackgrounDRb,你经常会通过AJAX请求创建一个新的worker对象。在View中可以使用periodically_call_remote来取得任务的进度,再用你喜欢的方式展现给用户。接下来让我们补全刚才的 FooWorker类,并告诉你如何在一个rails controller中创建新的FooWorker对象并获取它的进度。
class FooWorker < BackgrounDRb::Rails
attr_reader :progress
def do_work(args)
@progress = 0
calculate_the_meaning_of_life(args)
end
def calculate_the_meaning_of_life(args)
while @progress < 100
# calculations here
@progress += 1
end
end
end
在controller中添加下面的代码:
class MyController < ApplicationController
def start_background_task
session[:job_key] =
MiddleMan.new_worker(:class => :foo_worker,
:args => "Arguments used to instantiate a new FooWorker object")
end
def get_progress
if request.xhr?
progress_percent = MiddleMan.get_worker(session[:job_key]).progress
render :update do |page|
page.call('progressPercent', 'progressbar', progress_percent)
page.redirect_to( :action => 'done') if progress_percent >= 100
end
else
redirect_to :action => 'index'
end
end
def done
render :text => "Your FooWorker task has completed
"
MiddleMan.delete_worker(session[:job_key])
end
end
再将下面的代码添加到你的start_background_task.rhtml视图中:
<%= periodically_call_remote(:url => {:action =>
'get_progress'}, :frequency => 1) %>
MiddleMan.new_worker方法会随机产生一个job_key,你可以把它存在session中方便存取。如果你想指定job_key的名字可以使用下面的方法:
# This will throw a BackgrounDRbDuplicateKeyError if the :job_key already exists.
MiddleMan.new_worker(:class => :foo_worker,
:job_key => :my_worker,
:args => "Arguments used to instantiate a new FooWorker object")
MiddleMan.get_worker :my_worker
BackgrounDRb安装之后还会生成一个配置文件RAILS_ROOT/config/backgroundrb.yml。里面有一个load_rails配置选项,如果设置为true你就可以在worker class中使用你的ActiveRecord对象了,在BackgrounDRb服务启动的时候会自动根据database.yml中的设置去访问数据库。
这个插件还可以用于缓存类似ActiveRecord object这类大对象或者需要大量计算的对象,你也可以把渲染后的View对象或者大的查询进行缓存,事实上你可以缓存任何文本和任何可以被序列华的对象。下面是一个使用缓存的例子:
# Fill the cache
@posts = Post.find(:all, :include => :comments)
MiddleMan.cache_as(:post_cache, @posts)
# OR
@posts = MiddleMan.cache_as :post_cache do
Post.find(:all, :include => :comments)
end
# Retrieve the cache
@posts = MiddleMan.cache_get(:post_cache)
# OR
@posts = MiddleMan.cache_get(:post_cache) { Post.find(:all, :include => :comments) }
MiddleMan.cache_get接受一个可选的block,如果缓存中的:post_cache是空的,block中的计算结果就会被放到cache中并赋给@post。 如果你没有提供block而且缓存是空的则返回nil。
在现在的实现中,你要自己负责对缓存过期,删除worker对象。有两种方法,一种是直接调用MiddleMan.delete_worker(:job_key)或者MiddleMan.delete_cache(:cache_key),也可以将一个时间对象传给MiddleMan.gc! ,删除所有在timestamp之前的jobs(文章开始提到MiddleMan包括@jobs和@timestamps两个hash)。下面的脚本可以删除30分钟以前的jobs,你可以把它放在cron中执行:
#!/usr/bin/env ruby
require "drb"
DRb.start_service
MiddleMan = DRbObject.new(nil, "druby://localhost:22222")
MiddleMan.gc!(Time.now - 60*30)
在最新的特性中会有一个定时机制加入到BackgrounDRb中,这将允许你定时的运行你自己的任务和垃圾回收,或者在你创建一个新的job或cache的时候就定义一个存活时间的参数。
插件中还包含了一些命令行脚本用于启动/停止 BackgrounDRb,在OS X、Linux或者BSD上面可以使用rake:
$ rake backgroundrb:start
$ rake backgroundrb:stop
在Windows上当你运行BackgrounDRb服务的时候要始终打开那个启动服务的命令行窗口(希望后面的版本可以有所改进)。所以在Windows上启动BackgrounDRb服务你要先打开一个命令行窗口,然后运行下面的命令:
> ruby script\backgroundrb\start
# ctrl-break to stop
现在你可能会问这东西在现实中究竟可以用在什么地方?在下面的列表中作者告诉了你,他正在用BackgrounDRb做什么:
作者在后续版本中还计划加入创建新进程的能力,以便能处理需要Ruby解释器实例的更大的任务。在Windows上希望可以作为service运行,希望熟悉Windows service的人能提供一些帮助,任何建议和补丁都非常欢迎。