用 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<br></br> class FooWorker < BackgrounDRb::Rails<br></br> def do_work(args)<br></br> # This method is called in its own new thread when you<br></br> # call new worker. args is set to :args<br></br> end<p> end</p><br></br>
当 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<br></br> attr_reader :progress<br></br> def do_work(args)<br></br> @progress = 0<br></br> calculate_the_meaning_of_life(args)<br></br> end<br></br> def calculate_the_meaning_of_life(args)<br></br> while @progress < 100<br></br> # calculations here<br></br> @progress += 1<br></br> end<br></br> end<br></br> end<br></br>
在 controller 中添加下面的代码:
class MyController < ApplicationController<br></br> def start_background_task<br></br> session[:job_key] =<br></br> MiddleMan.new_worker(:class => :foo_worker,<br></br> :args => "Arguments used to instantiate a new FooWorker object")<br></br> end<br></br> def get_progress<br></br> if request.xhr?<br></br> progress_percent = MiddleMan.get_worker(session[:job_key]).progress<br></br> render :update do |page|<br></br> page.call('progressPercent', 'progressbar', progress_percent)<br></br> page.redirect_to( :action => 'done') if progress_percent >= 100<br></br> end<br></br> else<br></br> redirect_to :action => 'index'<br></br> end<br></br> end<br></br> def done<br></br> render :text => "<h2>Your FooWorker task has completed</h2>"<br></br> MiddleMan.delete_worker(session[:job_key])<br></br> end<br></br> end<br></br>
再将下面的代码添加到你的 start_background_task.rhtml 视图中:
<p> <%= periodically_call_remote(:url => {:action => </p><br></br>'get_progress'}, :frequency => 1) %>
MiddleMan.new_worker 方法会随机产生一个 job_key,你可以把它存在 session 中方便存取。如果你想指定 job_key 的名字可以使用下面的方法:
# This will throw a BackgrounDRbDuplicateKeyError if the :job_key already exists.<br></br> MiddleMan.new_worker(:class => :foo_worker,<br></br> :job_key => :my_worker,<br></br> :args => "Arguments used to instantiate a new FooWorker object")<p> MiddleMan.get_worker :my_worker </p>
BackgrounDRb 安装之后还会生成一个配置文件 RAILS_ROOT/config/backgroundrb.yml。里面有一个 load_rails 配置选项,如果设置为 true 你就可以在 worker class 中使用你的 ActiveRecord 对象了,在 BackgrounDRb 服务启动的时候会自动根据 database.yml 中的设置去访问数据库。
这个插件还可以用于缓存类似 ActiveRecord object 这类大对象或者需要大量计算的对象,你也可以把渲染后的 View 对象或者大的查询进行缓存,事实上你可以缓存任何文本和任何可以被序列华的对象。下面是一个使用缓存的例子:
# Fill the cache<br></br> @posts = Post.find(:all, :include => :comments)<br></br> MiddleMan.cache_as(:post_cache, @posts)<br></br> # OR<br></br> @posts = MiddleMan.cache_as :post_cache do<br></br> Post.find(:all, :include => :comments)<br></br> end<p> # Retrieve the cache</p><br></br> @posts = MiddleMan.cache_get(:post_cache)<br></br> # OR<br></br> @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<br></br> require "drb"<br></br> DRb.start_service<br></br> MiddleMan = DRbObject.new(nil, "druby://localhost:22222")<br></br> MiddleMan.gc!(Time.now - 60*30)
在最新的特性中会有一个定时机制加入到 BackgrounDRb 中,这将允许你定时的运行你自己的任务和垃圾回收,或者在你创建一个新的 job 或 cache 的时候就定义一个存活时间的参数。
插件中还包含了一些命令行脚本用于启动 / 停止 BackgrounDRb,在 OS X、Linux 或者 BSD 上面可以使用 rake:
$ rake backgroundrb:start<br></br>$ rake backgroundrb:stop
在 Windows 上当你运行 BackgrounDRb 服务的时候要始终打开那个启动服务的命令行窗口(希望后面的版本可以有所改进)。所以在 Windows 上启动 BackgrounDRb 服务你要先打开一个命令行窗口,然后运行下面的命令:
> ruby script\backgroundrb\start<br></br> # ctrl-break to stop
现在你可能会问这东西在现实中究竟可以用在什么地方?在下面的列表中作者告诉了你,他正在用 BackgrounDRb 做什么:
- 下载并缓存 RSS,这样可以做一个 RSS 聚合器。
- 使用 watir 驱动浏览器在后来访问网站并收集信息,做自动的屏幕抓取。
- Xen VPS 的自动创建和系统管理任务。
- 后台为 Hyper Estraier 和 erret 创建索引。
- 连接 Rails 和 IRC 机器人。
作者在后续版本中还计划加入创建新进程的能力,以便能处理需要 Ruby 解释器实例的更大的任务。在 Windows 上希望可以作为 service 运行,希望熟悉 Windows service 的人能提供一些帮助,任何建议和补丁都非常欢迎。
- rubyforge 项目主页
- Blog
- 安装插件: script/plugin install svn://rubyforge.org//var/svn/backgroundrb
查看英文原文: Introduction to BackgrounDRb - - - - - -
作者简介:苏锐,Ruby on Rails 开发者,关注各种 Web 开发技术,Mac 爱好者。他的博客为: http://www.surui.net 。参与 InfoQ 中文站内容建设,请邮件至 china-editorial@infoq.com 。
评论