Rails Metal: a micro-framework with the power of Rails: \m/

Updates:

  • Clarified the distinction between Rails Metal and Rack middleware after more information from @Josh in the comments. Thanks!
  • Read more about metal from DHH on the Official Rails Blog.
  • Demonstrate Testing Metal end points
  • Update Poller example to match new style
  • Cover Sinatra Integration
  • Correct benchmarks

Josh Peek committed a new feature to Edge Rails today: Rails Metal. After the recent work to replace Rails’ crufty request processing code with Rack and integrate its middleware support, Rails Metal is a logical progression that allows Rails apps to use the power of Rack middleware to create super-fast actions.

For example, here’s a sample “Hello World” Metal:

  class Poller < Rails::Rack::Metal
    def call(env)
      if env["PATH_INFO"] =~ /^\/poller/
        [200, {"Content-Type" => "text/html"}, "Hello, World!"]
      else
        [404, {"Content-Type" => "text/html"}, "Not Found"]
      end
    end
  end

And for comparison, a “Hello World” controller:

    class OldPollerController < ApplicationController
      def poller
        render :text => "Hello World!"
      end
    end

So, let’s fire up ruby script/server and see what this gives us:

  # traditional Controller
  $ curl 127.0.0.1:3000/old_poller/poller
    Hello World!

  # the new Metal
  $ curl 127.0.0.1:3000/poller
    Hello World!

