Slide 1

Slide 1 text

EventMachine Server Design With XMPP @davidgraham 17 April 2012

Slide 2

Slide 2 text

gem install vines

Slide 3

Slide 3 text

gem install couchproxy

Slide 4

Slide 4 text

XMPP is not HTTP

Slide 5

Slide 5 text

Long Lived Connections

Slide 6

Slide 6 text

Connections Mostly Idle

Slide 7

Slide 7 text

Stateful

Slide 8

Slide 8 text

Requires Asynchronous IO

Slide 9

Slide 9 text

EventMachine

Slide 10

Slide 10 text

You do not block the event loop.

Slide 11

Slide 11 text

No content

Slide 12

Slide 12 text

# test.ru app = proc do |env| sleep 20 [ 200, {'Content-Type' => 'text/html'}, ['Hello World'] ] end run app $ thin --rackup test.ru start $ curl http://localhost:3000 $ curl http://localhost:3000

Slide 13

Slide 13 text

# test.ru app = proc do |env| sleep 20 [ 200, {'Content-Type' => 'text/html'}, ['Hello World'] ] end run app $ thin --rackup test.ru --threaded start $ curl http://localhost:3000 $ curl http://localhost:3000

Slide 14

Slide 14 text

require 'eventmachine' module EchoServer def post_init puts "-- someone connected to the echo server!" end def receive_data data send_data ">>> you sent: #{data}" end end EventMachine::run do EventMachine::start_server "127.0.0.1", 8081, EchoServer puts 'running echo server on 8081' end Easy!

Slide 15

Slide 15 text

IO Read Parser Queue State Machine IO Write

Slide 16

Slide 16 text

require 'eventmachine' module EchoServer def post_init puts "-- someone connected to the echo server!" end def receive_data data send_data ">>> you sent: #{data}" end end EventMachine::run do EventMachine::start_server "127.0.0.1", 8081, EchoServer puts 'running echo server on 8081' end Easy! Maybe Not

Slide 17

Slide 17 text

IO Read Parser Queue State Machine IO Write

Slide 18

Slide 18 text

XMPP

Slide 19

Slide 19 text

Stream Negotiation

Slide 20

Slide 20 text

Slide 21

Slide 21 text

Slide 22

Slide 22 text

Slide 23

Slide 23 text

Slide 24

Slide 24 text

Slide 25

Slide 25 text

Stanzas

Slide 26

Slide 26 text

Wherefore art thou, Romeo?

Slide 27

Slide 27 text

Wherefore art thou, Romeo? away I’m not here!

Slide 28

Slide 28 text

Wherefore art thou, Romeo? away I’m not here!

Slide 29

Slide 29 text

Parsers

Slide 30

Slide 30 text

Line Separated def post_init @parser = BufferedTokenizer.new end def receive_data(data) @parser.extract(data).each do |line| receive_line(line) end end

Slide 31

Slide 31 text

XML def post_init @parser = Nokogiri::XML::SAX::PushParser.new(self) end def receive_data(data) @parser << data end

Slide 32

Slide 32 text

HTTP def post_init @parser = HTTP::Parser.new end def receive_data(data) @parser << data end

Slide 33

Slide 33 text

JSON::Stream def post_init @parser = JSON::Stream::Parser.new end def receive_data(data) @parser << data end

Slide 34

Slide 34 text

YAJL-JSON def post_init @parser = YAJL::Parser.new end def receive_data(data) @parser << data end

Slide 35

Slide 35 text

@parser << data

Slide 36

Slide 36 text

IO Read Parser Queue State Machine IO Write

Slide 37

Slide 37 text

Queues

Slide 38

Slide 38 text

10.1. In-Order Processing An XMPP server MUST ensure in-order processing of the stanzas and other XML elements it receives over a given input stream from a connected client or remote server. XMPP RFC 6120

Slide 39

Slide 39 text

Blocking Queue def post_init @nodes = Queue.new end def process_node_queue node = @nodes.pop # blocks process_node(node) process_node_queue end

Slide 40

Slide 40 text

Asynchronous Queue def post_init @nodes = EM::Queue.new end def process_node_queue @nodes.pop do |node| process_node(node) process_node_queue end end

Slide 41

Slide 41 text

IO Read Parser Queue State Machine IO Write

Slide 42

Slide 42 text

State Machines

Slide 43

Slide 43 text

Security

Slide 44

Slide 44 text

Sanity

Slide 45

Slide 45 text

Easy to Test

Slide 46

Slide 46 text

it 'rejects invalid elements' do state = Stream::Client::Auth.new assert_raises(StreamErrors::NotAuthorized) do state.node('') end end Authentication Fails

