Archive

Archive for the ‘rails’ Category

系统接口设计

May 24th, 2011 yakjuly No comments

最近的工作redmine的二次开发,需要频繁的与别的系统打交道,在写接口这方面的代码 有了点心得写下来 分享一下。

首先 redmine作为一个前端展示给客户的系统,需要和 翻译系统,代码管理系统,SDK管理系统交互。

这三个系统 通过普通的Net::HTTP请求,返回内容。

返回内容格式不尽相同,

1.翻译系统直接返回 xml字符串,

2.sdk系统返回 {“code”: “200″, “data”: “xxxx”} 或 {“code”: “400″, “data”: ” xxxx”, “message”: “xx wrong”}

3.代码管理系统返回 {“status”: 0, “val”: “xxxxx”, } 或  {“status” : -1, “err”: {“message”: “xxxx”, “event”: “xxx”}}

我希望无论请求哪个接口都能在日志种记录下来,发生错误时能有合适的客户提示,代码不要重复同时也要便于修改。

定义加强版Net::HTTP

require "net/http"
require "tempfile"
require "base64"
require "digest"

class EnhancedNetHttp

  class Error < StandardError; end

  @@default_logger = Logger.new(Rails.root.join("log/net_http.log"))
  def self.default_logger
    @@default_logger
  end

  USER_AGENT    = "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/523.10.6 (KHTML, like Gecko) Version/3.0.4 Safari/523.10.6"
  BOUNDARY      = "---xxxxxxxxxxxxxxxxxx"
  CONTENT_TYPE  = "multipart/form-data; boundary=#{BOUNDARY};"

  attr_accessor :error_klass

  def initialize(*args)
    options = args.extract_options!
    self.logger = options[:logger]
    self.error_klass = options[:error_klass]
  end

  def logger
    @logger || EnhancedNetHttp.default_logger
  end

  def logger=(new_logger)
    @logger = new_logger
  end

  def error_klass
    @error_klass || EnhancedNetHttp::Error
  end

  def error_klass=(new_error_klass)
    @error_klass = new_error_klass
  end

  def post_multipart_form(url, params = {})
    uri = URI.parse(url)
    http = Net::HTTP.new(uri.host, uri.port)

    http.request(multipart_request(uri, params))
  end

  def post_form(url, params = {})
    uri = URI.parse(url)

    response = Net::HTTP.post_form(uri, params)

    if response.is_a?(Net::HTTPSuccess)
      logger.info format_message(uri, "POST", response, params)
    else
      raise error_klass.new(format_message(uri, "POST", response, params))
    end
    response
  end

  def get_response(url)
    uri = URI.parse(url)
    response = Net::HTTP.get_response(URI.parse(url))

    if response.is_a?(Net::HTTPSuccess)
      logger.info format_message(uri, "GET", response)
    else
      raise error_klass.new(format_message(uri, "GET", response))
    end
    response
  end

  def oauth_post(url, params = {})
    uri           = URI.parse(url)
    auth_header   = oauth_header(url, params)
    data          = ActiveSupport::JSON.encode(params)

    response = Net::HTTP.start(uri.host, uri.port) {|http|
      http.request_post(uri.path, data, auth_header)
    }

    if response.is_a?(Net::HTTPSuccess)
      logger.info format_message(uri, "OAUTH POST", response, params)
    else
      raise error_klass.new(format_message(uri, "OAUTH POST", response, params))
    end
    response
  end

  private

  def oauth_header(url, params= {})
    oauth = {
      "oauth_consumer_key" => "xxxxxxxxxxxx",
      "oauth_signature_method" => "HMAC-SHA1",
      "oauth_timestamp" => Time.now.to_i,
      "oauth_nonce" => Time.now.to_i,
      "oauth_version" => "1.0"
    }
    data = ActiveSupport::JSON.encode(params)
    param_string  = oauth.keys.sort.collect{ |item| "#{item}=#{oauth[item]}"}.join('&')
    base          = 'POST&' << CGI.escape(url) << '&' << CGI.escape(param_string)
    base          << "&#{CGI.escape(data)}" unless params.blank?
    signature = Base64.encode64(OpenSSL::HMAC.digest(OpenSSL::Digest::Digest.new('sha1'), "xxxxxxxxxxxxxxxxxxxxxx", base))
    oauth["oauth_signature"] = signature.strip
    header_info = oauth.keys.sort.collect{ |item| "#{item}=\"#{oauth[item]}\""}.join(',')
    {"Authorization" => "OAuth:" + header_info}
  end

  def multipart_request(uri, params = {})
    request = Net::HTTP::Post.new(uri.request_uri)
    post_body = []
    params.stringify_keys.each do |key, value|
      post_body << "--#{BOUNDARY}\r\n"
      post_body << to_multipart(key, value)
    end
    post_body << "--#{BOUNDARY}--\r\n" unless post_body.empty?

    request.body = post_body.join
    request["Content-Type"] = CONTENT_TYPE
    request["User-Agent"] = USER_AGENT
    request
  end

  def to_multipart(key, value)
    if value.class == Tempfile
      "Content-Disposition: form-data; name=\"#{CGI::escape(key)}\"; filename=\"#{value.original_filename}\"\r\n" <<
        "Content-Type: \"application/octet-stream\"\r\n\r\n#{value.read}\r\n"
    elsif value.respond_to?(:path) and value.respond_to?(:read)
      "Content-Disposition: form-data; name=\"#{CGI::escape(key)}\"; filename=\"#{value.path}\"\r\n" <<
        "Content-Type: \"application/octet-stream\"\r\n\r\n#{value.read}\r\n"
    else
      "Content-Disposition: form-data; name=\"#{CGI::escape(key)}\"\r\n\r\n#{value.to_s}\r\n"
    end
  end

  def format_message(uri, method, response, params = {})
    "#{Time.now} [#{response.class.name}] #{method} #{uri} \n #{params.inspect if params.present?}"
  end

