A bug fix for Rails 2.3 Stable: 关于测试环境下有时不能正确写入Cookie的问题
解决ActiveRecord经常要reload的一种方案

感觉Rack::Builder就是个奇怪的存在

bachue posted @ 2013年3月16日 01:09 in Ruby with tags rails sinatra middleware rack ruby , 3506 阅读

为什么这么说呢?从没有见过运用如此广泛,几乎是所有Ruby Web Framework基础的开源项目中,这个文件里竟然有如此之多令人费解的部分(完全可以认为是Bug)

简单的举个例子吧,首先,对于Rack来说,.ru文件和.rb文件的区别在哪里?

def self.parse_file(config, opts = Server::Options.new)
  options = {}
  if config =~ /\.ru$/
    cfgfile = ::File.read(config)
    if cfgfile[/^#\\(.*)/] && opts
      options = opts.parse! $1.split(/\s+/)
    end
    cfgfile.sub!(/^__END__\n.*\Z/m, '')
    app = new_from_string cfgfile, config
  else
    require config
    app = Object.const_get(::File.basename(config, '.rb').capitalize)
  end
  return app, options
end

代码中告诉了我们答案,如果是.ru文件,Rack::Builder不仅仅会将其读出,而且还会从中解析出以#\开头的部分,作为Rack::Server启动的选项,举个例子:

#\--port 3000
run Proc.new {|env| [200, {"Content-Type" => "text/html"}, "Hello Rack!"]}

比如这个config.ru文件,rackup在执行这个文件是将启动出3000端口的服务器而非9292。

这本来是个不错的设计啦,但是仔细看代码就会发现,只支持一个选项,第二个选项用方括号运算符是匹配不到的,额。。。你妹。。

不得不承认Rack::Builder支持map包含map,也就是形成树结构,这点不错。更重要的是Rack仅仅执行那些不得不执行的map的代码块,如果路由没匹配到就不予执行这些都很好。但是当你发现Rack::Builder没接收到一个请求就会创建一个新的URLMap对象然后把路由计算成正则表达式,并且计算出的正则表达式不予缓存,每次都重新计算,就会感觉之前争取到的性能在这里又浪费了的惋惜啊。。好吧,这一定是考虑到开发模式下的需求。

接着就是最坑的一个Bug了

require 'rack'

infinity = Proc.new {|env| [200, {"Content-Type" => "text/html"}, 'root']}
builder = Rack::Builder.new do
  map '/' do
    run infinity
  end

  use Rack::CommonLogger

  map '/version' do
    map '/' do
      run Proc.new {|env| [200, {"Content-Type" => "text/html"}, "infinity 0.1"] }
    end

    map '/last' do
      run Proc.new {|env| [200, {"Content-Type" => "text/html"}, "infinity beta 0.0"] }
    end
  end
end
Rack::Handler::Thin.run builder, :Port => 9292
curl 0.0.0.0:9292/version/last

虽然看上去结果应该是infinity beta 0.0,但实际上结果是root,额。。

这个Bug的关键在于use Rack::CommonLogger这句语句的位置,可以看到,它写在两个map的中间。而use方法是这么实现的:

def use(middleware, *args, &block)
  if @map
    mapping, @map = @map, nil
    @use << proc { |app| generate_map app, mapping }
  end
  @use << proc { |app| middleware.new(app, *args, &block) }
end

为了让middleware处于middleware stack的外面,代码先判断当前有没有执行过map方法,如果执行过的话,先将它放进middleware stack,外面放middleware对象。这样执行的时候就能先执行到middleware了(大概作者是这么想的,但是这么做完全是错的,实际情况是先放进middleware stack的先被执行到)。如果你没有执行过map方法,那么虽然现在middleware stack上看上去只有middleware对象,但是等会执行的时候会把map包进来,这样就解决问题了。但是它显然没有想到,如果我把use写两个map中间的情况。此时,middleware stack对象会先把第一个map写进去,然后跟着middleware。然后在下一个map执行的时候,重新赋值map对象,并在最后被middleware stack的两个元素包住。这样请求一进来的时候就会发现,竟然是第一个map直接接受请求,连middleware都不是(之前讲过,实际情况是先写进middleware stack的先被执行到)。

算起来好像只有把use语句写在所有map之前的时候才是能真正正常工作的。大概确实很少人不这么做吧,否则这么明显的问题为何不被解决呢。

虽然我们可以看到,Rack::Builder实现了自己的路由和middleware调用方法,但是无论是Rails还是Sinatra,都无一基于Rack::Builder,不仅仅是因为Rack::Builder没有更多的功能,已有功能也不容易扩展,而且,里面坑太多了,还不如自己从头写呢。

pavzi.com 说:
2024年1月09日 14:05

Pavzi.com provides all the news about Gadgets, the Economy, Technology, Business, Finance and many more. The main concept or our aim behind this website has been the will to provide resources with full information on each topic which can be accessed through the Internet. To ensure that every reader gets what is important and worthy about the topic they search and link to hear from us. pavzi.com Our site is a multiple Niche or category website which will ensure to provide information and resources on each and every topic. Some of the evergreen topics you will see on our website are Career, Job Recruitment, Educational, Technology, Reviews and others. We are targeting mostly so it is true that Tech, Finance, and Product Reviews. The only reason we have started this website is to make this site the need for your daily search use.


登录 *


loading captcha image...
(输入验证码)
or Ctrl+Enter