ruby-mine

exploring the mine

Ruby/Eventmachine

von cypher am 01.04.2008 (20 Uhr)

Wie ihr vielleicht gemerkt habt gab es letze Woche wieder keinen Artikel. Ich möchte mich dafür entschuldigen und werde bis auf weiteres nur alle zwei Wochen einen neuen Artikel schreiben, da sich das zeitlich bei mir sonst kaum ausgeht.

Was?

Ruby/Eventmachine implementiert ereignisgesteuerte Ein- und Ausgabe für Netzwerkprogrammierung in Ruby. Eventmachine zielt dabei vor allem auf hohe Skalierbarkeit, Stabilität und Performanz, aber auch darauf eine API bereitzustellen die möglichst einfach zu verwenden ist. Eventmachine selber steht in 3 Varianten zur Verfügung: Purem Ruby, das ohne weitere Abhängigkeiten läuft, Ruby mit einer C++-Extension und einer Version für JRuby, die in Java geschrieben ist.

Installieren & Einrichten

Eventmachine ist wie gewohnt per Rubygems verfügbar:

gem install eventmachine

Wenn man die C++-Extension verwenden möchte, muss ein C/C++-Compiler installiert sein, und Debian-User müssen wie gewohnt das ruby-dev-Paket installiert haben. Falls das nicht der Fall ist, oder das Kompilieren der Erweiterung fehlschlägt, steht nur die reine Ruby-Version zur Verfügung.

Verwenden

Ein einfacher Echo-Server (ein Server der alles zurückschickt was er empfängt) ist in Eventmachine schnell implementiert:

1
2
3
4
5
6
7
8
9
10
11
12
require 'rubygems'
require 'eventmachine'

module EchoServer
  def receive_data( data )
    send_data data
  end
end

EventMachine.run {
  EventMachine.start_server "0.0.0.0", 8081, EchoServer
}

Hier sieht man auch die Grundstruktur von Eventmachine: Man require'd zuerst Eventmachine, und ruft dann EventMachine.run auf, welche einen Block erwartet. In diesem Block schreibt man den Code für die ereignisgesteuerte Ein/Ausgabe. In unserem Beispiel starten wir nur einen Server.

EventMachine erzeugt für jede Verbindung zum Server ein Connection-Objekt. Dieses Objekt inkludiert automatisch das Modul dass wir start_server mitgegeben haben (man kann aber auch eine Klasse erstellen die von EventMachine::Connection ableitet). EventMachine selber ruft dabei nur 3 Methoden auf: post_init (Die Verbindung wurde aufgebaut und initialisiert), receive_data (Es wurden Daten empfangen) und unbind (Die Verbindung wurde geschlossen). Die restlichen Methoden stellt EventMachine zu unserer Verfügung bereit.

Das EventMachine-Modul selber stellt ebenfalls einiges an Methoden bereit. Davon sind einige besonders von Interesse: run und stop_event_loop, connect und close_connection, add_timer und add_periodic_timer, defer sowie start_server und stop_server.

run kennen wir schon vom Echo-Server. stop_event_loop macht genau das was der Name verspricht, und kümmert sich darum dass alle Destruktoren ausgeführt werden.

connect und close_connection öffnen und schliessen eine Connection. Dazu ein (leicht modifiziertes) Beispiel aus der Dokumentation, welches auch gleich die oben erwähnten post_init und unbind-Methoden demonstriert:

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
28
29
30
31
require 'rubygems'
require 'eventmachine'