So, the point of all of these other “micro-frameworks” is that they’re faster than Rails, right? Let’s benchmark this new “Hello World” Metal:

  # first, let's benchmark the traditional controller
  $ ab -n 1000 http://127.0.0.1:3000/old_poller/poller
  ... snip ...
  Requests per second:    408.45 [#/sec] (mean)
  Time per request:       2.448 [ms] (mean)

  # now for the Metal middleware
  $ ab -n 1000 http://127.0.0.1:3000/poller
  ... snip ...
  Requests per second:    1154.66 [#/sec] (mean)
  Time per request:       0.866 [ms] (mean)

For this trivial “Hello World” benchmark, Rails Metal is 2.8x faster than a Controller. Awesome. Have a couple actions of your app you need to optimize? Instead of breaking them out into a separate application using a micro-framework, add a Metal inside your existing app. You get the performance benefits of processing requests outside of ActionPack, and it’s all integrated as a part of your Rails app. Easy!

Sinatra Metal

You can now also use Sinatra to create Metal end points:

  Sinatra::Application.default_options.merge!(:run => false, :env => 
  :production)
  Api = Sinatra.application unless defined? Api

  get '/interesting/new/ideas' do
    'Hello Sinatra!'
  end

First person to show the use of a Merb app as a Metal end point wins a prize.

Standalone Execution

Additionaly, Rails Metal are able to be executed in a separate process from your Rails application using rackup:

  rackup -s mongrel app/metal/poller.rb

This runs the Poller Metal separeately from Rails, on it’s own port (rackup defaults to 9292). This is perfect if you have an action that’s taking a very long time (for example a file upload) that you’d like to split out from the normal Rails request processing queue.

Testing Metal

Update: After several people commented asking how to test metal, DHH chimed in and recommend Integration Testing for Metal end points, as they hit the whole stack, and I submitted a patch cleaning up the Integration Testing behavior of Metal. Testing Metal end points now works just like any other Integration test:

      class PollerTest < ActionController::IntegrationTest
        test "poller returns hello world" do
          get "/poller"
          assert_response 200
          assert_response :success
          assert_response :ok
          assert_equal "Hello World!", response.body
        end
      end

Fun With Middleware

So, essentially, Rails Metal is a thin wrapper around Rails’ new Rack middleware support. Rack middleware is pretty powerful stuff: framework-independent components that process requests independently or in concert with other middleware. For example, here’s a simple piece of Rack middleware that runs a regex on responses:

class RegexMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    status, headers, response = @app.call(env)
    new_response = []
    response.each do |part|
      new_response << part.gsub(/World/, 'Middleware')
    end
    [status, headers, new_response]
  end
end

To use this rack middleware in Rails, add this line to your environment.rb

Rails::Initializer.run do |config|
  ...
  config.middleware.use RegexMiddleware
end 

Restart your server, and check out what happens:

  $ curl 127.0.0.1:3000/poller
    Hello Middleware!

The Rack middleware filtered the output of the Metal we created before. This works with output generated by normal controllers and everything too. The possible uses of this pattern are endless:

  • Single Sign On
  • Request/Response Signing (think OAuth)
  • Asset Compression

rack-contrib is a nice collection of Rack middleware if you’re interested in more examples.

Rails Metal is a simple wrapper around the existing (yet undocumented) Rack middleware support in Edge Rails that attempts to DRY the process of using middleware to create endpoints (like a poller) as opposed to filters (which are better implemented as traditional middleware, like the examples above). For example, Rails Metal might be used:

  • To speed up a ‘poller’ action called by all active users of a popular web-based chat application every 3 seconds (hint: Campfire).
  • To improve the performance of any API endpoint
  • To process file uploads outside of the Rails request queue
  • To authorize delivery of cached content

We don’t need no stinking micro-frameworks

With the additional of Metal and Rack middleware support, Rails effectively includes a micro-framework of its own; one that either tighly integrates with Rails or is executed separately – whichever the need dictates.

This is a great response by the Rails team to all of the buzz surrouding micro-frameworks: a micro-framework with the power of Rails. I’m definitely going to try this approach to squeeze a couple extra requests per second out of a heavily trafficked API call – let me know in the comments if you find a use for it.

Read up a bit more on Rack and then take a look at Josh’s commit Introducing Rails Metal (and the ensuing comments) if you’re interested for more information.

Comments

Leave a response

  1. Dr Nic had this to say Tue, 16 Dec 2008 22:48:03 GMT

    How do I unit test/integration test my metal code?

  2. Joshua Peek had this to say Tue, 16 Dec 2008 22:56:12 GMT

    Awesome write up.

    Not that you can’t, but metal != middleware. Metal bits are designed to be endpoints.

    Mainly because we already have a middleware api. (I think it got lost in one of the big commit. So my fault)

    config.middleware.use Rack::Cache

    is the preferred way to inject middleware into the request pipeline.

    The reason for Rails::Rack::Metal subclass is to make standalone endpoints correctly. You don’t really need it for normal middleware.

    I know there is alot of confusion right now, docs sucks, my fault. But awesome post. Its cool to see you got it out so fast.

  3. Jesse Newland had this to say Tue, 16 Dec 2008 23:17:18 GMT

    @Josh – thanks for the clarification. I’ve removed the portion about middleware and will follow this post up with another one about the new middleware API soon. Kickass work on Metal!

  4. Brendan Schwartz had this to say Wed, 17 Dec 2008 02:15:37 GMT

    Dude, great write up.

  5. Joshua Peek had this to say Wed, 17 Dec 2008 05:02:12 GMT

    The middleware section is dead on. Perfect!

  6. Michael Christenson II had this to say Wed, 17 Dec 2008 12:35:47 GMT

    I’m going to repeat Dr. Nic’s request about testing. How do you plan on testing metal endpoints?

  7. DHH had this to say Wed, 17 Dec 2008 12:55:06 GMT

    Metal end points can be tested with integration testing. Those tests go through the entire stack.

  8. Jesse Newland had this to say Wed, 17 Dec 2008 14:38:36 GMT

    Actually integration testing doesn’t work out of the box with Metal end points. I’ve just added a patch to Lighthouse that makes it possible to test Metal end points just like any other Integration test.

  9. Nick Sieger had this to say Wed, 17 Dec 2008 14:51:58 GMT

    Minor nit, but the Rack spec says that the third element of the response array is an object that responds to #each. So your regex matcher should probably be:

    
    def call(env)
      status, headers, response = @app.call(env)
      new_response = []
      response.each do |part|
        new_response << part.gsub(/World/, 'Middleware')
      end
      [status, headers, new_response]
    end
    
    

    You should also wrap the response string in an array in your metal for the same reason.

  10. Jesse Newland had this to say Wed, 17 Dec 2008 15:03:42 GMT

    Thanks Nick, I’ve updated the examples in my post so that all elements of the response array respond_to?(:each).

  11. ara.t.howard had this to say Wed, 17 Dec 2008 15:39:07 GMT

    @nick

    ruby -e ” puts ‘string’.respond_to?(:each) ” #=> true

  12. Nick Sieger had this to say Wed, 17 Dec 2008 15:58:04 GMT

    @ara: sure, but

    ruby19 -e “puts ‘string’.respond_to?(:each)” #=> false

    Just something to keep in mind.

  13. Joshua Peek had this to say Wed, 17 Dec 2008 16:30:40 GMT

    24 hours later, API modified: http://github.com/rails/rails/commit/61a41154

    :)

  14. Randy Parker had this to say Wed, 17 Dec 2008 20:38:52 GMT

    Hard to say which makes me smile wider – your thorough description, or the ease with which I can move critical requests from Rails into sub-millisecond niche, without derailing my head from the rest of my Ruby development!

  15. Andrew had this to say Thu, 18 Dec 2008 04:01:32 GMT

    Uhh, what computer are you running your tests on? With helloworld test through traditional controller on Rails 2.2.2, I get 200req/sec with Webrick and Ruby 1.8, and over 500req/sec with Mongrel on Ruby 1.9…

  16. Michael Christenson II had this to say Thu, 18 Dec 2008 04:16:41 GMT

    Thanks DHH and Jesse for that.

  17. Johannes Fahrenkrug had this to say Thu, 18 Dec 2008 14:20:36 GMT

    Way cool. Thank for this very sharp knife.

  18. Akhil Bansal had this to say Thu, 18 Dec 2008 15:29:39 GMT

    Great feature!!!

    I’d love to use this feature.

  19. David Reese had this to say Fri, 19 Dec 2008 04:34:11 GMT

    Great summary!

    But I second Andrew’s question about your rails benchmarks… with production settings (cache classes, etc), my rails stack does ‘hello world’ at 2-3 ms, and metal comes in at just under 1ms. D2H twittered with similar numbers. Just wondering if your numbers overhype the speedup, or if i’m underestimating somehow?

  20. Jesse Newland had this to say Fri, 19 Dec 2008 18:01:01 GMT

    The benchmarks were incorrectly run in development mode the first time. They’ve been updated in the post with more accurate numbers.

  21. Bala Paranj had this to say Sat, 20 Dec 2008 01:31:18 GMT

    Just confirming, can I use this feature for interacting with payment gateways? Will it use fewer resources? I also would be interested in hearing about using this with Passenger.

  22. leftround had this to say Mon, 22 Dec 2008 11:30:20 GMT

    very good

  23. Matthew Higgins had this to say Tue, 23 Dec 2008 01:03:20 GMT

    This will be perfect for our syllable counter on Haiku Village

    It might be a stretch, but auto complete endpoints could use this too.

  24. mikong had this to say Sun, 04 Jan 2009 15:28:30 GMT

    The Poller class example seems to be incorrect. I was only able to make it work after changing the call method to a class method (i.e. self.call), and by removing the 200 and 404 status out of its own array (e.g. from “[[200], {...” to “[200, {...”).

  25. Matt Todd had this to say Wed, 07 Jan 2009 16:47:18 GMT

    Hey, the Poller example incorrectly wraps the status in an array which will cause a NoMethodError since it tries to call to_i on the first element of the response (in this case, [200]).

    It should be something like:

    [200, {“Content-Type” => “text/html”}, “Hello, World!”]

  26. septik had this to say Mon, 23 Feb 2009 13:34:12 GMT

    “We don’t need no stinking micro-frameworks”

    Are you saying Sinatra, Ramaze, etc stink so that’s why we will re-invent the wheel with Metal?

  27. Jesse Newland had this to say Mon, 23 Feb 2009 13:40:47 GMT

    @septik: not at all, that’s just a silly expression. I’m a huge fan of Sinatra.

  28. Gudata had this to say Fri, 27 Mar 2009 13:37:28 GMT

    Do youknow how to get the ”@app” in a metal?

    If I relay on rails to load my metal from the app/metal the initialize is skipped.

    But if I load it from environment.rb with

    config.middleware.insert_before ActionController::Failsafe, ProcessesList

    it works.

    require(File.dirname(__FILE__) + "/../../config/environment") unless defined?(Rails)
    
    class ProcessesList
      def initialize(app)
        @app = app
      end
    
      def self.call(env)
        if env["PATH_INFO"] =~ /^\/processes_list/
          [200, {"Content-Type" => "text/html"}, ["I got my app: #{@app.inspect}"]]
        else
          [404, {"Content-Type" => "text/html"}, ["Not Found"]]
        end
      end
    end
    

    Which is the Rails way ?

  29. Jesse Newland had this to say Fri, 27 Mar 2009 14:05:13 GMT

    @Gudata: Metal are designed to be a sub-set of Rack Middleware for use as super-fast endpoints. If you’d like to get at the @app instance variable, then you’re probably wanting to build a true piece of Rack Middleware. These are loaded like you showed above:

    config.middleware.insert_before ActionController::Failsafe, ProcessesList
    
  30. Gudata had this to say Mon, 30 Mar 2009 07:23:25 GMT

    Yes, thats what we have done, but then you should NOT keep the “metal” file into the metals folder because then it is loaded automagicaly from Rails.

    Please give your suggestion where is ok to keep your true Rack Middlewares ? I mean in the project/directory structure?

Your Comments