end

这里的EnhancedNetHttp就是相当于一个NetHttp请求的代理,这个是读Gary的blog想到的。

以前在javascript中 经常写回调函数,用于在多个方法中交互,在这里我就想 为何不传入错误类型 来细化请求错误的原因呢?

于是在EnhancedNetHttp里就有了 logger 与 error_klass

post_multipart_form 和 oauth_post 则是在Net::HTTP的基础上增加的2个方法。

调用接口时则使用EnhancedNetHttp类来负责发送请求。

接下来就非常简单了 对三个不同系统的接口写三个Service,配置不通的错误类型和日志。这样方便反映错误。

实际上调用接口出错 我认为发让系统邮件给开发人员会比较好,第一时间发现错误总比过了很久去日志里查要好。

class Sdk::Service
  class Error < StandardError; end

  @@logger    = Logger.new(Sdk.config[:log_path])
  cattr_reader :logger

  def self.net_http
    @net_http ||= EnhancedNetHttp.new(:logger => logger, :error_klass => Sdk::Service::Error)
  end

  attr_accessor :host, :platform

  def initialize(*args)
    options = args.extract_options!
    options.assert_valid_keys(:host, :platform)

    self.host = options[:host] =~ /\/$/ ? options[:host].chop : options[:host]
    self.platform = options[:platform]
  end

  def admin_user_query(user_id)
    url = "/xxxxxxx/xxxxx"
    params = {:abc => user_id, :efg => 456}
    get_json(url, params)
  end

  def admin_user_xml(version_id)
    url = "/bbbbb/cccccl"
    params = {:version => version_id}
    get_xml(url, params)
  end

  def admin_owned_item_xml(version_id)
    url = "/ttttttttt/uuuuu"
    params = {:version => version_id}
    get_xml(url, params)
  end

  private

  def get_response(full_url, params)
    url = "#{full_url}?#{params.to_query}"
    Sdk::Service.net_http.get_response(url)
  end

  def oauth_post(url, params = {})
    Sdk::Service.net_http.oauth_post(url, params)
  end

  def get_xml(url_path, params = {})
    response = oauth_post("#{host}#{url_path}", params)
    Zlib::GzipReader.new(StringIO.new(response.body)).read
  end

  def get_json(url_path, params = {})
    response = oauth_post("#{host}#{url_path}", params)
    json     = ActiveSupport::JSON.decode response.body
    if json["code"] == 200
      HashWithIndifferentAccess.new(json["data"])
    else
      logger.error("Response [#{json['code']}]: #{url_path} - #{json.inspect}")
      raise Sdk::Service::Error.new(json["message"])
    end
  end