module DumbHttpClient

  def post_init
    send_data "GET / HTTP/1.1\r\nHost: _\r\n\r\n"
    @data = ""
  end

  def receive_data data
    @data << data
    if  @data =~ /[\n][\r]*[\n]/m
      puts "RECEIVED HTTP HEADER:"
      $`.each {|line| puts ">>> #{line}" }

      puts "Now we'll terminate the loop, which will also close the connection"
      EventMachine.stop_event_loop
    end
  end

  def unbind
    puts "A connection has terminated"
  end

end # DumbHttpClient

EventMachine.run {
  EventMachine.connect "forum.ruby-portal.de", 80, DumbHttpClient
}
puts "The event loop has ended"

Für add_timer, add_periodic_timer, start_server und stop_server verweise ich einfach auf die Dokumentation, nachdem auch diese Methoden genau das machen was ihr Name aussagt.

Deferrables

Eine Methode ist noch übrig: defer. Um den Sinn hinter 'Deferring' zu verstehen muss man wissen dass ereignisgesteuerte Programme einen Nachteil haben: Solange wir auf ein Ereignis reagieren, kann EventMachine keine neuen Ereignisse signalisieren (wie z.B. eine neue Verbindung). Deshalb sind z.B. die Webserver Thin und Evented Mongrel, die beide auf EventMachine basieren (Ebb ist ebenfalls event-driven, basiert aber auf libev), in Vergleichstest mit Mongrel schneller solange ein Request sehr kurz ist, haben aber massive Performanzprobleme mit langen Requests (z.B. Fileuploads). Mongrel verwendet ein Threaded Networking Model, erzeugt also einen neuen Thread für jeden Request. Bei kurzen Request ist das ein relativ grosser Overhead, aber bei längeren Requests hat Mongrel keinerlei Probleme auf neue Verbindungen zu reagieren.

EventMachine bietet für dieses Problem zwei Lösungen an: die defer-Methode und das EventMachine::Deferrable-Modul.

Die defer-Methode wird am besten an einem Beispiel (aus der Dokumentation) demonstriert:

1
2
3
4
5
6
7
8
9
operation = proc {
  # perform a long-running operation here, such as a database query.
  "result" # as usual, the last expression evaluated in the block will be the return value.
}
callback = proc {|result|
  # do something with result here, such as send it back to a network client.
}

EventMachine.defer( operation, callback )

Der callback-Parameter ist dabei optional. operation wird dabei in einem Threadpool gespeichert und dann asynchron zum Event-Loop ausgeführt. Die Methode eignet sich also um z.B. blockierende Operationen zu implementieren.

Wenn man schon mit Python's Twisted vertraut ist, dann wird EventMachine::Deferrable einem sehr bekannt vorkommen. Auch hier spricht ein Beispiel mehr als 1000 Worte:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
require 'rubygems'
require 'eventmachine'

class MyClass
  include EM::Deferrable

  def print_value x
    puts "MyClass instance received #{x}"
  end
end

EM.run {
  df = MyClass.new
  df.callback {|x|
    df.print_value(x)
    EM.stop
  }

  EM::Timer.new(2) {
    df.set_deferred_status :succeeded, 100
  }
}

Dieses Programm läuft für 2 Sekunden, gibt dann "MyClass instance received 100" aus, und stoppt dann den Event-Loop.

Auf den ersten Blick verhält sich das Deferrable-Modul also so ähnlich wie die defer-Methode. Ganz so einfach ist es aber nicht.

Ein Objekt welches Deferrable inkludiert ist wie jedes andere Ruby-Objekt, und kann auch genauso verwendet werden. Zusätzlich dazu können aber gewisse Callbacks und Errbacks (Callbacks für den Fall eines Misserfolgs) für dieses Objekt festgelegt werden, die irgendwann in der Zukunft ausgeführt werden wenn sich der Status des Objekts ändert.

Wozu der Aufwand? Durch dieses Pattern kann das Durchführen einer Operation (z.B. ein Request an einen anderen Server) von allen Operationen abgekapselt werden, die auf den Erfolg (oder Misserfolg) dieser Operation abhängig sind. Das kann man sich zunutze machen wenn man einen Http-Client implementiert (Beispiel aus der Dokumentation):

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

# ... stuff ...

EM.run {
  df = EM::Protocols::HttpClient.request( :host=>"www.example.com", :request=>"/index.html" )
  
  df.callback {|response|
    puts "Succeeded: #{response[:content]}"
    EM.stop
  }
  
  df.errback {|response|
    puts "ERROR: #{response[:status]}"
    EM.stop
  }
}

HttpClient.request liefert sofort ein Objekt zurück das Deferrable inkludiert. Im Hintergrund wird der HTTP-Request durchgeführt, während unser Event-Loop weiterläuft. Sobald der Request durchgeführt wurde, setzt das HttpClient-Objekt mittels set_deferred_status entweder auf :succeeded oder :failed, wodurch sofort die entsprechenden Callbacks aufgerufen werden.

Es ist dabei möglich mehrere Callbacks zu definieren (Beispiel aus der Dokumentation):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
require 'rubygems'
require 'eventmachine'

EM.run {
  df = EM::Protocols::HttpClient.request( :host=>"www.example.com", :request=>"/index.html" )

  df.callback {|response|
    df.set_deferred_status :succeeded, response[:content]
  }

  df.callback {|string|
    puts "Succeeded: #{string}"
    EM.stop
  }

  df.errback {|response|
    puts "ERROR: #{response[:status]}"
    EM.stop
  }
}

Durch erneutes aufrufen von set_deferred_status kann man die Argumente verändern, die das nächste Callback übergeben bekommt. Den Status selbst kann man nicht mehr ändern.

Fin

EventMachine bietet noch einiges mehr als hier vorgestellt wurde, z.B. eingebauten Support für HTTP (EventMachine::Protocols::HttpClient) und SMTP (EventMachine::Protocols::SmtpServer und EventMachine::Protocols::SmtpClient).

Nächstes mal wirds um Datamapper gehen, einem O/R-Mapper.


Kommentare

  1. Johannes schrieb am 02.04.2008 (08 Uhr)

    Interessanter Artikel. Auf den Datamapper bin ich schon gespannt. Ist mein meinen Augen eigentlich das Gleiche wie ActiveRecord. Mal sehen welche Unterschiede du aufzeigst.