Introducing Cramp 22

Posted by pratik
on Thursday, January 07

Cramp is the latest entry on the ruby web frameworks list. However, unlike all the others, Cramp is an asynchronous framework, always running inside EventMachine reactor loop. Cramp isn’t a good fit for most of the web applications out there. However, Cramp is good at holding and working with a large number of open connections. Hence it’ll work great for things like comet, long polling, streaming API or even when your application needs to handle thousands of concurrent connections.

This article assumes that you’re aware with the evented programming model. If you are not, things below this point might not make much of a sense. If you’re interested in learning, you could start by reading about EventMachine and Twisted.

Install

As Cramp requires 0.2.0-pre Arel and 3.0-pre versions of ActiveSupport and ActiveModel. So you’ll have to install them first. This step will be irrelevant after Rails 3 gets released. But for now, the following should install them :

gem install arel --pre
gem install activemodel --pre

And then,

gem install cramp

That’ll install Cramp gem along with all the needed dependencies. Please note that Cramp depends on ActiveSupport 3.0-pre gem, which isn’t backwards compatible and this may affect any other gems requiring ActiveSupport without specifying the version number.

Two faces of Cramp

Cramp comes with two layers : Controller & Model.

1) Cramp::Controller

Cramp::Controller is an asynchronous controller layer, that tries being rack compliant as much as possible. Currently, you must use thin or Rainbows! in order to run it. If you’re using Rainbows!, make sure you use EventMachine concurrency model.

Here’s the “hello world” of Cramp::Controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
require 'rubygems'
require 'cramp/controller'

class WelcomeAction < Cramp::Controller::Action
  on_start :send_hello_world

  def send_hello_world
    render "Hello World"
    finish
  end
end

Rack::Handler::Thin.run WelcomeAction, :Port => 3000

Now run it directly :

ruby welcome_action.rb

Here WelcomeAction is the rack endpoint, which you can use with config.ru or what have you.

Now let’s dig dipper to understand the code snippet above.

When serving a request, a Cramp::Controller::Action object goes through the following four stages :

  • Initialization – This is the initial stage of an action when a request is received. During this stage, it’s possible to abort the request along with whatever headers/body you wish to send. Hence, it is typically useful for checking permissions and validating the request etc.
  • Response Initialization – If the request doesn’t get aborted during the initialization stage, Cramp::Controller::Action enters the second stage, where response headers and a deferrable body are sent to the client. It’s important to note that as the headers are already sent here, following stages will only be able to send body and not able to change the headers.
  • Starting – This is where the actual work happens. From this stage, you can send body to the clients as many times as you wish to or finish the request.
  • FinishingCramp::Controller::Action enters this stage if the request is marked as finished during the Starting stage or if the client closes the connection. This stage is useful for cleanup activities or anything else you may wish to do upon request completion.

To hook into each of these states, Cramp::Controller::Action provides the following callbacks/methods :

before_start

before_start callback provides hook into the initialization stage. Each before_start callback accepts a block and must call yield ( or block.call – whichever is appropriate ) or halt. Here’s what a typical usage looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
before_start :verify_id, :find_user

def verify_id
  if params[:id] !~ /\A\d+\z/
    halt 500, {'Content-Type' => 'text/plain'}, "Invalid ID"
  else
    yield
  end
end

def find_user
  User.where(User[:id].eq(params[:id])).first do |user|
    if @user = user
      yield
    else
      halt 404, {}, "User not found"
    end
  end
end

As you can see above, halt takes status, headers and body as parameters and sends them to the client. It would also halt the callback chain and the request itself. Please note that on_finish callbacks will not get run when you halt. Calling yield will continue the filter chain or enter the next stage if no filters are left to run.

respond_with

After the before_start stage, Cramp::Controller::Action enters the next stage of Response Initialization. It’ll call the method respond_with, which must return an array of [status, headers]. If this method is not defined, [200, {‘Content-Type’ => ‘text/html’}] status and headers will be used by default.

Here’s an example of using respond_with to send custom status and headers :

1
2
3
4
def respond_with
  content_type = params[:format] == 'xml' ? 'application/xml' : 'application/json'
  [200, {'Content-Type' => content_type}]
end

A deferrable body is also sent out along with these headers.

on_start

After all the verifications and sending out headers, the real work starts. on_start callbacks provide multiple entry points into the Starting stage. These callbacks can send any body to the client using render() method or finish the request by invoking finish(). Note that the render() method can be called any number of times.

Here’s a full example imitating running two long running sql queries from on_start callbacks:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
require 'rubygems'
require 'cramp/controller'
require 'cramp/model'

Cramp::Model.init(:username => 'root', :database => 'arel_development')

class SleepingAction < Cramp::Controller::Action
  on_start :start_sleeping

  def start_sleeping
    Cramp::Model.select 'select sleep(1)', method(:on_first_sleep)
  end

  def on_first_sleep
    render "First Sleep Complete...\n"
    render "Going to sleep one more time now..\n"

    Cramp::Model.select 'select sleep(1)', method(:on_second_sleep)
  end

  def on_second_sleep
    render "Second Sleep Complete. Time to finish!\n"
    finish
  end
end

Rack::Handler::Thin.run SleepingAction, :Port => 3000

Now if you hit this using curl:

[lifo@null ~]$ curl http://0.0.0.0:3000/
First Sleep Complete...
Going to sleep one more time now..
Second Sleep Complete. Time to finish!
[lifo@null ~]$

on_finish

on_finish provides hook into the last stage : Finishing. These callbacks are run when you call finish() from an on_start callback or when the client closes the connection. These callbacks are the perfect place for any cleaning up activities.

Here’s an example of a Cramp::Controller::Action using periodical timer and an on_finish callback.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
require 'rubygems'
require 'cramp/controller'
require 'cramp/model'

class PollUserAction < Cramp::Controller::Action
  on_start :start_polling
  on_finish :stop_polling

  def start_polling
    # Call find_user every 10 seconds
    @timer = EventMachine::PeriodicTimer.new(2) { find_user }
  end

  def find_user
    User.first {|u| render "#{u.inspect}\n" }
  end

  def stop_polling
    puts "Cancelling the timer.."
    @timer.cancel
  end
end

Rack::Handler::Thin.run PollUserAction, :Port => 3000

In the case above, if you don’t call @timer.cancel, it’ll keep running even after the client closes the connection. The following section will cover the helper method provided by Cramp::Controller for the above pattern called periodic_timer.

Helper Methods

Periodic Timers

The on_finish callback example above has a very common pattern : Starting periodic timers using on_start and cleaning them up using on_finish. Cramp::Controller provides a better alternative : periodic_timer

Using periodic_timer to rewrite the example above :

1
2
3
4
5
6
7
class PollUserAction < Cramp::Controller::Action
  periodic_timer :poll_user, :every => 2

  def poll_user
    User.first {|u| render "#{u.inspect}\n" }
  end
end

And then Cramp::Controller will take care of starting and stopping EventMachine::PeriodicTimer from the appropriate stages.

You can use more than one periodic timers as well :

1
2
3
4
5
6
7
8
9
10
11
12
13
class PollUserAction < Cramp::Controller::Action
  periodic_timer :poll_user, :every => 2
  periodic_timer :check_limit_exceed, :every => 10

  def poll_user
    ..
  end

  def check_limit_exceed
    finish if request_limit_exceeded
  end

end

In the example above, check_limit_exceed() calls finish() if the request limit is exceeded, which in turn will terminate the connection and stop all the timers too.

Keeping Connection Alive

If you’re using Cramp for streaming or long polling, you’d want to make sure the client doesn’t close the connection prematurely. Cramp::Controller has a handy helper method to make sure that doesn’t happen – keep_connection_alive

keep_connection_alive sends the client an empty string (” “) every 15 seconds by default.

1
2
3
4
class PollUserAction < Cramp::Controller::Action
  periodic_timer :poll_user, :every => 2
  keep_connection_alive
end

Or you can change the period by supplying :every option :

1
2
3
4
class PollUserAction < Cramp::Controller::Action
  periodic_timer :poll_user, :every => 2
  keep_connection_alive, :every => 30
end

Routing, Request and Parameters

Cramp::Controller is capable of using a Rack middlewares for routing request and populating params. Here’s an example of a Cramp::Controller using Usher for routing :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
require 'rubygems'
require 'cramp/controller'

class HomeController < Cramp::Controller::Action
  on_start :send_hello_world

  def send_hello_world
    render "Hello World with #{params[:id]}"
    finish
  end
end

routes = Usher::Interface.for(:rack) do
  add('/:id').to(HomeController)
end

Rack::Handler::Thin.run routes, :Port => 3000

Cramp::Controller::Action implements the params method like:

1
2
3
def params
  @params ||= @env['usher.params']
end

If you’re using another rack based router like rack-mount, you might want to override that with the appropriate definition.

As Cramp::Controller tries being a Rack based framework, you should be able to use Rails middlewares for cookies and sessions stores.

Views

Cramp::Controller does not have any built in mechanism for rendering files. However, using something like tilt should be very straight forward.

Testing

Cramp::Controller comes with a few helper methods to write integration tests for the application. However, that’s not within the scope of this article. You should have a read through http://github.com/lifo/cramp/tree/master/test/controller to know more.

Benchmarks

I haven’t really benchmarked Hello World for Cramp::Controller. However it should be very fast. I was able to have a single Cramp::Controller instance stream to 2000+ concurrent connections just fine. And this is where Cramp::Controller really shines, and not some silly Hello World masturbations.

Also, note that OS X isn’t the best environment for testing concurrent connections, you should be using Linux with a tuned Kernel for this. Check this article for details on tuning the Kernel.

Gotchas

Even though Cramp::Controller tries being as much Rack compliant as possible, it’s not a 100% Rack compliant framework. Rack specs are designed primarily for synchronous frameworks. As Cramp::Controller uses deferrable body, any middlewares operating on the response body will not work without modifications.

2) Cramp::Model

Cramp::Model is an asynchronous ORM ( only MySQL supported at the moment ) built on top of ARel and ActiveModel. It’s currently in a very primitive state and provides the following features :

  • Validations using ActiveModel
  • CRUD operations
  • AR/ARel like Finder methods

Here’s what a Cramp model looks like :

1
2
3
4
5
6
7
8
9
10
11
require 'rubygems'
require 'cramp/model'

Cramp::Model.init(:username => 'root', :database => 'cramp_development')

class User < Cramp::Model::Base
  attribute :id, :type => Integer, :primary_key => true
  attribute :name

  validates_presence_of :name
end

You are required to declare all the columns you wish to use as Attributes. If type is not supplied, String type is assumed.

Currently Cramp::Model provides the following finder method :

  • all
  • first
  • each

It also provides the following methods for specifying options on the find :

  • where
  • select
  • group
  • order
  • limit
  • offset

The above methods are delegated to Arel and can be chained. As Cramp::Model is an asynchronous ORM, you must supply a callback method for processing the result records. You could do that by calling all, first or each at the end of the chain and supply the callback as a block or a method.

Here are some example usages :

1
2
3
4
5
6
7
8
9
10
11
EM.run do
  User.select(:id, :name).limit(10).offset(20).first {|u| .. }
  User.where(User[:name].eq('Lush')).limit(2).all {|users| ... }
  User.each {|user| .. }

  def lush_users_found(users)
    ...
  end

  User.where(User[:name].eq('Lush'), User[:id].gt(5)).all method(:lush_users_found)
end

For the basic CRUD operations, Cramp::Model provides methods similar to Model#save, which accepts an optional callback. If a callback is provided, it’ll be called after the completion with a Status object, containing metadata about the success or failure of the save operation. Status object has just two methods defined on it : record & success?.

Here’s how you would typically want to save a record to the database :

1
2
3
4
5
6
7
8
9
10
11
12
13
EM.run do
  def user_saved(status)
    if status.success?
      ...
    else
      user = status.record
      puts "Oops. Found errors : #{status.record.errors.inspect}"
    end
  end

  user = User.new
  user.save method(:user_saved)
end

Contributing to Cramp

Cramp is hosted on Github http://github.com/lifo/cramp. Please do fork and use the issues if you encounter a bug or need any kind of help.

UPDATE 1 : Add instructions to install arel 0.2.0-pre

UPDATE 2 : Rainbows! work with cramp!

Comments

Leave a response

  1. Norbert CrombachJanuary 07, 2010 @ 02:24 PM

    Looking good!

  2. LenaryJanuary 07, 2010 @ 02:38 PM

    “Or you can change the period by supplying very option :” when talking about “keep_connection_alive” – surely you mean “every”

  3. PratikJanuary 07, 2010 @ 02:57 PM

    @Lenary Fixed that, thanks for the heads up.

  4. Abhishek ParolkarJanuary 07, 2010 @ 03:31 PM

    its cool.. I clearly see its great future with websockets

  5. GregJanuary 07, 2010 @ 05:34 PM

    Sweet. Is there anything that would prevent Cramp::Model from being used standalone in a non-web EventMachine context (an AMQP client daemon, for instance)? It looks like it should work, based on a quick read-through of the code, but I may be missing something.

  6. GregJanuary 07, 2010 @ 05:42 PM

    Never mind, looks like http://github.com/lifo/cramp/blob/master/examples/orm.rb answered my question.

  7. I'm DarioJanuary 07, 2010 @ 09:50 PM

    Awesome. I will keep an eye on Cramp because I’m playing with highly concurrent apps in different languages.

  8. brianthecoderJanuary 08, 2010 @ 04:13 AM

    Any sample js interacting with this?

  9. Tony ArcieriJanuary 08, 2010 @ 06:49 AM

    Have you checked out Rev (http://github.com/tarcieri/rev)? I ask because one of the most common requests in async web frameworks in Ruby is to be able to proxy responses, which may come off high speed local networks. Unfortunately, due to the way EventMachine is designed when you attempt this the entire response is buffered into memory, which fails miserably if the response is large. Rev is designed so you can spoonfeed the response buffer.

  10. tronJanuary 08, 2010 @ 08:56 PM

    I’m getting:

    /usr/local/lib/site_ruby/1.8/rubygems.rb:280:in `activate’: can’t activate activesupport (= 2.3.5, runtime) for [“activerecord-2.3.5”], already activated activesupport-3.0.pre for [“cramp-0.6”] (Gem::LoadError)

    when trying to require ‘cramp/model’. It looks like Cramp loads activesupport-3.0.pre but then the Arel gem attempts to load ActiveRecord which tries loading ActiveSupport 2.3.5 (or whatever version corresponds with the installed version of AR), causing the failure.

  11. PratikJanuary 08, 2010 @ 09:14 PM

    Hey tron,

    Could you try updating your arel gem ? Latest version of arel doesn’t require AR. So that should fix this.

    Thanks!

  12. tronJanuary 08, 2010 @ 09:31 PM

    @Pratik:

    Really? Because I see AR being required here – http://github.com/rails/arel/blob/752813016ed227ecfbe0bf69c92de2e2c3e7a988/lib/arel.rb. Am I missing something?

  13. PratikJanuary 08, 2010 @ 09:49 PM

    Hey,

    My apologies. You are correct. I was under the false impression that that was fixed/pushed. But just realized I had been using monkey patched ARel locally.

    I’ll get this fixed asap. Thanks for reporting it !

  14. AkitaOnRailsJanuary 12, 2010 @ 11:18 PM

    If anyone is interested, I’ve developed a small proof-of-concept app implementing a naive-chat. http://github.com/akitaonrails/cramp_chat_demo/

    Pull requests are welcome :-)

  15. rogerJanuary 26, 2010 @ 12:23 AM

    Have you considered using fibers to be able to interrupt the work flow while it yields, thus enabling users to not have to write only blocks (a la neverblock)?

    -r

  16. PratikJanuary 26, 2010 @ 07:49 AM

    Hey Roger,

    I haven’t really played much with fibers. Are there any good example libraries to get one started ?

    Thanks!

  17. roger rubygemsJanuary 27, 2010 @ 02:23 AM

    libraries that use fibers…hmm..revactor uses them, never block does, http://oldmoe.blogspot.com/2008/07/untwisting-event-loop.html explains how they “could” be used in a web server (replace asymy with mysql_plus, though). I think thin has an evented option….

    GL. -r

  18. MakotoMarch 14, 2010 @ 02:01 AM

    Hi. Pratik.

    Are there any reasons why EventMachine concurrency model needs to be specified when setting up Rainbows! ? Did you encounter any specific problems when you choose different concurrency models?

    Thanks

  19. PratikMarch 14, 2010 @ 10:42 AM

    Makoto,

    Two reasons mostly :

    1) Some of the Cramp features uses EM for timers/periodic timers etc. 2) I have no experience with the other concurrency models :)

  20. MakotoMarch 14, 2010 @ 07:46 PM

    Thank you for your info.

    I was wondering what the problem was. Sounds like we don’t know unless we try. I’m currently trying Rainbows various concurrency model (using Sunshowers), so may try if that works out.

  21. Charles MelhornApril 24, 2010 @ 11:59 PM

    Hi Pratik,

    FYI, “gem install arel—pre” and “gem install activemodel—pre” currently install activesupport-3.0.0.beta3 and activemodel-3.0.0.beta3 respectively. That causes the installation of the Cramp gem to fail because it’s looking for the 3.0.0.beta (not ‘beta3’) versions of ActiveSupport and ActiveModel.

    Charles

  22. Pedro TeixeiraJune 09, 2010 @ 12:51 AM

    Hi Pratik,

    I followed your instructions and I’m having problems trying to boot cramp on rails 2.3.8:

    can’t activate activesupport (= 3.0.0.beta4, runtime) for [], already activated activesupport-2.3.8 for [“rails-2.3.8”]

    any clues?

    Thanks.

    -Pedro

Comment