em-rserve 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/TODO CHANGED
@@ -8,7 +8,6 @@
8
8
  * login command
9
9
  support crypt and store salt from connection setup
10
10
  * better attach method
11
- * write doc
12
11
  * more spec once the RServe spec is better understood/have enough examples
13
12
  * subclass Request for each commmand instead of current quick'n dirty mechanism
14
13
  * better test requests stacking/unstacking
data/em-rserve.gemspec CHANGED
@@ -7,7 +7,7 @@ Gem::Specification.new do |s|
7
7
  s.version = EM::Rserve::VERSION
8
8
  s.authors = ["crapooze"]
9
9
  s.email = ["crapooze@gmail.com"]
10
- s.homepage = ""
10
+ s.homepage = "https://github.com/crapooze/em-rserve"
11
11
  s.summary = %q{An EventMachine client for RServe}
12
12
  s.description = %q{Do evented stats with EventMachine and RServe}
13
13
 
@@ -0,0 +1,44 @@
1
+
2
+ module EM::Rserve
3
+ # A Backend is a way to specify connection parameters.
4
+ # The Pooler rely on a Backend to feed him the list of places to connect.
5
+ class Backend
6
+ include Enumerable
7
+
8
+ # A Server just holds an host and a port.
9
+ Server = Struct.new(:host, :port)
10
+
11
+ def initialize
12
+ yield self if block_given?
13
+ end
14
+
15
+ def next
16
+ raise NotImplementedError, "you should use subclasses of Backend"
17
+ end
18
+ end
19
+
20
+ # The default backend: an RServe running on localhost and default port (6311)
21
+ class DefaultBackend < Backend
22
+ def initialize
23
+ super
24
+ @server = Server.new('127.0.0.1', 6311).freeze
25
+ end
26
+
27
+ def next
28
+ @server
29
+ end
30
+ end
31
+
32
+ # Round-robin looping on servers
33
+ class RoundRobinBackend < Backend
34
+ def initialize(servers)
35
+ super()
36
+ raise ArgumentError, "need at least one server" if servers.empty?
37
+ @servers = servers
38
+ end
39
+
40
+ def next
41
+ @servers.unshift(@servers.pop).first
42
+ end
43
+ end
44
+ end
@@ -7,10 +7,18 @@ require "em-rserve/qap1/header"
7
7
  require "em-rserve/qap1/message"
8
8
 
9
9
  module EM::Rserve
10
+ # A Connection speaks to RServe using the methods in Protocol::Connector
11
+ # In addition, a Connection implements helper methods to call RServe commands.
10
12
  class Connection < EM::Connection
11
13
  include Protocol::Connector
12
14
  include QAP1
13
15
 
16
+ # Asks the server to close the connection.
17
+ # Note that this does not close the TCP connection yet because you will
18
+ # receive an acknowledgement first.
19
+ #
20
+ # Returns and pass to the optional block a new Request instance. See
21
+ # Protocol::Connector#request.
14
22
  def shutdown!(&blk)
15
23
  header = Header.new(Constants::CMD_shutdown,0,0,0)
16
24
  send_data header.to_bin
@@ -18,6 +26,10 @@ module EM::Rserve
18
26
  request(&blk)
19
27
  end
20
28
 
29
+ # Evaluates a R-code string in the R session
30
+ #
31
+ # Returns and pass to the optional block a new Request instance. See
32
+ # Protocol::Connector#request.
21
33
  def r_eval(string, void=false,&blk)
22
34
  data = Message.encode_string(string)
23
35
  if void
@@ -31,7 +43,11 @@ module EM::Rserve
31
43
  request(&blk)
32
44
  end
33
45
 
34
- def login(user,pwd, crypted=true, &blk)
46
+ # Logs-in if the RServe connection asks for a user/password pair
47
+ #
48
+ # Returns and pass to the optional block a new Request instance. See
49
+ # Protocol::Connector#request.
50
+ def login(user, pwd, crypted=true, &blk)
35
51
  raise NotImplementedError, "will come later"
36
52
  #XXX need to read the salt during connection setup
37
53
  cifer = crypted ? pwd : crypt(pwd, salt)
@@ -43,6 +59,11 @@ module EM::Rserve
43
59
  request(&blk)
44
60
  end
45
61
 
62
+ # Detaches current session, the response will hold a key to later re-attach
63
+ # the session.
64
+ #
65
+ # Returns and pass to the optional block a new Request instance. See
66
+ # Protocol::Connector#request.
46
67
  def detach(&blk)