end

在重构代码的时候还用到一个有趣又简单的代理

class Translate::Proxy

  attr_accessor :service

  def initialize(service)
    self.service = service
  end

  def cache_exception(&block)
    yield
  rescue Exception => e
    raise Translate::Service::Error.new(e.message)
  end

  def method_missing(method_sym, *arguments, &block)
    if self.service.respond_to? method_sym
      cache_exception { self.service.send(method_sym, *arguments, &block) }
    else
      super
    end
  end

end

这个Proxy负责统一抛出的错误类型,如果是Net::HTTP请求发生错误 在这里则也会进行一次转换类型。

总结:优秀的程序员,总是能把代码组织的非常漂亮。多学习 多思考 多运用 会让生活变的更美好。

Categories: rails, ruby Tags:

半天搞定capistrano部署rails3至dreamhost

March 30th, 2011 yakjuly No comments

以前在公司部署都是用webistrano,图形化界面,部署代码也是sonic写,自己都不带操心的。部署得练练,不然被人一问三不知,可是很羞愧的。

首先做了一个简单的rails3 app:cookbook代码上传至github

在Gemfile中添加 gem ‘capistrano’

读capistarno的wiki getting start

安装完后要准备一些事情:

  • 必须使用ssh访问你的服务器
  • 远程服务器必须安装了POSIX-compatible shell,“sh”命令必须在默认的系统环境中
  • 将你电脑中的public key放在服务器上,保证你不用输入密码来登陆服务器

前两样基本都能满足,第三个 需要你 在终端执行命令

ssh-keygen -t rsa

系统会提示输入passphrase 此处不填,两次回车会在 ~/.ssh 下创建两个文件 id_rsa 和id_rsa.pub。文件用途如下
~/.ssh/id_rsa

  • 该用户默认的 RSA 身份认证私钥(SSH-2)。此文件的权限应当至少限制为”600″。
  • 生成密钥的时候可以指定采用密语来加密该私钥(3DES)。
  • ssh(1) 将在登录的时候读取这个文件。

~/.ssh/id_rsa.pub

  • 该用户默认的 RSA 身份认证公钥(SSH-2)。此文件无需保密。
  • 此文件的内容应该添加到所有 RSA 目标主机的 ~/.ssh/authorized_keys 文件中。

密钥和公钥创建后把公钥传到要部署的服务器上

scp ~/.ssh/id_rsa.pub yakjuly@yakjuly.com:~/.ssh/authrozied_keys

以后你从本地电脑ssh登陆服务器 就不用输入密码了,系统会根据密钥和公钥对你的身份进行认证。
同理 你把公钥 写到github帐户信息的public sshkeys中 根据 git@github.com:/xxxx/xx.git 下载时 也不用输入密码了。
因为我用github管理代码 因此 服务器上也需要生成一套密钥,以便于下载代码不需要输入密码, 把公钥填到github帐户中,这里有教程

准备好后 先按照capistrano wiki上的小例子,练下手。

在项目root下创建一个capfile文件,

task :search_libs, :hosts => "yakjuly@www.yakjuly.com" do
  run "ls -x1 /usr/lib | grep -i xml"
end

运行

cap search_libs

显示结果

