There are times when Rails feels very heavy! Jason Seifer has joked for ages about how Rails can’t scale, and we all know that’s a fallacy, but there are times when you know a bit of code is simple and could be really lightweight but having it as part of a controller, having gone through the Rails routing process and all the filters would just slow it down.
Well, at the end of 2008 Rail Metal was introduced and it came like a breath of fresh air. It simply works as a lightweight class dropped in to /app/metal/ that is given the opportunity to handle a request before the main Rails stack. If it returns a 404 HTTP status it is taken to mean that it wasn’t handled and it should continue up in to the next Metal or Rails, if it returns anything else the request stops there.
What is less obvious is that you can use it to handle things you want to happen on any request in a very lightweight manner (you may have auditing requirements and may have other metals running so a before_filter wouldn’t cut it!). You’d simply run your code anyway, then return a 404 HTTP status to continue up the stack.
The problem comes when you want to do something after the Rails stack runs (or before it). In this case you don’t get the opportunity with Rails Metal, but there is another feature that came along at the same time – Rails Middleware. It works in a similar way to Metals except that the class is registered in environment.rb using config.middleware.use “MyMiddleware” and you can choose where to put it in the stack of middleware to be executed. When it’s run, you get passed an instance to the application, you take some action (if you like), run the application, process the output in some way (if you like) and return it up the stack to the next Middleware.
When I first started reading about Rails Metal/Middleware it seemed like a great idea, but it didn’t really offer anything with a real tangible benefit. How wrong I was! I’ve now had the opportunity to use it on a large project and it really does simplify a lot of things. For example, a client of mine has a problem with their Reverse Proxy trimming response headers for PUT requests. This is wrong, but we found that our Rails app was sending multiple cookie values for a given name (e.g. if in our code we set the same cookie name with multiple values, each one would send it’s own header).
Now this is probably something that, given enough time, we could fix/monkey patch in the Rails source – but it was a lot easier/simpler to write/test a Middleware class that reads the HTTP headers that Rails has generated, sort the Set-Cookie headers in to a hash (thereby only keeping the last update for a given name) and regenerate them. You might ask why we were sending multiple cookie values, the reason is that we have a class that allows us to treat a cookie like an ActiveRecord store, and multiple updates to the “database” during a request send multiple headers. It is what it is…
We also wanted to implement a caching solution. Ideally, Rails in-built full page caching solution would be perfect – but the problem comes when you have a multi-lingual application where the user’s choice of language can be from one of:
In these cases (except the first one) the problem is that the page is cached within public/ but it would cache the language as the first visitor had it.
Therefore we went with a Middleware solution that uses Memcached and the various methods of determining a language to create a key (it also uses POST parameters for certain URLs where AJAX is used for filtering forms but the underlying data rarely changes) and then either returns cached content or it creates then caches the content. This solution works really well, enabling us to support large amounts of page impressions while maintaining the flexibility we require. We also use a portion of the key with a random integer in it to act as a namespace, we just change this upon restarting the application to ensure we don’t return data from a previous deployment version.
We use Metal for simpler things such as visitor/customer throttling (the client wants to maintain a high level of availability for authenticated customers so may apply a limit to the number of visitors if a DoS attack happens) or resetting a keep alive when logged in (customers are logged out if inactive for a period).
While this article shows some of the situations where Rails Metal/Middleware has come to the rescue for me, I would recommend ensuring you need it rather than willy-nilly creating lots of Metal/Middleware – it’s not as obvious to new developers that there’s special processing that happens in these places.
I’d be interested to hear in the comments if you’re using them or not (and if you are, what for?).