Slide 47

Slide 47 text

it 'accepts valid plain auth passwords' do state = Stream::Client::Auth.new next_state = Stream::Client::BindRestart.new stream = MiniTest::Mock.new stream.expect(:write, nil, ['']) stream.expect(:advance, nil, [next_state]) node = "#{@password}" state.node(node) assert stream.verify end Authentication Passes

Slide 48

Slide 48 text

Case Statement

Slide 49

Slide 49 text

case @state when :start_document then start_document when :start_object then start_object when :start_string then start_string when :unicode_escape then unicode_escape when :start_surrogate_pair then start_surrogate when :start_negative_number then start_negative when :start_zero then start_zero when :start_float then start_float when :start_exponent then start_exponent when :start_int then start_int when :start_true then start_true when :start_false then start_false when :start_null then start_null when :end_key then end_key when :start_array then start_array when :end_value then end_value when :end_document then end_document end JSON::Stream

Slide 50

Slide 50 text

case @state when :start_document case ch when "{" @state = :start_object @stack.push(:object) notify_start_document notify_start_object when "[" @state = :start_array @stack.push(:array) notify_start_document notify_start_array when WS # ignore else error("Expected object or array start") end when :start_object case ch JSON::Stream

Slide 51

Slide 51 text

case @state when :start_document case ch when LEFT_BRACE @state = :start_object @stack.push(:object) notify_start_document notify_start_object when LEFT_BRACKET @state = :start_array @stack.push(:array) notify_start_document notify_start_array when WS # ignore else error("Expected object or array start") end when :start_object case ch when RIGHT_BRACE end_container(:object) when QUOTE @state = :start_string @stack.push(:key) when WS # ignore else error("Expected object key start") end when :start_string case ch when QUOTE if @stack.pop == :string @state = :end_value notify_value(@buf) else # :key @state = :end_key notify_key(@buf) end @buf = "" when BACKSLASH @state = :start_escape when CONTROL error('Control characters must be escaped') else @buf << ch end when :start_escape case ch when QUOTE, BACKSLASH, SLASH @buf << ch @state = :start_string when B @buf << "\b" @state = :start_string when F @buf << "\f" @state = :start_string when N @buf << "\n" @state = :start_string when R @buf << "\r" @state = :start_string when T @buf << "\t" @state = :start_string when U @state = :unicode_escape else error("Expected escaped character") end when :unicode_escape case ch when HEX @unicode << ch if @unicode.size == 4 codepoint = @unicode.slice!(0, 4).hex if codepoint >= 0xD800 && codepoint <= 0xDBFF error('Expected low surrogate pair half') if @stack[-1].is_a?(Fixnum) @state = :start_surrogate_pair @stack.push(codepoint) elsif codepoint >= 0xDC00 && codepoint <= 0xDFFF high = @stack.pop error('Expected high surrogate pair half') unless high.is_a?(Fixnum) pair = ((high - 0xD800) * 0x400) + (codepoint - 0xDC00) + 0x10000 @buf << pair @state = :start_string else @buf << codepoint @state = :start_string end end else error('Expected unicode escape hex digit') end when :start_surrogate_pair case ch when BACKSLASH @state = :start_surrogate_pair_u else error('Expected low surrogate pair half') end when :start_surrogate_pair_u case ch when U @state = :unicode_escape else error('Expected low surrogate pair half') end when :start_negative_number case ch when ZERO @state = :start_zero @buf << ch when DIGIT_1_9 @state = :start_int @buf << ch else error('Expected 0-9 digit') end when :start_zero case ch when POINT @state = :start_float @buf << ch when EXPONENT @state = :start_exponent @buf << ch else @state = :end_value notify_value(@buf.to_i) @buf = "" @pos -= 1 redo end when :start_float case ch when DIGIT @state = :in_float @buf << ch else error('Expected 0-9 digit') end when :in_float case ch when DIGIT @buf << ch when EXPONENT @state = :start_exponent @buf << ch else @state = :end_value notify_value(@buf.to_f) @buf = "" @pos -= 1 redo end when :start_exponent case ch when MINUS, PLUS, DIGIT @state = :in_exponent @buf << ch else error('Expected +, -, or 0-9 digit') end when :in_exponent case ch when DIGIT @buf << ch else error('Expected 0-9 digit') unless @buf =~ DIGIT_END @state = :end_value num = @buf.include?('.') ? @buf.to_f : @buf.to_i notify_value(num) @buf = "" @pos -= 1 redo end when :start_int case ch when DIGIT @buf << ch when POINT @state = :start_float @buf << ch when EXPONENT @state = :start_exponent @buf << ch else @state = :end_value notify_value(@buf.to_i) @buf = "" @pos -= 1 redo end when :start_true keyword(TRUE_KEYWORD, true, TRUE_RE, ch) when :start_false keyword(FALSE_KEYWORD, false, FALSE_RE, ch) when :start_null keyword(NULL_KEYWORD, nil, NULL_RE, ch)

