0-60: Deploying Goliath on Heroku Cedar

Earlier this week Heroku rolled out a major upgrade to their webstack. The HTTP/1.1 support and the billing upgrades are both great improvements, but the "process model" definitely takes the crown: all of the sudden Heroku is much more than a Ruby+Rack hosting platform. Prior to this release, Heroku hosted Ruby only apps on top of the Thin web-server. Each "dyno" represented an instance of Thin, and as long as you deployed a Rack-compatible app, then Heroku would spin it up and run it for you. This was great, but also somewhat limiting - for one, you couldn't run non-Rack apps!

The process model changes all that. You can now run any application within their cloud, with the help of a simple Procfile. In fact, it doesn't even have to be Ruby! Node, Clojure apps, long-running Ruby workers, it is all fair game.

Goliath on Heroku Cedar

Goliath is an async web server and framework we developed at PostRank. While it does follow the Rack spec, it is also substantially different: it requires Ruby 1.9 runtime for Fiber support, and it uses an entirely different HTTP parser from Thin. Combined, these differences meant that up until now, deploying Goliath on any existing cloud environment was a no-go. However, with the new cedar stack, this is no longer an issue:

source :gemcutter

gem 'goliath', :git => 'git://github.com/postrank-labs/goliath.git'
gem 'em-http-request', :git => 'git://github.com/igrigorik/em-http-request.git'
gem 'em-synchrony', :git => 'git://github.com/igrigorik/em-synchrony.git'
gem 'yajl-ruby'
require 'goliath'
require 'em-synchrony/em-http'
require 'em-http/middleware/json_response'
require 'yajl'

# automatically parse the JSON HTTP response
EM::HttpRequest.use EventMachine::Middleware::JSONResponse

class Hello < Goliath::API

  # parse query params and auto format JSON response
  use Goliath::Rack::Params
  use Goliath::Rack::Formatters::JSON
  use Goliath::Rack::Render

  def response(env)
    resp = nil

    if params['query']
      # simple GitHub API proxy example
      logger.info "Starting request for #{params['query']}"
      conn = EM::HttpRequest.new("http://github.com/api/v2/json/repos/search/#{params['query']}").get
      logger.info "Received #{conn.response_header.status} from Github"
      resp = conn.response
      resp = env # output the Goalith environment

    [200, {'Content-Type' => 'application/json'}, resp]
web: bundle exec ruby hello.rb -sv -e prod -p $PORT
goliath - Goliath is an async Ruby web server framework

A simple "Hello World HTTP proxy" example. Our API will parse an incoming request, dispatch an async HTTP request and return the results back to the user - no callbacks! We specified our libraries in a Gemfile, and we provided a Procfile with the startup parameters for our API. Believe it or not, that is all what we need. To deploy it:

$ git init .
$ git add . && git commit -a -m 'hello world'
$ heroku create --stack cedar goliath-demo
$ git push heroku master
$ curl http://goliath.herokuapp.com/
$ curl http://goliath.herokuapp.com/?query=eventmachine

Running a simple benchmark and checking the logs (heroku logs) shows that an average request to our new Goliath API takes approximately 0.35ms (2500+ req/s). Not enough to handle the load? Simply start another API: heroku scale web=2.

Goliath, Heroku & the Cloud

Combine easy deployment, support for HTTP/1.1 and the async nature of Goliath, and all of the sudden you can develop and deploy simple streaming API's and async API endpoints with near minimal effort! If you can't tell, definitely something I am very excited about - a huge step for Heroku! Now, we just need to wait for a response from the CloudFoundry team - this should get interesting.

Ilya GrigorikIlya Grigorik is a web ecosystem engineer, author of High Performance Browser Networking (O'Reilly), and Principal Engineer at Shopify — follow on Twitter.