Archive

Archive for the ‘ruby’ 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:

补习metaclass

March 29th, 2011 yakjuly No comments
1. class << A 是什么意思?
2. class << self 又是怎样的呢?
1. 首先,Ruby中创建(定义)一个类有两种方式:
a) 定义一个(适合于)普通的类
class ClassName
   body
end
b) 为某一个具体的对象定义其特定的类(singleton class, 或者叫metaclass,类的singleton class就是metaclass?)
class << object                    # 这里define 的就是singleton class
   body                             # body里要定义 的method 就是 singleton method
end
而这个例子就是第二种情况 – 事实上,这是在定义一个class A 的singleton class,而这个singleton class的作用就是存放对class A有意义的class method。
需要插一句的是,这种做法的存在又基于: in Ruby, a class is just a instance of class Class。所以,我们不仅仅可以为object来定义针对它自身的类,还可以为class来定义针对它自身的类 – 因为它们其实都是某个类的instance。
虽然这里定义的是class method,但是由于scope是在class A这个实例的class里面,所以我们事实上是在定义这个singleton class的instance method。
所以,这里的method name 是不需要class name 为前缀的。
事实上,这好比:
def A.[classMethodName_1]
   method body here
end
def A.[classMethodName_2]
  method body here
end
又或者是
def self.[classMethodName]
  method body here
end
2. 这个要看context,就是看这个self的作用域在哪里 – 具体就是这个语句在什么地方出现。
而一般来说,举例会有:
 

class A
   class << self
     def methodForA
        puts "singleton method(class method) for A"
     end
  end
end

 

而这个是完全等同于:
 

class A
   def self.methodForA
     puts "singleton method(class method) for A"
  end
end

 

谁先定义,谁被覆盖,谁后定义,谁起作用。
class A
  puts self
  puts(class << self; self; end)
end

# => A
# => # <Class:A>

As it is cleared from the output that inside the class A, self is reflecting the class A itself whereas class << self; self; end is <Class:A>, so what is the exact difference ?
Lets exemplify it…

class A

  self.module_eval do
    define_method :wish do
      puts “hello instance method”
    end
  end

  (class << self; self; end).module_eval do
    define_method :wish do
      puts “hello class method”
    end
  end

end

A.wish # => hello class method
A.new.wish # => hello instance method

class B

  self.module_eval do
    define_method :wish do
      puts “hello instance method”
    end
  end

end

B.new.wish # => hello instance method
B.wish # => undefined method `wish’ for B:Class (NoMethodError)

I suppose that it is clear now, in case if it is not than look at this… The following code will do exactly same as above but the class method wish will be available to all classes or we can say to all instances of class Class

class Class

  self.module_eval do
    define_method :wish do
      puts “hello — this is class method for instance of class Class”
    end
  end

end

class A

  self.module_eval do
    define_method :wish do
      puts “hello instance method”
    end
  end

end

A.wish # => hello — this is class method for instance of class Class
A.new.wish # => hello instance method

class B

  self.module_eval do
    define_method :wish do
      puts “hello instance method”
    end
  end

end

B.wish # => hello — this is class method for instance of class Class
B.new.wish # => hello instance method
Categories: ruby Tags:

nodeJS 事件驱动模型 多线程

March 29th, 2011 yakjuly No comments

这几天一直关注nodeJS,看了很多关于nodejs的评论 以及example代码。

十几篇博文看下来 我的脑袋里突然多了很多东西,记下来 继续深入。

现在http不能完全满足 浏览器与服务器的交互,常用的方法 1.长轮询 2.websocket 3.flash socket

虽然现在html5在移动设备上很火,但是在pc端还没有攻占下来。

flash socket属于hack的一种 不算正道

长轮询,客户端始终发一个请求到服务器端,服务器端有内容则立即返回,没有内容则保持连接 不返回结果,等到有内容时再返回结果,客户端得到结果后再发请求。 这样客户端与服务器端就一直保持着一个连接,可以用来做web IM, web game 等

但是 这种长轮询 无论在客户端有没有需要得到内容得时候都需要和服务器端保持一个空连接,大量的并发和连接 服务器是一大考验。

apache vs nginx: 基于线程模型 vs 事件驱动模型

稍微回顾一下请求Request 这一环节的过程。现今多数的Web 服务器中,有一条新的链接就会申请一条线程来负责处理至到这个Request 周期结束,接着执行其他流程。可以想象,成千上万个链接便有成千上万条线程(Thread-spawning )。每条线程姑且以堆栈2MB 的消耗去计算,一条条线程它们的累加都是不小的数目。如何优化和改进本身就是一个大问题,此外,使用系统线程,必须考虑线程锁的问题,否则造成堵塞主进程又是一个令人操心的难题。

nodejs的优势 正是应对长轮询的压力,nodejs的优势的原因是采用 事件驱动模型 ,什么是事件驱动模型呢?

总之不是直接的某个函数method()去执行(那是同步的方式,Node.js也支持),而是写回调callback

例如:

function test(){

  var result = doSomething();
  ..do something else..
}

在这段代码中在doSomething的时候 result只能等待结果,等待后才能接下来执行程序。

function test(){
  $.ajax({
    url: xxx,
    success: doSomething
  })

  .. do something else
}

这段代码应该很熟悉,jquery调用ajax请求,这段代码中ajax请求通过传递一个callback作为参数,不需要等待请求结果返回再执行something else,这就是我理解的事件驱动模型。

事件驱动模型的特点:某些方法返回结果更快,程序的执行不再用blocking方式写。

在这里也顺便了解了下ruby1.9引入的新概念 Fiber

Fiber VS Thread

Ruby的Fiber是一种控制结构,它的运行方式和操作方法很像thread。

Fiber Thread
not concurrency concurrency
resume, suspend(call Fiber.yield或者等到block结束) resume, suspend
return and give control back to caller not return or give control back unless join it 

 

 

fib = Fiber.new do
   f1 = f2 = 1
   loop do
     Fiber.yield f1 (每次resume,在遇到Fiber.yield或者block结尾时结束)
     f1, f2 = f2, f1 + f2 (第二次resume,从这里开始)
   end
end  

10.times { puts fib.resume }

ruby-1.9.2-p136 > 10.times {puts fib.resume }
1
1
2
3
5
8
13
21
34
55

另一种实现方法

def local_proc
  f1 = f2 = 1
  return Proc.new {f1, f2 = f2, f1 + f2; p f1;}
end
proc1 = local_proc
10.times {proc1.call}

上 面两个方法的实现方式是很不相同的。Fiber方式通过 enable the automatic conversion of internal iterators, such as each, to enumerator or external iterators来实现。(就是允许转换为 内部的 循环序列)

 

参考资料:http://blog.csdn.net/zhangxin09/archive/2010/08/25/5836777.aspx

http://andyhu1007.javaeye.com/blog/634916

http://www.tbdata.org/archives/tag/nginx

http://pragdave.blogs.pragprog.com/pragdave/2007/12/pipelines-using.html

http://davidwalsh.name/websocket

https://github.com/joyent/node/wiki/Projects,-Applications,-and-Companies-Using-Node

Categories: javascript, ruby 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: