← notes

Deep dive: Rackup

February 25, 2020
ruby

Rack is a bridge between web servers and web applications. Most of the web frameworks in Ruby are based on Rack. In this article, I'd like to explore and demystify how its rackup tool works.

Diving deep into its source code revealed that some eval wizardry is being used. Here's a sneak-peak of its beauty.

# Evaluate the given +builder_script+ string in the context of
# a Rack::Builder block, returning a Rack application.
def self.new_from_string(builder_script, file = "(rackup)")
  # We want to build a variant of TOPLEVEL_BINDING with self as a Rack::Builder instance.
  # We cannot use instance_eval(String) as that would resolve constants differently.
  binding, builder =
    TOPLEVEL_BINDING.
      eval('Rack::Builder.new.instance_eval { [binding, self] }')
  eval builder_script, binding, file
  builder.to_app
end

I'll get to it later and explain how it works and why it's needed.

For now, let's get started with a "Hello World" example:

# in config.ru

run(proc { [200, { 'Content-Type' => 'text/plain' }, ['Hello World']] })

Running this with rackup will start Webrick and serve our little "Hello World" app on port 9292.

Getting started

It looks like running the rackup command somehow started the server and provided the run method to the top-level context. Let's find where it's coming from.

Looking at Rack's source code we discover this file sitting in a bin/ directory:

#!/usr/bin/env ruby
# frozen_string_literal: true

require "rack"
Rack::Server.start

Looks like we're starting the server here. Let's dig into the source code to find out what happens.

def start(&block)
  if options[:warn]
    $-w = true
  end

  if includes = options[:include]
    $LOAD_PATH.unshift(*includes)
  end

  Array(options[:require]).each do |library|
    require library
  end

  if options[:debug]
    $DEBUG = true
    require 'pp'
    p options[:server]
    pp wrapped_app
    pp app
  end

  check_pid! if options[:pid]

  # Touch the wrapped app, so that the config.ru is loaded before
  # daemonization (i.e. before chdir, etc).
  handle_profiling(options[:heapfile], options[:profile_mode], options[:profile_file]) do
    wrapped_app
  end

  daemonize_app if options[:daemonize]

  write_pid if options[:pid]

  trap(:INT) do
    if server.respond_to?(:shutdown)
      server.shutdown
    else
      exit
    end
  end

  server.run(wrapped_app, **options, &block)
end

Let's ignore server.run for a bit and dive deeper into the wrapped_app .

def wrapped_app
  @wrapped_app ||= build_app app
end

Let's also ignore build_app and focus on the app.

def app
  @app ||= options[:builder] ?
    build_app_from_string :
    build_app_and_options_from_config
end

We are not passing any options, so in our case we're interested in build_app_and_options_from_config.

def build_app_and_options_from_config
  if !::File.exist? options[:config]
    abort "configuration #{options[:config]} not found"
  end

  app, options = Rack::Builder.parse_file(self.options[:config],
                                          opt_parser)
  @options.merge!(options) { |key, old, new| old }
  app
end

This leads us to Rack::Builder.parse_file which explains it's purpose in the comments.

# Parse the given config file to get a Rack application.
#
# If the config file ends in +.ru+, it is treated as a
# rackup file and the contents will be treated as if
# specified inside a Rack::Builder block, using the given
# options.
#
# If the config file does not end in +.ru+, it is
# required and Rack will use the basename of the file
# to guess which constant will be the Rack application to run.
# The options given will be ignored in this case.
#
# Examples:
#
#   Rack::Builder.parse_file('config.ru')
#   # Rack application built using Rack::Builder.new
#
#   Rack::Builder.parse_file('app.rb')
#   # requires app.rb, which can be anywhere in Ruby's
#   # load path. After requiring, assumes App constant
#   # contains Rack application
#
#   Rack::Builder.parse_file('./my_app.rb')
#   # requires ./my_app.rb, which should be in the
#   # process's current directory.  After requiring,
#   # assumes MyApp constant contains Rack application
def self.parse_file(config, opts = Server::Options.new)
  if config.end_with?('.ru')
    return self.load_file(config, opts)
  else
    require config
    app = Object.const_get(::File.basename(config, '.rb').split('_').map(&:capitalize).join(''))
    return app, {}
  end
end

Our file ends with .ru, so let's look at Rack::Builder.load_file.

# Load the given file as a rackup file, treating the
# contents as if specified inside a Rack::Builder block.
#
# Treats the first comment at the beginning of a line
# that starts with a backslash as options similar to
# options passed on a rackup command line.
#
# Ignores content in the file after +__END__+, so that
# use of +__END__+ will not result in a syntax error.
#
# Example config.ru file:
#
#   $ cat config.ru
#
#   #\ -p 9393
#
#   use Rack::ContentLength
#   require './app.rb'
#   run App
def self.load_file(path, opts = Server::Options.new)
  options = {}

  cfgfile = ::File.read(path)
  cfgfile.slice!(/\A#{UTF_8_BOM}/) if cfgfile.encoding == Encoding::UTF_8

  if cfgfile[/^#\\(.*)/] && opts
    warn "Parsing options from the first comment line is deprecated!"
    options = opts.parse! $1.split(/\s+/)
  end

  cfgfile.sub!(/^__END__\n.*\Z/m, '')
  app = new_from_string cfgfile, path

  return app, options
end

Looks like Builder.new_from_string is the key for getting to bottom of this. Let's look it up.

# Evaluate the given +builder_script+ string in the context of
# a Rack::Builder block, returning a Rack application.
def self.new_from_string(builder_script, file = "(rackup)")
  # We want to build a variant of TOPLEVEL_BINDING with self as a Rack::Builder instance.
  # We cannot use instance_eval(String) as that would resolve constants differently.
  binding, builder =
    TOPLEVEL_BINDING.
      eval('Rack::Builder.new.instance_eval { [binding, self] }')
  eval builder_script, binding, file
  builder.to_app
end

Wow, look at that! This is where the magic happens. Let's dive into it.

Double eval magic

This code makes Rack::Builder instance a top level context and evaluates our script within that context. This provides the run method to our config.ru script, which is actually defined in Rack::Builder#run [1].

TOPLEVEL_BINDING is, simply, a binding of the top-level context[2]. Why do we need it here? Couldn't we simply write:

def self.new_from_string(builder_script, file = "(rackup)")
  bind, builder = Rack::Builder.new.instance_eval { [binding, self] }
  eval builder_script, bind, file
  builder.to_app
end

I was confused by this, so I've asked the author, Benoit Daloze, to explain it and he did. Not having TOPLEVEL_BINDING would give the Rack namespace to constants defined in our config.ru:

# config.ru

p Builder # would print out Rack::Builder

That's why we need to evaluate within TOPLEVEL_BINDING.

A simpler, and a previously used version of this magic is:

def self.new_from_string(builder_script, file = "(rackup)")
  eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app",
    TOPLEVEL_BINDING, file, 0
end

However, this means that magic comments from config.ru would be ignored, since they are not present at the beginning of the eval-ed string. We would have to check for them and add them at the beginning of the string like this:

def self.new_from_string(builder_script, file = "(rackup)")
  eval "# frozen_string_literal: true\nRack::Builder.new {\n" +
       builder_script + "\n}.to_app",
       TOPLEVEL_BINDING, file, 0
end

Benoit's solution to this is much more elegant.

Going deeper

Calling run in our config.ru script, means that our journey continues to Rack::Builder#run:

# Takes an argument that is an object that responds to #call and returns a Rack response.
# The simplest form of this is a lambda object:
#
#   run lambda { |env| [200, { "Content-Type" => "text/plain" }, ["OK"]] }
#
# However this could also be a class:
#
#   class Heartbeat
#     def self.call(env)
#      [200, { "Content-Type" => "text/plain" }, ["OK"]]
#     end
#   end
#
#   run Heartbeat
def run(app)
  @run = app
end

Since there are no other method calls to follow, it looks like we've come the end. Let's continue looking at branches that we've ignored previously.

The last thing we ignored was build_app in wrapped_app[]:

def build_app(app)
  middleware[options[:environment]].reverse_each do |middleware|
    middleware = middleware.call(self) if middleware.respond_to?(:call)
    next unless middleware
    klass, *args = middleware
    app = klass.new(app, *args)
  end
  app
end

Inspecting middleware further leads us to:

def middleware
  default_middleware_by_environment
end

def default_middleware_by_environment
  m = Hash.new {|h, k| h[k] = []}
  m["deployment"] = [
    [Rack::ContentLength],
    logging_middleware,
    [Rack::TempfileReaper]
  ]
  m["development"] = [
    [Rack::ContentLength],
    logging_middleware,
    [Rack::ShowExceptions],
    [Rack::Lint],
    [Rack::TempfileReaper]
  ]

  m
end

wrapped_app is going to become a chain of middleware that's going to look like this:

Rack::ContentLength ⟶
Rack::CommonLogger ⟶
Rack::ShowExceptions ⟶
Rack::Lint ⟶
Rack::TempfileReaper ⟶
our app

The next thing we ignored is server.run[].

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

  unless @_server
    @_server = Rack::Handler.default

    # We already speak FastCGI
    @ignore_options = [:File, :Port] if @_server.to_s == 'Rack::Handler::FastCGI'
  end

  @_server
end

The default Rack handler is Webrick, so in our case, server.run is actually Rack::Handler::Webrick.run:

def self.run(app, **options)
  environment  = ENV['RACK_ENV'] || 'development'
  default_host = environment == 'development' ? 'localhost' : nil

  if !options[:BindAddress] || options[:Host]
    options[:BindAddress] = options.delete(:Host) || default_host
  end
  options[:Port] ||= 8080
  if options[:SSLEnable]
    require 'webrick/https'
  end

  @server = ::WEBrick::HTTPServer.new(options)
  @server.mount "/", Rack::Handler::WEBrick, app
  yield @server  if block_given?
  @server.start
end

This simply starts the Webrick server and passes our middleware to it as app.

And that's it! This article hopefully demystified what happens behind the curtain when you run rackup and you hopefully better understand how it works now.


Notes and examples

  1. Here's the simplified version of what Rack::Builder.new_from_string is doing:

    class Context
      def test
        puts "testing"
      end
    end
    
    bind = Context.new.instance_eval { binding }
    eval "test", bind
    

    The first argument for eval is our simplified config.ru. Running this will print out "testing".

  2. I'm just kidding. It's not "simply". Let me explain what the hell I am talking about. Ruby documentation says that Binding "encapsulates the execution context at some particular place in the code and retain this context for future use". TOPLEVEL_BINDING is simply Binding of the top level-context. Here's an example:

    foo = 42
    
    def print_foo
      print foo # undefined local variable or method `foo' for main:Object
    end
    begin; print_foo; rescue => ex; puts ex.message; end
    
    def print_foo_with_top_level_binding
      print TOPLEVEL_BINDING.eval("foo") # 42
    end
    print_foo_with_top_level_binding