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
endAnd for comparison, a “Hello World” controller:
class OldPollerController < ApplicationController
def poller
render :text => "Hello World!"
end
endSo, 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!'
endFirst 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.rbThis 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
endFun 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
endTo 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
How do I unit test/integration test my metal code?
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)
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.
@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!
Dude, great write up.
The middleware section is dead on. Perfect!
I’m going to repeat Dr. Nic’s request about testing. How do you plan on testing metal endpoints?
Metal end points can be tested with integration testing. Those tests go through the entire stack.
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.
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:
You should also wrap the response string in an array in your metal for the same reason.
Thanks Nick, I’ve updated the examples in my post so that all elements of the response array
respond_to?(:each).@nick
ruby -e ” puts ‘string’.respond_to?(:each) ” #=> true
@ara: sure, but
ruby19 -e “puts ‘string’.respond_to?(:each)” #=> false
Just something to keep in mind.
24 hours later, API modified: http://github.com/rails/rails/commit/61a41154
:)
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!
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…
Thanks DHH and Jesse for that.
Way cool. Thank for this very sharp knife.
Great feature!!!
I’d love to use this feature.
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?
The benchmarks were incorrectly run in development mode the first time. They’ve been updated in the post with more accurate numbers.
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.
very good
This will be perfect for our syllable counter on Haiku Village
It might be a stretch, but auto complete endpoints could use this too.
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, {...”).
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!”]
“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?
@septik: not at all, that’s just a silly expression. I’m a huge fan of Sinatra.
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 endWhich is the Rails way ?
@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
@appinstance variable, then you’re probably wanting to build a true piece of Rack Middleware. These are loaded like you showed above: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?