em-rserve 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []