Learning EventMachine

This post is part of my weekly tech learning series, where I take one hour each week to try out a piece of technology that I'd like to learn.

This week I decided to try out EventMachine: a fast, simple event-processing library for Ruby.

I have to say, I was pretty impressed. Both by its documentation and also how easy it was to get started.

Running Log

I decided to stick with my running log idea from last week. It's a simple app idea that is different enough from the standard todo list examples used everywhere.

Getting started

Right away I found links to EventMachine's wiki which was filled with an introduction and some code snippets. I did get stuck for a bit, but that is completely my fault and my own personal bias towards reading code instead of a description of that code.

Simple Prototype

To prevent getting stuck like last week with backbone.js I decided to try a quick prototype before I started on the app. That way there would be less code to debug if there was a problem.

But there was a problem.

I was trying to build a simple server that would accept input and return it reversed (e.g. "Hello" turns into "olleH"). I copied the first example from the code snippets, started the server, and tried to telnet to it:

$ telnet 127.0.0.1 8081 Trying 127.0.0.1... telnet: Unable to connect to remote host: Connection refused

Uh oh. I checked the server, it was still running, no exceptions raised. I tried telnet again, still nothing.

That's when I pulled out my old sysadmin knowledge and ran `netstat -luntp`:

Active Internet connections (only servers) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN - tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN - tcp 0 0 0.0.0.0:25 0.0.0.0:* LISTEN - tcp 0 0 0.0.0.0:443 0.0.0.0:* LISTEN - tcp 0 0 0.0.0.0:17500 0.0.0.0:* LISTEN 10838/dropbox tcp 0 0 0.0.0.0:24800 0.0.0.0:* LISTEN 20984/synergys tcp 0 0 0.0.0.0:60198 0.0.0.0:* LISTEN 8382/skype

Truncated a bit

This command lists every process that is listening on a TCP or UDP socket, its interface address (localhost or ip addresses), as well as the process id and program name.

That's when I noticed that my server wasn't even listening on a port.

Going back to my code I started inspecting line by line to see what was wrong. I even tried to copy some other example EventMachine servers and ran them (most of them worked).

Finally I found my problem.

I used the first example from the Code Snippets which is under the Client Example.

After cursing I then realized that my intuition was correct at the beginning when I though EventMachine.connect was an odd method name for starting a server.

Lesson #1: Read ALL of the documentation, not just the code.

Lesson #2: If an API seems confusing, double check that you are not reading it incorrectly.

So one change from #connect to #start_server and my server booted, showed up in netstat, and I could telnet to it.

#!/usr/bin/env ruby require 'rubygems' require 'eventmachine'

class Reverser < EventMachine::Connection def post_init send_data ">> Ready\n" end

def receive_data(data) send_data data.strip.reverse + "\n" end

end

EventMachine.run { EventMachine.start_server '127.0.0.1', 8081, Reverser puts "Running on port 8081" }

Building the Running Log

Now that I've prototyped and got EventMachine running completely it was time to start on my Running Log app. I knew I wouldn't have enough time to implement a complete CRUD type of system so I settled on the ability to add a run and then list all of the runs that have been added (the CR__ in CRUD).

Tests

Since I was working in Ruby, I also decided to use TDD to make sure I don't misstep along the way. I'm running 1.9.3 so using minitest was the perfect fit. I also didn't want to mess with multiple files so I embedded the test directly into the main server file. Not only will this keep the implementation and test in one place but it also serves as some basic internal documentation.

The only problem was that I usually used the __FILE__ == $0 trick to embed tests but that wouldn't work with EventMachine because I was executing the file directly.

[Sidebar]

__FILE__ == $0 checks if the file is getting executed directly, such as ruby your_file.rb. The technique is to code your library like normal and wrap your tests in that check. So running the file directly will run the tests while using require 'your_file' will load the library without the tests.

class YourClass end

if __FILE__ == $0

Your testing code

end

[End sidebar]

Instead I just decided to uses ARGVs so the server starts when the first argument is "start", otherwise to run the test suite.

Since EventMachine lets you separate your implementation logic into a module, I was able to easily include that module in a test class and not have to worry about testing much of EventMachine itself. This made testing cycle a lot faster.

Running Log Methods

Since I was only concerned with two public APIs for the server I was able to keep the implementation pretty slim:

You can see the implementations below, #add_run_from_user is the most complex because I'm doing the data parsing and validation inline. In actual production code I'd probably split it up to be clearer.

All in all there was about 50 lines of implementation code and close to 100 lines of test code which covered some of the common use cases.

Screencast

Below is a screencast of me walking through the code and how the app works.

Summary

Working with EventMachine was quite fun. I know I'm not really using it to it's full power but it's nice to know that there wouldn't be that much else I'd have to do to scale it up in production.

My favorite part was how well it separated the network server part from the application code. You can see in my test, the RunLogWrapper class is not using EventMachine but manages to pull in my entire implementation for testing.

I could see EventMachine being used for running web services and acting as glue servers for other systems. I'm definitely happy I spent I hour to play with EventMachine and learn the basics.

Code

Here is the full code for the server along with the embedded tests.

#!/usr/bin/env ruby require 'rubygems' require 'eventmachine'

module RunLog def receive_data(data) if data.match(/list/i) list_runs else add_run_from_user(data) end end

def list_runs output = runs.inject("") do |o, run| o += "Date: #{run[:date]} | Distance: #{run[:distance]} | Duration: #{run[:duration]} | Pace: #{run[:pace]} | Comment: #{run[:comment]}\n" o end send_data output end

def add_run_from_user(data) run_data = { :date => "", :distance => "", :duration => "", :pace => "", :comment => "" } date, distance, duration, pace, comment = data.split(',') run_data[:date] = date.strip if date run_data[:distance] = distance.strip if distance run_data[:duration] = duration.strip if duration run_data[:pace] = pace.strip if pace run_data[:comment] = comment.strip if comment

if run\_data.any? {|key, value| !value.nil? && value != ""}
  add\_run(run\_data)
  send\_data "OK\\n"
else
  send\_data "INPUT ERROR\\n"
end

end

def runs @runs || [] end

def add_run(run) @runs ||= [] @runs << run end end

command = ARGV.shift

case command when "start" EventMachine.run { EventMachine.start_server '127.0.0.1', 8081, RunLog puts "Running on port 8081" } else require 'minitest/autorun'

class RunLogWrapper include RunLog

attr\_reader :output\_buffer

# Used to stub EM's methods
def send\_data(\*args)
  @output\_buffer ||= ""
  @output\_buffer << args.join("\\n")
end

end

class TestRunLog < MiniTest::Unit::TestCase def setup @runlog = RunLogWrapper.new end

def test\_sanity
  assert\_equal 4, 2 + 2
end

def test\_receive\_data\_with\_list\_command
  @runlog.add\_run({
    :date => "2012-09-14",
    :distance => "3mi",
    :duration => "30:00",
    :pace => "10:00",
    :comment => "Nice and easy run"
  })

  @runlog.receive\_data("list")

  assert\_equal "Date: 2012-09-14 | Distance: 3mi | Duration: 30:00 | Pace: 10:00 | Comment: Nice and easy run\\n", @runlog.output\_buffer
end

def test\_receive\_data\_with\_good\_input
  @runlog.receive\_data("2012-09-14, 3mi, 30:00, 10:00, Nice and easy run")

  assert\_equal 1, @runlog.runs.length
  expected\_run = {
    :date => "2012-09-14",
    :distance => "3mi",
    :duration => "30:00",
    :pace => "10:00",
    :comment => "Nice and easy run"
  }
  assert\_equal expected\_run, @runlog.runs.first

end

def test\_receive\_data\_with\_empty\_input
  @runlog.receive\_data("\\n")

  assert\_equal 0, @runlog.runs.length
end

def test\_receive\_data\_with\_missing\_fields
  @runlog.receive\_data("2012-09-14, 3mi, 30:00")

  assert\_equal 1, @runlog.runs.length
  expected\_run = {
    :date => "2012-09-14",
    :distance => "3mi",
    :duration => "30:00",
    :pace => "",
    :comment => ""
  }
  assert\_equal expected\_run, @runlog.runs.first

end

def test\_receive\_data\_with\_malformed\_input
  @runlog.receive\_data("This input, isn't quite, right, but, it still is accepted, for now")

  assert\_equal 1, @runlog.runs.length
  expected\_run = {
    :date => "This input",
    :distance => "isn't quite",
    :duration => "right",
    :pace => "but",
    :comment => "it still is accepted"
  }
  assert\_equal expected\_run, @runlog.runs.first

end

end

end

Topics: Event machine Processes Ruby Server Tech learning

Would you like a daily tip about Shopify?

Each tip includes a way to improve your store: customer analysis, analytics, customer acquisition, CRO... plus plenty of puns and amazing alliterations.