47
68
  header = Header.new(Constants::CMD_detachSession, 0, 0, 0)
48
69
  send_data header.to_bin #port, key of 20 bytes
@@ -50,16 +71,28 @@ module EM::Rserve
50
71
  request(&blk)
51
72
  end
52
73
 
74
+ # Attaches to the session using the secret key.
75
+ #
76
+ # Returns and pass to the optional block a new Request instance. See
77
+ # Protocol::Connector#request.
53
78
  def attach(key, &blk)
54
- #XXX it seems that there is no need to send a Header + Message, and
55
- #raw_writing because the server does a read of 32 bytes on newly accepted
56
- #connections
57
- raise ArgumentError, "wrong key length, Rserve wants 32bytes" unless key.size == 32
79
+ #XXX it seems that there is no need to send a Header + Message. We can
80
+ #just write the key because the RServe code does a read of 32 bytes on newly
81
+ #accepted connections and tests the key.
82
+ raise ArgumentError, "wrong key length, Rserve wants 32bytes" unless key.size == 32
58
83
  send_data key
59
84
 
60
85
  request(&blk)
61
86
  end
62
87
 
88
+ # Assign an R object (represented by a Ruby instance of Sexp) to a symbol
89
+ # within the context of the connection. symbol must respond to :to_s, this
90
+ # value will be the symbol name in R.
91
+ # If parse_symbol_name is true, RServe will verify whether the R symbol is
92
+ # a legal symbol.
93
+ #
94
+ # Returns and pass to the optional block a new Request instance. See
95
+ # Protocol::Connector#request.
63
96
  def assign(symbol, sexp_node, parse_symbol_name=true, &blk)
64
97
  data = Message.new([symbol.to_s, sexp_node]).to_bin
65
98
  data << "\xFF" * data.length % 4
@@ -3,20 +3,48 @@ require "fiber"
3
3
  require "em-rserve/connection"
4
4
 
5
5
  module EM::Rserve
6
+ # A FiberedConnection is a type of EM::RServe::Connection but it handles every
7
+ # connection in a Ruby Fiber.
8
+ # This class also implements many high-level methods such that you don't have to understand the in and out of Ruby Fiber.
9
+ #
10
+ # It is usually enough to use the connection this way:
11
+ # EM.run do
12
+ # Fiber.new do
13
+ # conn = FiberedConnection.new
14
+ # conn[:foo] = [1, 2, 3, 4]
15
+ # conn[:bar] = [1, 2, 3, 4]
16
+ # puts conn.call('cor(foo, bar)')
17
+ # end.resume
18
+ # end
19
+ #
20
+ # For some context, please read http://www.igvita.com/2010/03/22/untangling-evented-code-with-ruby-fibers/
6
21
  class FiberedConnection < EM::Rserve::Connection
22
+
23
+ # The Fiber holding the context for this connection
7
24
  attr_accessor :fiber
8
25
 
26
+ # Starts a new connection, the first parameter is a Fiber to hold the context
27
+ # of the connection, defaults to current fiber.
28
+ # This fiber cannot be the root Fiber.
29
+ # Remaining parameters are the same than in EM::Rserve::Connection.start
9
30
  def self.start(fiber=Fiber.current, *args)
10
31
  conn = super(*args)
11
32
  conn.fiber = fiber
12
33
  Fiber.yield
13
34
  end
14
35
 
36
+ # Called when ready, resume the Fiber execution
15
37
  def ready
16
38
  super
17
39
  fiber.resume self if fiber
18
40
  end
19
41
 
42
+ # Evaluates a piece of R script and returns:
43
+ # - script is a string (or an object that we'll transform to a string with :to_s)
44
+ # - nil if the script doesn't return anything (it often does but not on)
45
+ # - a Ruby object coming from the translation of a Sexp which represents an R object
46
+ # - WARNING: current version also return nil on error
47
+ # Blocks the Fiber but not the event loop.
20
48
  def call(script, *args)
21
49
  r_eval(script.to_s, *args) do |req|
22
50
  req.errback do |err|
@@ -40,6 +68,10 @@ class FiberedConnection < EM::Rserve::Connection
40
68
  Fiber.yield
41
69
  end
42
70
 
71
+ # Sets a symbol to a val in the R context.
72
+ # sym will be passed verbatim to EM::Rserve::Connection#assign
73
+ # val is a Ruby object that will get translated to a Sexp which represents an R object
74
+ # Blocks the Fiber but not the event loop.
43
75
  def set(sym, val)
