Workling + Carrot + RabbitMQ 做Rails后台任务

March 10th, 2011 yakjuly No comments

Workling 插件可以让 rails 以异步方式执行某些消耗时间和cpu的代码,他的特点是能非常方便的与别的消息队列服务器结合,而且代码也非常简单。

RabbitMQ 是一个由erlang编写的Message Queue服务器,特点是效率非常高。

后台任务需求:

程序中存在很多 耗费时间的任务,例如 整理文件,清理数据库垃圾,生成备份文件,请求某个网站接口。

在我们点击 页面触发这些任务时,我们可能不需要 立即返回结果,但是点了连接之后,系统立即就去做这项工作,页面迟迟不跳转,而我们就傻傻的在原地等小圈圈转呀转,转到天也黑了 或者 转出 502 来了。

事实上我们可以在点击这些任务后,让页面直接跳转,让系统在背后去做这些事情。监控是否完成的事情我们可以放到以后完善。这样 点击了一次 生成备份文件后,我知道 啊 系统会帮我做这个的,我不等他了先访问别的连接吧。

关于消息队列:

一般后台任务结合消息队列 来做,

消息队列 顾名思义,消息的队列,特点:消息,先进先出。

用消息队列的原因, 系统如果在一瞬间 产生了 很多个耗费时间的任务,那么哪个任务先做,哪个任务后做呢?如果不排下顺序的话,后来的任务就可能插队伍前面去了,那么执行的结果也可能是错误的。

后台异步执行原理:

页面点击 链接 ,系统接收请求,产生一个后台任务的消息,该消息包含要执行的任务的方法和参数,将消息保存到RabbitMQ中。系统立即返回结果,页面跳转。

系统另外启动一个后台进程(background job),该进程循环读取RabbitMQ中收到的消息,有消息的时候就按顺序读出来,并且依次执行消息里隐含的任务,没有消息则等待。

过程:

刚开始 使用workling时 使用的异步服务器客户端的是Starling,跑起来很正常,但是 当多个app 在不同的 服务器上  通过workling与一个starling跑的时候,starling会出现错误,google了一下 大概是因为memcache的接口在多个不同电脑同时读取消息时会有问题。

于是想用workling + amqp + RabbitMQ,由于amqp需要服务器支持Event Machine ,而unicorn不支持所以放弃使用amqp,根据我的尝试,如果你使用Event driven mongrelThin 完全可以直接使用amqp。

就在放弃了amqp要打算放弃rabbitmq的时候,七哥 Seven 提示我一个插件carrot。这个插件是异步读取RabbitMQ中的消息,不需要包含在某个代码块中。用这个插件不需要服务器支持event machine。这就能让unicorn 和rabbitmq结合起来了。

于是仿造workling对其他client的支持,增加了 workling 对 carrot的支持,使用非常简单,代码在 https://github.com/yakjuly/workling

 

使用方法:

1.安装rabbitmq

http://www.rabbitmq.com/install.html

2.安装carrot

gem sources -a http://gems.github.com/ (if necessary)
sudo gem install carrot

3.配置文件

因为需要支持多个不同的app同时访问一个rabbitmq-server,所以对队列的命名增加了一个需求。

workling.yml

development:
   host: lcoalhost
   port: 5576
   namespace: railsapp_development

4.environment.rb 中 添加以下代码

Workling::Remote.invoker = Workling::Remote::Invokers::CarrotSubscriber
Workling::Remote.dispatcher = Workling::Remote::Runners::ClientRunner.new
Workling::Remote.dispatcher.client = Workling::Clients::CarrotClient.new

5.启动后台监控进程

./script/workling_client start

 

目前不知道为什么workling_client restart会有问题,大家在使用的时候先用workling_client stop 再start。

在Rails3中,如果对后台任务需求并不特别要求及时或者效率的话,delayed_job是个不错的选择,如果一个网站 50% 的请求需要后台任务则 试试Redis + resque 吧。

Categories: rails, ruby, 杂七杂八 Tags:

狡诈的concat方法

December 23rd, 2010 yakjuly No comments

今天做一个小玩意儿,想用类似webistrano的构思,后台执行任务,前台显示进度。

于是在后台执行任务的时候 数据库产生一条 deployment的记录,deployment有log字段。

在执行任务的同时 把deployment的log字段中 添加 日志信息。

前台同时 定时读取deployment的信息,显示出来。

问题是 我在后台任务更新 log的时候,前台始终读不出来deployment的最新log。

调查半天,发现ActiveRecord::Base的一个bug。

我的代码是这样写的

def digui_add_file(struct, path, zip)
    dup_path = path.dup

    if struct["name"]
      if dup_path.blank?
        dup_path = struct['name']
      else
        dup_path << "/#{struct['name']}"
      end

      if struct["children"].present?
        struct["children"].each do |child|
          digui_add_file(child, dup_path, zip)
        end
      elsif struct["files"].present?
        files = UploadFile.all(:conditions => ["id in (?)", struct["files"]])
        files.each do |file|
          file_path = dup_path + "/" + file.filename
          #log info
          @deploy.log << "add: #{file_path}\n"
          @deploy.save
          zip.add(file_path, file.url)
        end
      else
        #log info
        @deploy.log << "mkdir: #{dup_path}\n"
        @deploy.save
        zip.mkdir(dup_path)
      end
    end
  end

产生的结果是 运行正常 在代码中 调查 p @deploy 也是正常, 但是@deploy 并没有更新。

console中运行以下命令:

>> d = Deployment.last
=> #<Deployment id: 27, uuid: "decf4194a1b9640d126bf927ba259588d3c6811f", log: "sdfdfbaga", result: "", profile_id: 113403, pid: nil, status: "success", created_at: "2010-12-23 03:51:40", updated_at: "2010-12-23 04:05:22">
>> d.log << "hahaha"
=> "sdfdfbagahahaha"
>> d.save
=> true
>> d.log
=> "sdfdfbagahahaha"
>> d.reload
=> #<Deployment id: 27, uuid: "decf4194a1b9640d126bf927ba259588d3c6811f", log: "sdfdfbaga", result: "public/f/tmp/level1.zip", profile_id: 113403, pid: nil, status: "success", created_at: "2010-12-23 03:51:40", updated_at: "2010-12-23 04:05:22">
>> d.log
=> "sdfdfbaga"

结果让人大吃一惊,原来 d.log << “xxxx” 在save前后并没有起到作用。

原因不明,按理说d.log << “xxx” 后 d.log #=> “xxx” 已经生效,d.save应该会把d的log更新了。

可惜事实不是如此。记录d只有在调用了log= 方法后 才会认为d被改变了。

以后在更新记录时切忌不要对单个字符串字段使用 << 或者concat 方法

Categories: rails, ruby Tags:

如何stub实例方法

November 19th, 2010 yakjuly 2 comments

rspec测试 controller。
在Factory构建数据的时候,需要调用一些方法,而这些方法无法在测试controller中通过实例来模拟。

例如,

class A < ActiveRecord::Base
  def before_save
    self.url = do_something
  end

  def do_something(arg=nil)
    42
  end

end

你希望在before_save的时候 将do_something的结果 修改为 24. 在测试 controller的时候怎么办呢?

new_method = A.method(:new)
A.stub!(:new).and_return do |*args|
  a = new_method.call(*args)
  a.should_receive(:do_something).with(any_args()).and_return(24)
  a
end

以上只适用于 new 出来的 实例。

A.any_instance.stubs(:do_something).returns(24)
Categories: rails, rspec, ruby Tags:

在windows xp下安装 ubuntu10.4.1双系统

November 9th, 2010 yakjuly No comments

可怜我的台式电脑,被我摧残了6年,到了晚年还被折腾成双系统,呜呼哀哉。

xp 我所欲也,ubuntu 亦我所欲也,两者兼得,吾愿足矣。

安装双系统之前 网上搜罗一堆资料,较容易让人明白的是利用grub安装双系统。

grub 是一个 开机引导软件。我电脑中的一键还原 就是利用了grub。可以在开机是选择是一键还原还是进入xp。

准备工作: 下载grub4dos 软件,下载ubuntu-10.04.1- desktop-i386.iso
1. 从grub4dos中找到 grub.exe;grldr;  解压到c盘根目录。
2. 从ubuntu-10.04.1- desktop-i386.iso中找到 casper文件夹下面的vmlinuz和initrd.lz,解压到c盘根目录。
3. 在c盘根目录上创建文件menu.lst,写入

title Install Ubuntu

root(hd0,0)

kernel /vmlinuz boot=casper iso-scan/filename=/ubuntu-10.04.1-desktop-i386.iso

initrd /initrd.lz

boot

意思是创建一个标题为Install Ubuntu 的启动项选择菜单,启动进入iso安装

4. 修改C盘中的boot.ini文件(先将其属性中的“只读”去掉,并且用记事本打开,若用写字板打开可能无法修改保存),在末尾行添加:

C:\grldr=”install ubuntu” 并将timeout的值设成5(让你有时间选择菜单)

5. 重新启动电脑 选择 install ubuntu,默认可能是进入文字模式,输入 startx启动图形界面(如果没效果的话,重新启动)
6. 进入图形界面,在点击 桌面上的 “安装ubuntu” 图标前 打开终端 输入 sudo umount -l /isodevice。
7. 安装ubuntu,选择分区时最好 选择高级,自己分配空间,一般来说 留3块分区给ubuntu,一块 设置目录为 “/”,我设置了10G。一块作为 交换空间(类似虚拟内存)5G,一块设置目录为 /home 10G。破电脑 就80G硬盘 再分就没得用了。网上有人说装ubuntu只要20分钟,我花了一个半小时,光设置分区就花了半个小时,硬盘那个响,把睡觉的人都给吵醒了。安装时把网断了,会更快。
8.安装完后,默认grub引导程序不起作用,ubuntu的一个bug,进入ubuntu后 打开终端 输入 sudo update-grub。 xp的引导菜单就又回来了。

grub是一个好东西,后来还帮助我解决了 错误编辑/etc/sudoers导致sudo无法使用 的问题。

Categories: ubuntu Tags:

subclasses_of的用法

October 21st, 2010 yakjuly No comments
Object.subclasses_of(ActiveRecord::Base)
#or
ActiveRecord::Base.send(:subclasses)

返回
ActiveRecord::Base的子类。

Categories: ruby Tags: