When Developers Go To Great Length To Save Typing 4 Letters

Great Lengths

Heroku is a great platform. Long before I joined and when I say long, I mean in startup terms (i.e. a few weeks before I joined :)) – the decision was made that CrowdHired would be hosted on Heroku. Shortly after I came on board, Heroku released their new Cedar stack and we quickly migrated across to that. I find it kinda amusing that we're currently in alpha, deploying to a platform that's in beta. Latest and greatest FTW. While migrating to the new stack we also settled on Thin as our web server. The Cedar stack allows you to use whatever web server you want in production and will run on Webrick by default – not ideal. Since we were going to use Thin in production it made sense that we'd also use it in development instead of Webrick.

When you're using Rails, swapping a new web server in is pretty painless. Just include the gem and then use the rails s command to launch your new server e.g

rails s thin

Pretty simple right? Well, it is, but I am very used to typing rails s to launch my server and no matter what gem you include in your project, rails s still starts Webrick (this is not entirely accurate but bare with me). Muscle memory being what it is, after typing rails s instead of rails s thin a couple of times (and only realising this a few hours later) I decided to see if I could make Thin the default for the rails s command.

Digging Into The Rails Command Line

The key thing here was to figure out how rails s actually works – only way to do that is to read some code. We know that there is a script/rails executable that lives in all our Rails project so it makes sense that this would be the entry point into figuring out rails s, but in reality it's not (or at least it’s not that simple). We don’t actually type script/rails s, we do rails s, so there must be an executable called rails within the Rails gem itself which is declared as such in rails' gemspec. This is indeed the case, it looks like this:

#!/usr/bin/env ruby
 
if File.exists?(File.join(File.expand_path('../..', __FILE__), '.git'))
  railties_path = File.expand_path('../../railties/lib', __FILE__)
  $:.unshift(railties_path)
end
require "rails/cli"

But even that is not the start of the story. Apparently, when you have an executable in a gem, rubygems will not use it as is, but will generate another executable which wraps your one. For the rails command it looks like this:

#!/usr/bin/env ruby
#
# This file was generated by RubyGems.
#
# The application 'rails' is installed as part of a gem, and
# this file is here to facilitate running it.
#
 
require 'rubygems'
 
version = ">= 0"
 
if ARGV.first =~ /^_(.*)_$/ and Gem::Version.correct? $1 then
  version = $1
  ARGV.shift
end
 
gem 'rails', version
load Gem.bin_path('rails', 'rails', version)

This is the true entry point you hit when you type rails s. Of course, all this does is load/call the original executable from the Rails gem.

The Rails source is broken up into several projects such as activerecord, activesupport etc. Probably the most important one of these is railties. This is where the rails executable takes us. Of course, before it does that it needs to put the lib/ folder of the railties project on the load path, but eventually we end up in railties/lib/rails/cli.rb. Here we almost immediately execute the following:

Rails::ScriptRailsLoader.exec_script_rails!

All this does is essentially figure out if we're inside a Rails app and if we are it executes script/rails passing through the command line arguments that you supply. So, we're now back in our Rails app; script/rails is the real entry point after all (although we're about to be taken straight back to railties). The file looks like this:

#!/usr/bin/env ruby
# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
 
APP_PATH = File.expand_path('../../config/application',  __FILE__)
require File.expand_path('../../config/boot',  __FILE__)
require 'rails/commands'

We require boot.rb so that we can hook into Bundler and make sure the relevant gems are on the load path (such as rails for example), we then jump into railties/rails/lib/commands.rb. Here everything is pretty straight forward, we have a big case statement which has a clause for "server". We instantiate a new Rails::Server and start it, which tells us very little by itself, but if we jump into railties/rails/lib/commands/server.rb we can see that Rails::Server simply extends Rack::Server (and delegates to Rack::Server's start method from its start method) all it adds is those familiar lines we're all used to seeing e.g.:

=> Booting Thin
=> Rails 3.0.7 application starting in development on http://0.0.0.0:3000
=> Call with -d to detach
=> Ctrl-C to shutdown server

So, if we want to change which server is picked up by default when we type rails s we need to go look in Rack.

A Quick Glance At Rack

Luckily we can easily grab it from Github and crack it open (you have to admire the Ruby open source ecosystem, it is truly awesome, largely due to Github, so big props to those guys). We of course need to check out lib/rack/server.rb where we find the following method:

def server
  @_server ||= Rack::Handler.get(options[:server]) || Rack::Handler.default(options)
end

So, if we don't pass in the name of the server we want, Rack::Handler.default will try to determine it for us.

def self.default(options = {})
  # Guess.
  if ENV.include?("PHP_FCGI_CHILDREN")
    # We already speak FastCGI
    options.delete :File
    options.delete :Port
 
    Rack::Handler::FastCGI
  elsif ENV.include?("REQUEST_METHOD")
    Rack::Handler::CGI
  else
    begin
      Rack::Handler::Mongrel
    rescue LoadError
      Rack::Handler::WEBrick
    end
  end
end

As you can see, it turns out that the real default server is actually Mongrel. So if you included Mongrel in your Rails project, typing rails s would automatically pick that up without you having to do anything. Only if Mongrel fails, do we fall back to Webrick which is part of Ruby and therefore is always present. So what do we do if we want Thin to be one of the defaults? Well, first thing first, we need to check if Rack includes a handler for Thin. If we look in lib/rack/handlers/ we can see that Rack itself includes the following:

cgi.rb
evented_mongrel.rb
fastcgi.rb
lsws.rb
mongrel.rb
scgi.rb
swiftiplied_mongrel.rb
thin.rb
webrick.rb

Luckily Thin is there, so what we really want is that default method to look something like this:

def self.default(options = {})
  # Guess.
  if ENV.include?("PHP_FCGI_CHILDREN")
    # We already speak FastCGI
    options.delete :File
    options.delete :Port
 
    Rack::Handler::FastCGI
  elsif ENV.include?("REQUEST_METHOD")
    Rack::Handler::CGI
  else
    begin
      Rack::Handler::Thin
    rescue LoadError
      begin
        Rack::Handler::Mongrel
      rescue LoadError
        Rack::Handler::WEBrick
      end
    end
  end
end

This way Thin will be the first default, followed by Mongrel and only then falling through to Webrick. Luckily, since we're using Ruby, we can reopen the class and replace the method with our version. I think this is a perfect example where the ability to reopen classes comes in extremely handy, without any need to worry about "scary consequences".

Getting It Working

All we really need to figure out is where to put the code that reopens the class so that it gets picked up before Rails tries to launch the server. The only logical place is the script/rails executable itself, which will now look like this:

#!/usr/bin/env ruby
# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
 
APP_PATH = File.expand_path('../../config/application',  __FILE__)
require File.expand_path('../../config/boot',  __FILE__)
require 'rack/handler'
Rack::Handler.class_eval do
  def self.default(options = {})
    # Guess.
    if ENV.include?("PHP_FCGI_CHILDREN")
      # We already speak FastCGI
      options.delete :File
      options.delete :Port
 
      Rack::Handler::FastCGI
    elsif ENV.include?("REQUEST_METHOD")
      Rack::Handler::CGI
    else
      begin
        Rack::Handler::Mongrel
      rescue LoadError
        begin
          Rack::Handler::Thin
        rescue LoadError
          Rack::Handler::WEBrick
        end
      end
    end
  end
end
require 'rails/commands'

This works without any problems, we type rails s and as long as Thin is in our Gemfile it starts up by default. As an aside, notice that I used class_eval to reopen Rack::Handler. Metaprogramming tricks like this should be part of every Ruby developer's toolbox, I'll talk more about this some other time (seeing as I am well into TL;DR land here).

Going through this exercise didn't take long (under 30 minutes) and taught me a bit more about Rails and Rack. Shortly after doing this – in a curious case of the universe working in interesting ways – I came across a Stackoverflow question asking about this exact scenario and got an inordinate amount of satisfaction from being able to easily answer it :). In-fact, even the fact that shortly after Jason found Pow and we switched over to that, doesn't really diminish the satisfaction of quickly solving a seemingly difficult problem in a neat way. The lesson here is this, no matter what problems you come across don't automatically look for a library to handle it. Do spend a few minutes investigating – it might be enough to solve it (especially if you're using Ruby) and you'll certainly learn something.

Image by Gerard Stolk presque 64

  • http://www.reviewgang.com Manu

    Nice writeup :). Reading code has its rewards.

    There is a small typo. It should be “bear with me” and not “bare with me”

    • http://www.skorks.com Alan Skorkin

      Doh, so it should

  • Matt Conway

    If you want to avoid reopening the class, you could also just:

    ARGV << "thin"

    at start of script/rails

    • http://www.skorks.com Alan Skorkin

      I hadn’t even though of doing that, yeah you’re right it should work even if you provide some options (such as -d to deamonize it) – a very nice simple solution. Looking at it again now, it occurs to me that you could also override the default options method inside Rails::Server and provide the server that way:

      Rails::Server.class_eval do
        def default_options
              super.merge({
                :Port => 3000,
                :environment => (ENV['RAILS_ENV'] || "development").dup,
                :daemonize => false,
                :debugger => false,
                :pid => File.expand_path("tmp/pids/server.pid"),
                :config => File.expand_path("config.ru"),
                :server => "thin"
              })
          end
      end

      This is also simpler than my original solution (although not as simple as appending to ARGV), I guess I was just having too much fun digging into Rails and Rack trying to figure out how things hang together :).

      • Matt Conway

        I hear ya, always good to dig into how things work, never know when what you discover will come in handy :)

  • http://deadorange.com/blog Nick Hoffman

    Why not just create a Bash alias for “rails s thin”? Eg:

    alias rst=”bundle exec rails server thin”

    • http://www.skorks.com Alan Skorkin

      Hi Nick,

      No reason, creating an alias is a valid way to get around the issue. I do have a couple of niggles against it. Firstly it is creating a totally different command rather than using rails s which everyone knows, not a biggie but nevertheless.

      Secondly, every developer has to create one of these as it doesn’t live in the codebase but rather in your machine environment, also not a biggie but there it is.

      Lastly if you’re using RVM with a separate gemsets per project, when you jump into a different project which doesn’t have thin as one of the installed gems, your alias is still active and will blow up if you try to invoke it, of course you know not to do that but once again there it is.

      Solving the problem within the codebase avoids all these issues.

      • Drew

        You set out this blog post as a configuration of a personal preference and now are arguing that it is an issue that it wouldn’t apply to the whole team. Which is it?

        Creating an alias would actually save you *more* typing than your result because you can name it whatever you want. Further, regarding your last point, there’s no rule saying that it has to be an alias, it could be a bash script with some conditional environmental handling.

        Aliases or scripts are exactly the right answer here, changing expected defaults is not.

        Finally, I think your primary takeaway is nonsensical. It uncategorically states a position that isn’t defensible; there are absolutely correct times to use a library – you have proven this by using rails.

    • Sam Livingston-Gray

      Similarly, I heard about this on the Ruby Show, and my first thought was “Great lengths? How hard is it to stick `alias r=’rails’` in your bash profile?” (=

  • Mike Gehard

    Any thoughts about submitting that patch to the Rack folks so we all don’t have to monkey patch Rack if we want to use thin?

    • http://www.skorks.com Alan Skorkin

      I can certainly do that, although it seems funny to me that it hasn’t been done by someone already, mongrel has been out of vogue for a while now so it seems weird to keep it as a default. Maybe I’ll take a punt and submit it like you suggest, it will only take a few minutes.

  • http://twitter.com/batasrki Srdjan Pejic

    Nice hack. I’ve a few comments.

    Firstly, I notice both in the default Rack handler, as well as in the list of handlers available through Rack that there’s no handler for Unicorn and also that there are lot of servers listed that I’d have thought were dead, e.g. all the mongrel variations.

    Secondly, is there no better way than rescue to select a handler? Is there no check to be made if a Mongrel or a Thin handler is available before defaulting to Webrick? I mean the original code for that was a bit ugly and the addition of Thin uglified it more. I’m not sure if doing a begin/rescue block inside a begin/rescue block has a performance penalty, as well.

    Other than that, great look inside a small part of Rails internals.

    • http://www.skorks.com Alan Skorkin

      Hey,

      Yeah, I found it curious as well that so many of the handlers were for servers which are essentially on the out. As to Unicorn, I’ve never used it, but from the 2 second look I had, it doesn’t look like it has a Rack handler. In-fact it doesn’t seem to be the way that the Unicorn people want Unicorn to be used, rather you should be using the executable provided with the unicorn gem to launch the server (instead of doing rails s).

      You can definitely check (perhaps via const_get) if a particular class/handler is defined, so a begin/rescue is not the only option.

  • http://student.johnpdaigle.com J. Paul Daigle

    Is the primary takeaway from this blog post that you should never use a library, or that a library is not the correct solution to some, or even most problems? Where did you read that?

  • http://kevinelliott.net/ Kevin Elliott

    Have you considered using Pow? Then you wouldn’t need to use “rails s” at all :)

    • http://www.skorks.com Alan Skorkin

      Yeah we ‘arrived’ at using Pow eventually :).

  • http://timmatheson.com Tim Matheson

    I was just going to mention alias’s.
    You’re syntax seems a little off at least for OSX.

    alias “rails s”= “rails s thin”

    Should do the trick, I wouldn’t suggest changing rails, if you switch to unicorn tomorrow you have to maintain that.

    If your using .rvmrc you can just drop it in there so it stays with your project.

  • http://timmatheson.com Tim Matheson

    I was just going to mention alias’s.
    You’re syntax seems a little off at least for OSX.

    alias “rails s”= “rails s thin”

    Should do the trick, I wouldn’t suggest changing rails, if you switch to unicorn tomorrow you have to maintain that.

    If your using .rvmrc you can just drop it in there so it stays with your project.

  • http://devrandom.postr.hu/ karatedog

    With this alias, ‘rails s mongrel’ would be expanded to ‘rails s thin mongrel’, so you’ll never use another webserver before unalias…

  • http://devrandom.postr.hu/ karatedog

    That’s why we love ZSH :-):

    rails () {
    if [[ $# -eq 1 && $1 = "s" ]]
    then
    command rails s thin
    else
    command rails $*
    fi
    }