44
76
  root = EM::Rserve::R::RubytoR::Translator.ruby_to_r val
45
77
 
@@ -55,7 +87,15 @@ class FiberedConnection < EM::Rserve::Connection
55
87
  Fiber.yield
56
88
  end
57
89
 
58
- # *WARNING* this method is unsafe always check your input parameter
90
+ # Same thing as call, RServe doesn't provide a way to read the value of a
91
+ # symbol although it provides a way of writing a symbol. Hence, we just
92
+ # evaluate a script with the name of the symbol.
93
+ # A sad side effect is the following warning:
94
+ #
95
+ # *WARNING* this method is unsafe because it evaluates arbitrary R Code.
96
+ # Always check that your input parameter looks like an R symbol.
97
+ #
98
+ # Blocks the Fiber but not the event loop.
59
99
  def get(sym)
60
100
  call(sym)
61
101
  end
@@ -1,13 +1,21 @@
1
1
 
2
2
  require "em-rserve/fibered_connection"
3
+ require "em-rserve/backend"
3
4
 
4
5
  module EM::Rserve
6
+ # A Pooler is a pool of already ready FiberedConnections as new RServe
7
+ # connections are requested, new fibers/connections are added.
8
+ # There is currently no limitation on the maximum number of establish
9
+ # connections, but just a limit on the minimum pre-established connections
5
10
  class Pooler
6
11
  class << self
7
- def r(klass=FiberedConnection)
12
+ # Immediately creates and yields a new connection of class klass.
13
+ # Shorthand method to create one connection wrapped in a Fiber.
14
+ def r(klass=FiberedConnection, backend=DefaultBackend.new)
8
15
  Fiber.new do
9
16
  begin
10
- conn = klass.start
17
+ server = backend.next
18
+ conn = klass.start(Fiber.current, server.host, server.port)
11
19
  yield conn
12
20
  ensure
13
21
  conn.close_connection
@@ -16,29 +24,50 @@ module EM::Rserve
16
24
  end
17
25
  end
18
26
 
19
- attr_reader :connections, :size, :connection_class
20
- def initialize(size=10, klass=FiberedConnection)
27
+ # An array of pending connections
28
+ attr_reader :connections
29
+ # The minimum number of connections to maintain
30
+ attr_reader :size
31
+ # The klass to use when instanciating new connections
32
+ attr_reader :connection_class
33
+ # The backend, which says on which host/port to connect to
34
+ attr_reader :backend
35
+
36
+ # Initializes and pre-establish size connections of class klass
37
+ def initialize(size=10, klass=FiberedConnection, backend=DefaultBackend.new)
21
38
  @connections = []
22
39
  @size = size
23
40
  @connection_class = klass
41
+ @backend = backend
24
42
  fill size
25
43
  end
26
44
 
45
+ # True if there are no connections left in the pool
27
46
  def empty?
28
47
  connections.empty?
29
48
  end
30
49
 
50
+ # True if there are at least size connections in the pool
31
51
  def full?
32
52
  connections.size >= size
33
53
  end
34
54
 
55
+ # Creates and yields a new connection in a Fiber
35
56
  def connection
36
57
  #XXX duplicated code from Pooler.r to avoid proc-ing the blk
37
58
  Fiber.new do
38
- yield connection_class.start
59
+ server = backend.next
60
+ yield connection_class.start(Fiber.current, server.host, server.port)
39
61
  end.resume
40
62
  end
41
63
 
64
+ # Pick and yield a new connection from the connections pool.
65
+ # If the pool, yield a new connection.
66
+ #
67
+ # This method also ensure that the TCP connection is closed once the work
68
+ # is finished.
69
+ #
70
+ # It also re-establish new connections, trying to maintain the pool filled.
42
71
  def r
43
72
  conn = connections.shift
44
73
  if conn
@@ -63,6 +92,8 @@ module EM::Rserve
63
92
  end
64
93
  end
65
94
 
95
+ # Preconnect a connection, once the connection is ready, it is added to the
96
+ # connections pool.
66
97
  def preconnect!
67
98
  connection do |conn|
68
99
  conn.fiber = nil
@@ -70,6 +101,7 @@ module EM::Rserve
70
101
  end
71
102
  end
72
103
 
104
+ # Shorthand to preconnect n connections in parallel.
73
105
  def fill(n=size)
74
106
  n.times{preconnect!}
75
107
  end
@@ -10,6 +10,7 @@ module EM::Rserve
10
10
  end
11
11
 
12
12
  module ClassMethods
13
+ # Starts a new TCP connection to the server/port parameters
13
14
  def start(server='127.0.0.1', port=6311)
14
15
  EM.connect(server, port, self)
15
16
  end
@@ -17,8 +18,16 @@ module EM::Rserve
17
18
 
18
19
  extend ClassMethods
19
20
 
21
+ # FIFO for pending requests.
22
+ #
23
+ # RServe protocol implements a synchronous request/response scheme over a
24
+ # single TCP stream. Thus we can hold contexts in a FIFO array of requests
25
+ # to the same server
20
26
  attr_accessor :request_queue
21
27
 
28
+ # Implements EM::Connection post_init hook:
29
+ # - sets the parser to parse IDs
30
+ # - create a request_queue
22
31
  def post_init
23
32
  super
24
33
  #type of parser carries the state, no need to carry it internally and do
@@ -28,6 +37,17 @@ module EM::Rserve
28
37
  @request_queue = []
29
38
  end
30
39
 
40
+ # Creates and appends a new request to the request_queue
41
+ # If a block is given, it will be called and the new request will be
42
+ # passed as argument.
43
+ # Also returns the newly created request.
44
+ #
45
+ # r = connector.request
46
+ # r.success{ p 'good' }
47
+ #
48
+ # connector.request do |r|
49
+ # r.success{p 'good'}
50
+ # end
31
51
  def request
32
52
  r = Request.new
33
53
  yield r if block_given?
@@ -35,6 +55,16 @@ module EM::Rserve
35
55
  r
36
56
  end
37
57
 
58
+ # Replaces current Parser by a new Parser.
59
+ # The klass parameter gives the class to instanciate.
60
+ # Instanciation of the new parser will receive self as only parameter.
61
+ # If the hold parser holds data in its buffer, we may lose this data, hence
62
+ # we call Parser#replace to correctly hand-over the parsing.
63
+ # Note that this complication comes from the RServe protocol which has an
64
+ # initialization phase.
65
+ # Rather than holding the state and testing whether the connection is
66
+ # initialized or not each time we receive a byte, once the initialization
67
+ # phase is done, we just change the parser.
38
68
  def replace_parser!(klass)
39
69
  new_parser = klass.new(self)
40
70
  if @parser
@@ -43,10 +73,12 @@ module EM::Rserve
43
73
  @parser = new_parser
44
74
  end
45
75
 
76
+ # Implements EM::Connection receive_data hook, just forwarding to the parser.
46
77
  def receive_data(dat)
47
78
  @parser << dat
48
79
  end
49
80
 
81
+ # Implements IDParser's receive_id callback.
50
82
  def receive_id(id)
51
83
  # on last line, the messaging can start
52
84
  if id.last_one?
@@ -60,9 +92,16 @@ module EM::Rserve
60
92
 
61
93
  # HOOKS TO OVERRIDE, PLEASE CALL SUPER
62
94
 
95
+ # This method gets called whenever the connection is started and initialized.
96
+ # By default, it does nothing, you don't need to call super when
97
+ # overriding this method.
63
98
  def ready
64
99
  end
65
100
 
101
+ # Implements MessageParser's callback.
102
+ # This method gets called whenever the server start answering with a
103
+ # message header.
104
+ # If you override this method, call super first.
66
105
  def receive_message_header(head)
67
106
  if head.error?
68
107
  receive_error_message_header(head)
@@ -73,14 +112,28 @@ module EM::Rserve
73
112
  end
74
113
  end
75
114
 
115
+ # This method gets called by receive_message_header if the header is an
116
+ # error. It dequeues the request_queue FIFO and calls its error method
117
+ # with the header as only parameter.
118
+ # If you override this method, call super first.
76
119
  def receive_error_message_header(head)
77
120
  request_queue.shift.error(head)
78
121
  end
79
122
 
123
+ # This method gets called by receive_message_header if the header is an error.
124
+ # If the header tells us there is no more data to answer this request
125
+ # (i.e., there will be no body data), dequeues the request_queue FIFO and
126
+ # calls the success method with "nil" as only parameter.
127
+ # If you override this method, call super first.
80
128
  def receive_success_message_header(head)
81
129
  request_queue.shift.success(nil) unless head.body?
82
130
  end
83
131
 
132
+ # Implements MessageParser's callback.
133
+ # This methods gets called whenever a message was completely received.
134
+ # Dequeues the request_queue FIFO and calls the success method with the
135
+ # message as only parameter.
136
+ # If you override this method, call super first.
84
137
  def receive_message(msg)
85
138
  request_queue.shift.success(msg)
86
139
  end
@@ -6,24 +6,38 @@ require 'em-rserve/r/sexp'
6
6
 
7
7
  module EM::Rserve
8
8
  module Protocol
9
+ # Top class for parsers.
9
10
  class Parser
10
- attr_reader :handler, :buffer
11
+ # A handler is an object which will receive method calls from parsers on
12
+ # specific events.
13
+ attr_reader :handler
11
14
 
15
+ # A buffer holding data.
16
+ attr_reader :buffer
17
+
18
+ # Initializes a new Parser and stores the handler.
12
19
  def initialize(handler)
13
20
  @handler = handler
14
21
  @buffer = ''
15
22
  end
16
23
 
24
+ # Replaces current buffer with other's parser's buffer.
25
+ # This is useful when handing-over from one type of parsing to another.
17
26
  def replace(other)
18
27
  @buffer.replace(other.buffer)
19
28
  self
20
29
  end
21
30
 
31
+ # Input some data to the parser. As there is new data, will start a
32
+ # parsing loop by calling parse_loop!
22
33
  def << data
23
34
  buffer << data if data
24
35
  parse_loop!
25
36
  end
26
37
 
38
+ # Infinitely calls the parse! method (to override in subclasses of parsers).
39
+ # To get out of the loop (e.g., when we realize that there is not enough
40
+ # data in the buffer to complete a message), one must throw :stop .
27
41
  def parse_loop!
28
42
  catch :stop do
29
43
  loop do
@@ -42,6 +56,8 @@ module EM::Rserve
42
56
  # This parser is useful only at the beginning.
43
57
  # Instead of carrying its dynamic all the time (e.g., keeping a state).
44
58
  # We pop-it out as another parser.
59
+ #
60
+ # This parser's handler must respond_to :receive_id
45
61
  class IDParser < Parser
46
62
  def parse!
47
63
  if buffer.size >= 4
@@ -55,6 +71,8 @@ module EM::Rserve
55
71
  end
56
72
 
57
73
  # This message parser will parse qap1 headers and associated qap1 data.
74
+ #
75
+ # This parser's handler must respond_to :receive_message and :receive_message_header
58
76
  class MessageParser < Parser
59
77
  def initialize(handler)
60
78
  super(handler)
@@ -1,19 +1,24 @@
1
1
 
2
2
  module EM::Rserve
3
3
  module Protocol
4
+ # Simple Request structure holding a references to a callback and an errback
4
5
  Request = Struct.new(:callback_blk, :errback_blk) do
6
+ # sets the async callback with a block argument
5
7
  def callback(&blk)
6
8
  self.callback_blk = blk
7
9
  end
8
10
 
11
+ # sets the async errback with a block argument
9
12
  def errback(&blk)
10
13
  self.errback_blk = blk
11
14
  end
12
15
 
16
+ # calls the errback
13
17
  def error(val)
14
18
  errback_blk.call(val) if errback_blk
15
19
  end
16
20
 
21
+ # calls the callback
17
22
  def success(val)
18
23
  callback_blk.call(val) if callback_blk
19
24
  end
@@ -1,5 +1,5 @@
1
1
  module EM
2
2
  module Rserve
3
- VERSION = "0.1.1"
3
+ VERSION = "0.1.2"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: em-rserve
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-10-21 00:00:00.000000000Z
12
+ date: 2011-10-22 00:00:00.000000000Z
13
13
  dependencies: []
14
14
  description: Do evented stats with EventMachine and RServe
15
15
  email:
@@ -25,6 +25,7 @@ files:
25
25
  - TODO
26
26
  - em-rserve.gemspec
27
27
  - lib/em-rserve.rb
28
+ - lib/em-rserve/backend.rb
28
29
  - lib/em-rserve/connection.rb
29
30
  - lib/em-rserve/fibered_connection.rb
30
31
  - lib/em-rserve/pooler.rb
@@ -52,7 +53,7 @@ files:
52
53
  - specs/parser_spec.rb
53
54
  - specs/ruby_to_r_translator_spec.rb
54
55
  - specs/spec_helper.rb
55
- homepage: ''
56
+ homepage: https://github.com/crapooze/em-rserve
56
57
  licenses: []
57
58
  post_install_message:
58
59
  rdoc_options: []