Slide 52

Slide 52 text

State Design Pattern

Slide 53

Slide 53 text

Start TLS AuthRestart Auth BindRestart Bind Ready Closed

Slide 54

Slide 54 text

class Start < State def initialize(stream, success=TLS) super end def node(node) raise StreamErrors::NotAuthorized unless stream?(node) stream.start(node) doc = Document.new features = doc.create_element('stream:features') do |el| el << doc.create_element('starttls') do |tls| tls.default_namespace = NAMESPACES[:tls] tls << doc.create_element('required') end end stream.write(features) advance end end

Slide 55

Slide 55 text

class Ready < State def node(node) stanza = to_stanza(node) raise StreamErrors::UnsupportedStanzaType unless stanza stanza.validate_to stanza.validate_from stanza.process end end

Slide 56

Slide 56 text

IO Read Parser Queue State Machine IO Write

Slide 57

Slide 57 text

EventMachine, a Lonely Island

Slide 58

Slide 58 text

Also, a Lonely Island

Slide 59

Slide 59 text

Let’s talk to a database!

Slide 60

Slide 60 text

gem install activerecord

Slide 61

Slide 61 text

gem install activerecord

Slide 62

Slide 62 text

gem install mongo_mapper

Slide 63

Slide 63 text

gem install mongo_mapper

Slide 64

Slide 64 text

gem install redis

Slide 65

Slide 65 text

gem install redis

Slide 66

Slide 66 text

gem install couchrest

Slide 67

Slide 67 text

gem install couchrest

Slide 68

Slide 68 text

Blocking (a.k.a. Normal) IO

Slide 69

Slide 69 text

Blocking (a.k.a. Normal) IO

Slide 70

Slide 70 text

gem install em-redis em-hiredis em-mongo em-http-request

Slide 71

Slide 71 text

gem install em-redis em-hiredis em-mongo em-http-request

Slide 72

Slide 72 text

No problem, let’s use threads!

Slide 73

Slide 73 text

# runs on thread pool operation = proc do sleep 20 42 end # runs on the reactor thread callback = proc do |result| send_data(result) end EM.defer(operation, callback) Thread Pool

Slide 74

Slide 74 text

user = find_user('[email protected]') user.name = 'Alice' user = save_user(user) puts user.name Callback Spaghetti find_user('[email protected]') do |user| user.name = 'Alice' save_user(user) do |user| puts user.name end end

Slide 75

Slide 75 text

Fibers

Slide 76

Slide 76 text

fiber = Fiber.new do Fiber.yield 1 Fiber.yield 2 Fiber.yield 3 end fiber.resume #=> 1 fiber.resume #=> 2 fiber.resume #=> 3 fiber.resume #=> FiberError: dead fiber called Episode IV: A New Stack

Slide 77

Slide 77 text

def find_user(username) User.find_by_username(username) # blocks end

Slide 78

Slide 78 text

def find_user(username) fiber = Fiber.current op = proc { User.find_by_username(username) } cb = proc {|user| fiber.resume(user) } EM.defer(op, cb) Fiber.yield end Threads + Fibers

Slide 79

Slide 79 text

def find_user(username) fiber = Fiber.current op = proc { User.find_by_username(username) } cb = proc {|user| fiber.resume(user) } EM.defer(op, cb) Fiber.yield end Boilerplate

Slide 80

Slide 80 text

def find_user(username) User.find_by_username(username) end defer :find_user Decorator

Slide 81

Slide 81 text

def self.defer(method) old = "_deferred_#{method}" alias_method old, method define_method method do |*args| fiber = Fiber.current op = proc { method(old).call(*args) } cb = proc {|result| fiber.resume(result) } EM.defer(op, cb) Fiber.yield end end Metaprogramming

Slide 82

Slide 82 text

def find_user(username) # find user end defer :find_user def save_user(user) # save user end defer :save_user def find_vcard(jid) # find card end defer :find_vcard def save_vcard(jid, card) # save card end defer :save_vcard DRY

Slide 83

Slide 83 text

def post_init @nodes = EM::Queue.new end def process_node_queue @nodes.pop do |node| Fiber.new do process_node(node) process_node_queue end.resume end end Fiber/Stanza

Slide 84

Slide 84 text

IO Read Parser Queue State Machine IO Write

Slide 85

Slide 85 text

The End @davidgraham [email protected]