* executing `search_libs'
 * executing "ls -x1 /usr/lib | grep -i xml"
 servers: ["www.yakjuly.com"]
 [yakjuly@www.yakjuly.com] executing command
 ** [out :: yakjuly@www.yakjuly.com] libwx_baseu_xml-2.6.so.0
 ** [out :: yakjuly@www.yakjuly.com] libwx_baseu_xml-2.6.so.0.3.1
 ** [out :: yakjuly@www.yakjuly.com] libxml2.a
 ** [out :: yakjuly@www.yakjuly.com] libxml2.la
 ** [out :: yakjuly@www.yakjuly.com] libxml2.so
 ** [out :: yakjuly@www.yakjuly.com] libxml2.so.2
 ** [out :: yakjuly@www.yakjuly.com] libxml2.so.2.6.32
 ** [out :: yakjuly@www.yakjuly.com] libxmlparse.a
 ** [out :: yakjuly@www.yakjuly.com] libxmlparse.so
 ** [out :: yakjuly@www.yakjuly.com] libxmlparse.so.1
 ** [out :: yakjuly@www.yakjuly.com] libxmlparse.so.1.2
 ** [out :: yakjuly@www.yakjuly.com] libxmltok.a
 ** [out :: yakjuly@www.yakjuly.com] libxmltok.so
 ** [out :: yakjuly@www.yakjuly.com] libxmltok.so.1
 ** [out :: yakjuly@www.yakjuly.com] libxmltok.so.1.2
 ** [out :: yakjuly@www.yakjuly.com] xml2Conf.sh
 command finished in 560ms

看日志就能知道了大概原理了:本地写好部署代码 远程执行 部署 返回日志 结果。

接着看Getting Start,接着写了 role ,set 设置跳板机 gateway,部署多个域名 等方法。 后面还讲了 cap invoke 和cap shell。
当我按照教程执行 cap -T的时候,结果没有出现下面那么多tasks

cap deploy               # Deploys your project.
cap deploy:check         # Test deployment dependencies.
cap deploy:cleanup       # Clean up old releases.
cap deploy:cold          # Deploys and starts a `cold' application.
cap deploy:migrate       # Run the migrate rake task.
cap deploy:migrations    # Deploy and run pending migrations.
cap deploy:pending       # Displays the commits since your last deploy.

原因是没有在app的root_path中执行 capify .
这个方法会在app根目录下创建 Capfile和 config/deploy.rb
cap默认的任务列表就可以在这个对这个app执行了

接下来 编辑 config/deploy.rb。 文件中的描述信息很清楚 跟着提示 填内容就行

set :application, "cookbook"
set :repository,  "git@github.com:yakjuly/cookbook_example.git"
set :user, "yakjuly"
set :scm, "git"
set :scm_verbose, true
set :branch, "master"
set :deploy_to, "/home/yakjuly/cookbook.yakjuly.com"
set :use_sudo, false

role :web, "yakjuly.com"
role :app, "yakjuly.com"

dreamhost没有sudo权限,这里要把use_sudo设置为false。
dreamhost的mysql数据库只能在cpannel中创建和访问 所以这里也省略role :db

第一次部署最好执行下

cap deploy:setup

会在服务器上创建shared, releases, current文件夹

  • release下保存着每次不同代码部署版本的文件夹,例如下面会有 20110330040708  20110330041621  20110330042212  20110330082446
  • current指向的是最新的releases的代码  current -> /home/yakjuly/cookbook.yakjuly.com/releases/20110330082446
  • shared是每个版本共享的log等文件存放的文件夹

例如:
current下的log文件夹
lrwxrwxrwx 1 yakjuly pg2609356   45 2011-03-30 01:24 log -> /home/yakjuly/cookbook.yakjuly.com/shared/log

 

这些都做了解后,开始部署吧

cap deploy

日志显示代码部署部分都正常 但是在 `deploy:restart’ 时候出错,因为dreamhost重新启动passenger是通过 touch tmp/restart.txt来触发的。
因此需要修改deploy:restart 添加代码到Capfile最后

namespace :deploy do
  desc "Restarting after deployment"
  task :restart, :roles => :app do
    run "cd #{release_path} && touch tmp/restart.txt"
  end
end

由于服务器上某些配置文件和开发环境不同,可以把修改配置文件的部署写在Capfile或者deploy.rb中

desc "change database etc"
task :link_symlink, :roles => :app do
  %w(database).each do |config|
    run "cd #{release_path} && rm -rf config/#{config}.yml && ln -sf ../../../shared/config/#{config}.yml config/"
  end
end

desc "set environment after code update."
task :set_environment, :roles => :app do
  run "sed 's/# ENV\\[/ENV\\[/g' #{release_path}/config/environment.rb > #{release_path}/config/environment.temp"
  run "mv #{release_path}/config/environment.temp #{release_path}/config/environment.rb"
end

after "deploy:update_code", :link_symlink, :set_environment

写到这里基本上大功告成了。
但是有个小问题需要注意,部署到服务器上的bundle install能够执行顺利,
但是passenger却爆xxx插件没有安装请运行 bundle install,是因为passenger没有找到bundle安装插件的path
需要在app的root目录下 添加 .bundle/config

---
BUNDLE_DISABLE_SHARED_GEMS: "1"
BUNDLE_PATH: /home/yakjuly/.bundler

运行一遍试试吧,部署是不是很简单?

servers: ["yakjuly.com"]
    [yakjuly.com] executing command
 ** [yakjuly.com :: err] From github.com:yakjuly/cookbook_example
 ** 4065eb6..459fc2e  master     -> origin/master
 ** [yakjuly.com :: out] HEAD is now at 459fc2e change title
    command finished in 1965ms
    copying the cached version to /home/yakjuly/cookbook.yakjuly.com/releases/20110330091501
  * executing "cp -RPp /home/yakjuly/cookbook.yakjuly.com/shared/cached-copy /home/yakjuly/cookbook.yakjuly.com/releases/20110330091501 && (echo 459fc2edd6fdbf48458ebebe27a18d776cf65182 > /home/yakjuly/cookbook.yakjuly.com/releases/20110330091501/REVISION)"
    servers: ["yakjuly.com"]
    [yakjuly.com] executing command
    command finished in 572ms
  * executing `deploy:finalize_update'
  * executing "chmod -R g+w /home/yakjuly/cookbook.yakjuly.com/releases/20110330091501"
    servers: ["yakjuly.com"]
    [yakjuly.com] executing command
    command finished in 550ms
  * executing "rm -rf /home/yakjuly/cookbook.yakjuly.com/releases/20110330091501/log /home/yakjuly/cookbook.yakjuly.com/releases/20110330091501/public/system /home/yakjuly/cookbook.yakjuly.com/releases/20110330091501/tmp/pids &&\\\n      mkdir -p /home/yakjuly/cookbook.yakjuly.com/releases/20110330091501/public &&\\\n      mkdir -p /home/yakjuly/cookbook.yakjuly.com/releases/20110330091501/tmp &&\\\n      ln -s /home/yakjuly/cookbook.yakjuly.com/shared/log /home/yakjuly/cookbook.yakjuly.com/releases/20110330091501/log &&\\\n      ln -s /home/yakjuly/cookbook.yakjuly.com/shared/system /home/yakjuly/cookbook.yakjuly.com/releases/20110330091501/public/system &&\\\n      ln -s /home/yakjuly/cookbook.yakjuly.com/shared/pids /home/yakjuly/cookbook.yakjuly.com/releases/20110330091501/tmp/pids"
    servers: ["yakjuly.com"]
    [yakjuly.com] executing command
    command finished in 562ms
  * executing "find /home/yakjuly/cookbook.yakjuly.com/releases/20110330091501/public/images /home/yakjuly/cookbook.yakjuly.com/releases/20110330091501/public/stylesheets /home/yakjuly/cookbook.yakjuly.com/releases/20110330091501/public/javascripts -exec touch -t 201103300915.02 {} ';'; true"
    servers: ["yakjuly.com"]
    [yakjuly.com] executing command
    command finished in 618ms
    triggering after callbacks for `deploy:update_code'
  * executing `link_symlink'
  * executing "cd /home/yakjuly/cookbook.yakjuly.com/releases/20110330091501 && rm -rf config/database.yml && ln -sf ../../../shared/config/database.yml config/"
    servers: ["yakjuly.com"]
    [yakjuly.com] executing command
    command finished in 549ms
  * executing `set_environment'
  * executing "sed 's/# ENV\\[/ENV\\[/g' /home/yakjuly/cookbook.yakjuly.com/releases/20110330091501/config/environment.rb > /home/yakjuly/cookbook.yakjuly.com/releases/20110330091501/config/environment.temp"
    servers: ["yakjuly.com"]
    [yakjuly.com] executing command
    command finished in 548ms
  * executing "mv /home/yakjuly/cookbook.yakjuly.com/releases/20110330091501/config/environment.temp /home/yakjuly/cookbook.yakjuly.com/releases/20110330091501/config/environment.rb"
    servers: ["yakjuly.com"]
    [yakjuly.com] executing command
    command finished in 602ms
  * executing `deploy:symlink'
  * executing "rm -f /home/yakjuly/cookbook.yakjuly.com/current && ln -s /home/yakjuly/cookbook.yakjuly.com/releases/20110330091501 /home/yakjuly/cookbook.yakjuly.com/current"
    servers: ["yakjuly.com"]
    [yakjuly.com] executing command
    command finished in 550ms
 ** transaction: commit
  * executing `deploy:restart'
  * executing "cd /home/yakjuly/cookbook.yakjuly.com/releases/20110330091501 && touch tmp/restart.txt"
    servers: ["yakjuly.com"]
    [yakjuly.com] executing command
    command finished in 548ms
Categories: dreamhost, linux, rails Tags:

swfupload_for_blade

March 19th, 2011 yakjuly No comments

rails的上传插件非常丰富,比较著名的有 attachment_fu, paperclip.

上传大文件时 需要显示进度 怎么办?办法有两种

 

1. 给nginx安装 nginx upload module 插件,并且使用 jquery-upload-progress 轮询nginx 并显示进度。

nginx upload module 插件能让nginx把上传的文件转移到服务器某个临时文件上,上传完毕后再给rails发一个请求。

优点:nginx完成了静态文件上传的动作,使rails进程只关注动态请求

该方案在 多层nginx映射的时候会有问题,出现 文件上传时 过一段时间 才能显示出进度,原因不明,而且轮询也会给nginx带来压力。

 

2.使用swfupload 在客户端显示上传进度。

缺点:需要rails进程处理文件上传

 

swfupload_for_blade

swfupload + paperclip,主要是使用rails g swfupload 快速生成 controller helper model view等代码,对不同的需求可以自己改代码 而不需要改插件。

使用方法:

Gemfile中添加代码

gem 'swfupload_for_blade'
gem 'paperclip'
gem 'mime-types', '1.16', :require => 'mime/types'

在application.rb中config的block中添加代码

config.autoload_paths += %W( #{config.root}/lib )
config.autoload_paths += %W( #{config.root}/app/middleware )

目的是加载attachable.rb 和 flash_sesson_cookie_middleware.rb

接着在config/initalizers/session_store.rb文件最后加上

Rails.application.config.middleware.insert_before(
  ActionDispatch::Session::CookieStore,
  FlashSessionCookieMiddleware,
  Rails.application.config.session_options[:key]
)

剩下的工作

rails g swfupload
rake db:migrate

class User < ActiveRecord::Base
  has_file
end

class Comment < ActiveRecord::Base
  has_files
end

in layout
<%= javascript_include_tag "jquery" %>

views
<%= swfupload_of(@user) %>
<%= swfupload_of(@comment) %>

目前已经更新 @user 在new 或者 created的情况下 一对一 ,一对多都能够上传文件。

Attachment表 需要定时删除没有关联的记录。

 

Categories: javascript, rails, ruby Tags:

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: