rjr 0.7.0 → 0.8.0

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/lib/rjr/node.rb CHANGED
@@ -11,19 +11,51 @@ require 'rjr/thread_pool'
11
11
 
12
12
  module RJR
13
13
 
14
- # Defines a node which can be used to dispatch rpc requests and/or register
15
- # handlers for incomping requests.
14
+ # Base RJR Node interface. Nodes are the central transport mechanism of rjr,
15
+ # this class provides the core methods common among all transport types and
16
+ # mechanisms to start and run the eventmachine reactor which drives all requests.
17
+ #
18
+ # A subclass of RJR::Node should be defined for each transport that is supported,
19
+ # implementing the 'listen' operation to listen for new requests and 'invoke_request'
20
+ # to issue them.
16
21
  class Node
17
- # node always has a node id
22
+ class << self
23
+ # @!group Config options
24
+
25
+ # Default number of threads to instantiate in local worker pool
26
+ attr_accessor :default_threads
27
+
28
+ # Default timeout after which worker threads are killed
29
+ attr_accessor :default_timeout
30
+
31
+ # @!endgroup
32
+ end
33
+
34
+ # Unique string identifier of the node
18
35
  attr_reader :node_id
19
36
 
20
- # attitional parameters to set on messages
37
+ # Attitional header fields to set on all
38
+ # requests and responses received and sent by node
21
39
  attr_accessor :message_headers
22
40
 
41
+ # Nodes use internal thread pools to handle requests and free
42
+ # up the eventmachine reactor to continue processing requests
43
+ # @see ThreadPool
23
44
  attr_reader :thread_pool
24
45
 
46
+ # RJR::Node initializer
47
+ # @param [Hash] args options to set on request
48
+ # @option args [String] :node_id unique id of the node *required*!!!
49
+ # @option args [Hash<String,String>] :headers optional headers to set on all json-rpc messages
50
+ # @option args [Integer] :threads number of handler to threads to instantiate in local worker pool
51
+ # @option args [Integer] :timeout timeout after which worker thread being run is killed
25
52
  def initialize(args = {})
26
- @node_id = args[:node_id]
53
+ RJR::Node.default_threads ||= 15
54
+ RJR::Node.default_timeout ||= 5
55
+
56
+ @node_id = args[:node_id]
57
+ @num_threads = args[:threads] || RJR::Node.default_threads
58
+ @timeout = args[:timeout] || RJR::Node.default_timeout
27
59
 
28
60
  @message_headers = {}
29
61
  @message_headers.merge!(args[:headers]) if args.has_key?(:headers)
@@ -31,11 +63,23 @@ class Node
31
63
  ObjectSpace.define_finalizer(self, self.class.finalize(self))
32
64
  end
33
65
 
66
+ # Ruby ObjectSpace finalizer to ensure that node terminates all
67
+ # operations when object is destroyed
34
68
  def self.finalize(node)
35
69
  proc { node.halt ; node.join }
36
70
  end
37
71
 
38
- # run job in event machine
72
+ # Run a job in event machine.
73
+ #
74
+ # This will start the eventmachine reactor and thread pool if not already
75
+ # running, schedule the specified block to be run and immediately return.
76
+ #
77
+ # For use by subclasses to start listening and sending operations within
78
+ # the context of event machine.
79
+ #
80
+ # Keeps track of an internal counter of how many times this was invoked so
81
+ # a specific node can be shutdown / started up without affecting the
82
+ # eventmachine reactor (@see #stop)
39
83
  def em_run(&bl)
40
84
  @@em_jobs ||= 0
41
85
  @@em_jobs += 1
@@ -44,8 +88,7 @@ class Node
44
88
 
45
89
  unless !@thread_pool.nil? && @thread_pool.running?
46
90
  # threads pool to handle incoming requests
47
- # FIXME make the # of threads and timeout configurable)
48
- @thread_pool = ThreadPool.new(10, :timeout => 5)
91
+ @thread_pool = ThreadPool.new(@num_threads, :timeout => @timeout)
49
92
  end
50
93
 
51
94
  if @@em_thread.nil?
@@ -63,10 +106,12 @@ class Node
63
106
  EventMachine.schedule bl
64
107
  end
65
108
 
109
+ # Returns boolean indicating if this node is still running or not
66
110
  def em_running?
67
111
  @@em_jobs > 0 && EventMachine.reactor_running?
68
112
  end
69
113
 
114
+ # Block until the eventmachine reactor and thread pool have both completed running
70
115
  def join
71
116
  @@em_thread.join if @@em_thread
72
117
  @@em_thread = nil
@@ -74,6 +119,8 @@ class Node
74
119
  @thread_pool = nil
75
120
  end
76
121
 
122
+ # Decrement the event machine job counter and if equal to zero,
123
+ # immediately terminate the node
77
124
  def stop
78
125
  @@em_jobs -= 1
79
126
  if @@em_jobs == 0
@@ -82,6 +129,8 @@ class Node
82
129
  end
83
130
  end
84
131
 
132
+ # Immediately terminate the node, halting the eventmachine reactor and
133
+ # terminating the thread pool
85
134
  def halt
86
135
  @@em_jobs = 0
87
136
  EventMachine.stop
data/lib/rjr/tcp_node.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # RJR TCP Endpoint
2
2
  #
3
+ # Implements the RJR::Node interface to satisty JSON-RPC requests over the TCP protocol
4
+ #
3
5
  # Copyright (C) 2012 Mohammed Morsi <mo@morsi.org>
4
6
  # Licensed under the Apache License, Version 2.0
5
7
 
@@ -11,14 +13,24 @@ require 'rjr/message'
11
13
 
12
14
  module RJR
13
15
 
14
- # TCP client node callback interface,
15
- # send data back to client via established tcp socket.
16
+ # TCP node callback interface, used to invoke json-rpc methods
17
+ # against a remote node via a tcp socket connection previously opened
18
+ #
19
+ # After a node sends a json-rpc request to another, the either node may send
20
+ # additional requests to each other via the socket already established until
21
+ # it is closed on either end
16
22
  class TCPNodeCallback
23
+
24
+ # TCPNodeCallback initializer
25
+ # @param [Hash] args the options to create the tcp node callback with
26
+ # @option args [TCPNodeEndpoint] :endpoint tcp node endpoint used to send/receive messages
27
+ # @option args [Hash] :headers hash of rjr message headers present in client request when callback is established
17
28
  def initialize(args = {})
18
29
  @endpoint = args[:endpoint]
19
30
  @message_headers = args[:headers]
20
31
  end
21
32
 
33
+ # Implementation of {RJR::NodeCallback#invoke}
22
34
  def invoke(callback_method, *data)
23
35
  msg = RequestMessage.new :method => callback_method, :args => data, :headers => @message_headers
24
36
  # TODO surround w/ begin/rescue block incase of socket errors
@@ -26,9 +38,13 @@ class TCPNodeCallback
26
38
  end
27
39
  end
28
40
 
29
- # helper class intialized by event machine corresponding to
30
- # a client or server socket connection
41
+ # @private
42
+ # Helper class intialized by eventmachine encapsulating a socket connection
31
43
  class TCPNodeEndpoint < EventMachine::Connection
44
+
45
+ # TCPNodeEndpoint intializer
46
+ #
47
+ # specify the TCPNode establishing the connection and an optional first message to send
32
48
  def initialize(args = {})
33
49
  @rjr_node = args[:rjr_node]
34
50
 
@@ -36,6 +52,7 @@ class TCPNodeEndpoint < EventMachine::Connection
36
52
  @send_message = args[:init_message]
37
53
  end
38
54
 
55
+ # {EventMachine::Connection#post_init} callback, sends first message if specified
39
56
  def post_init
40
57
  unless @send_message.nil?
41
58
  send_data @send_message.to_s
@@ -43,6 +60,7 @@ class TCPNodeEndpoint < EventMachine::Connection
43
60
  end
44
61
  end
45
62
 
63
+ # {EventMachine::Connection#receive_data} callback, handle request / response messages
46
64
  def receive_data(data)
47
65
  if RequestMessage.is_request_message?(data)
48
66
  @rjr_node.thread_pool << ThreadPoolJob.new { handle_request(data) }
@@ -54,6 +72,9 @@ class TCPNodeEndpoint < EventMachine::Connection
54
72
  end
55
73
 
56
74
 
75
+ private
76
+
77
+ # Internal helper, handle request message received
57
78
  def handle_request(data)
58
79
  client_port, client_ip = Socket.unpack_sockaddr_in(get_peername)
59
80
  msg = RequestMessage.new(:message => data, :headers => @rjr_node.message_headers)
@@ -73,6 +94,7 @@ class TCPNodeEndpoint < EventMachine::Connection
73
94
  send_data(response.to_s)
74
95
  end
75
96
 
97
+ # Internal helper, handle response message received
76
98
  def handle_response(data)
77
99
  msg = ResponseMessage.new(:message => data, :headers => @rjr_node.message_headers)
78
100
  res = err = nil
@@ -90,7 +112,26 @@ class TCPNodeEndpoint < EventMachine::Connection
90
112
  end
91
113
  end
92
114
 
93
- # TCP node definition, listen for and invoke json-rpc requests via tcp sockets
115
+ # TCP node definition, listen for and invoke json-rpc requests via TCP sockets
116
+ #
117
+ # Clients should specify the hostname / port when listening for requests and
118
+ # when invoking them.
119
+ #
120
+ # @example Listening for json-rpc requests over tcp
121
+ # # register rjr dispatchers (see RJR::Dispatcher)
122
+ # RJR::Dispatcher.add_handler('hello') { |name|
123
+ # "Hello #{name}!"
124
+ # }
125
+ #
126
+ # # initialize node, listen, and block
127
+ # server = RJR::TCPNode.new :node_id => 'server', :host => 'localhost', :port => '7777'
128
+ # server.listen
129
+ # server.join
130
+ #
131
+ # @example Invoking json-rpc requests over tcp
132
+ # client = RJR::TCPNode.new :node_id => 'client', :host => 'localhost', :port => '8888'
133
+ # puts client.invoke_request('jsonrpc://localhost:7777', 'hello', 'mo')
134
+ #
94
135
  class TCPNode < RJR::Node
95
136
  RJR_NODE_TYPE = :tcp
96
137
 
@@ -98,8 +139,16 @@ class TCPNode < RJR::Node
98
139
  attr_accessor :response_cv
99
140
  attr_accessor :responses
100
141
 
142
+ private
143
+ # Initialize the tcp subsystem
144
+ def init_node
145
+ end
146
+
101
147
  public
102
- # initialize the node w/ the specified params
148
+ # TCPNode initializer
149
+ # @param [Hash] args the options to create the tcp node with
150
+ # @option args [String] :host the hostname/ip which to listen on
151
+ # @option args [Integer] :port the port which to listen on
103
152
  def initialize(args = {})
104
153
  super(args)
105
154
  @host = args[:host]
@@ -114,18 +163,19 @@ class TCPNode < RJR::Node
114
163
  @connection_event_handlers = {:closed => [], :error => []}
115
164
  end
116
165
 
117
- # register connection event handler
166
+ # Register connection event handler
167
+ # @param [:error, :close] event the event to register the handler for
168
+ # @param [Callable] handler block param to be added to array of handlers that are called when event occurs
169
+ # @yield [TCPNode] self is passed to each registered handler when event occurs
118
170
  def on(event, &handler)
119
171
  if @connection_event_handlers.keys.include?(event)
120
172
  @connection_event_handlers[event] << handler
121
173
  end
122
174
  end
123
175
 
124
- # Initialize the tcp subsystem
125
- def init_node
126
- end
127
-
128
176
  # Instruct Node to start listening for and dispatching rpc requests
177
+ #
178
+ # Implementation of {RJR::Node#listen}
129
179
  def listen
130
180
  em_run {
131
181
  init_node
@@ -134,6 +184,10 @@ class TCPNode < RJR::Node
134
184
  end
135
185
 
136
186
  # Instructs node to send rpc request, and wait for / return response
187
+ # @param [String] uri location of node to send request to, should be
188
+ # in format of jsonrpc://hostname:port
189
+ # @param [String] rpc_method json-rpc method to invoke on destination
190
+ # @param [Array] args array of arguments to convert to json and invoke remote method wtih
137
191
  def invoke_request(uri, rpc_method, *args)
138
192
  uri = URI.parse(uri)
139
193
  host,port = uri.host, uri.port
@@ -3,11 +3,14 @@
3
3
  # Copyright (C) 2010-2012 Mohammed Morsi <mo@morsi.org>
4
4
  # Licensed under the Apache License, Version 2.0
5
5
 
6
- # Work item to be executed in thread pool
6
+ # Work item to be executed in a thread launched by pool
7
7
  class ThreadPoolJob
8
8
  attr_accessor :handler
9
9
  attr_accessor :params
10
10
 
11
+ # ThreadPoolJob initializer
12
+ # @param [Array] params arguments to pass to the job when it is invoked
13
+ # @param [Callable] block handle to callable object corresponding to job to invoke
11
14
  def initialize(*params, &block)
12
15
  @params = params
13
16
  @handler = block
@@ -15,11 +18,15 @@ class ThreadPoolJob
15
18
  end
16
19
 
17
20
 
18
- # Launches a specified number of threads on instantiation,
19
- # assigning work to them as it arrives
21
+ # Utility to launches a specified number of threads on instantiation,
22
+ # assigning work to them in order as it arrives.
23
+ #
24
+ # Supports optional timeout which allows the developer to kill and restart
25
+ # threads if a job is taking too long to run.
20
26
  class ThreadPool
21
27
 
22
- # Encapsulate each thread pool thread in object
28
+ # @private
29
+ # Helper class to encapsulate each thread pool thread
23
30
  class ThreadPoolJobRunner
24
31
  attr_accessor :time_started
25
32
 
@@ -29,6 +36,10 @@ class ThreadPool
29
36
  @thread_lock = Mutex.new
30
37
  end
31
38
 
39
+ # Start thread and pull a work items off the thread pool work queue and execute them.
40
+ #
41
+ # This method will return immediately after the worker thread is started but the
42
+ # thread launched will persist until {#stop} is invoked
32
43
  def run
33
44
  @thread_lock.synchronize {
34
45
  @thread = Thread.new {
@@ -49,6 +60,7 @@ class ThreadPool
49
60
  }
50
61
  end
51
62
 
63
+ # Return boolean indicating if worker thread is running or not
52
64
  def running?
53
65
  res = nil
54
66
  @thread_lock.synchronize{
@@ -57,7 +69,8 @@ class ThreadPool
57
69
  res
58
70
  end
59
71
 
60
- # should not invoke after stop is called
72
+ # Return boolean indicating if worker thread run time has exceeded timeout
73
+ # Should not invoke after stop is called
61
74
  def check_timeout(timeout)
62
75
  @timeout_lock.synchronize {
63
76
  if !@time_started.nil? && Time.now - @time_started > timeout
@@ -67,6 +80,7 @@ class ThreadPool
67
80
  }
68
81
  end
69
82
 
83
+ # Stop the worker thread being executed
70
84
  def stop
71
85
  @thread_lock.synchronize {
72
86
  if @thread.alive?
@@ -77,6 +91,7 @@ class ThreadPool
77
91
  }
78
92
  end
79
93
 
94
+ # Block until the worker thread is finished
80
95
  def join
81
96
  @thread_lock.synchronize {
82
97
  @thread.join unless @thread.nil?
@@ -85,6 +100,9 @@ class ThreadPool
85
100
  end
86
101
 
87
102
  # Create a thread pool with a specified number of threads
103
+ # @param [Integer] num_threads the number of worker threads to create
104
+ # @param [Hash] args optional arguments to initialize thread pool with
105
+ # @option args [Integer] :timeout optional timeout to use to kill long running worker jobs
88
106
  def initialize(num_threads, args = {})
89
107
  @num_threads = num_threads
90
108
  @timeout = args[:timeout]
@@ -116,32 +134,37 @@ class ThreadPool
116
134
  end
117
135
  end
118
136
 
137
+ # Return boolean indicated if thread pool is running.
138
+ #
139
+ # If at least one worker thread isn't terminated, the pool is still considered running
119
140
  def running?
120
141
  !terminate && (@timeout.nil? || (!@timeout_thread.nil? && @timeout_thread.status)) &&
121
142
  @job_runners.all? { |r| r.running? }
122
143
  end
123
144
 
124
- # terminate reader
145
+ # Return boolean indicating if the thread pool should be terminated
125
146
  def terminate
126
147
  @terminate_lock.synchronize { @terminate }
127
148
  end
128
149
 
129
- # terminate setter
150
+ # Instruct thread pool to terminate
151
+ # @param [Boolean] val true/false indicating if thread pool should terminate
130
152
  def terminate=(val)
131
153
  @terminate_lock.synchronize { @terminate = val }
132
154
  end
133
155
 
134
156
  # Add work to the pool
157
+ # @param [ThreadPoolJob] work job to execute in first available thread
135
158
  def <<(work)
136
159
  @work_queue.push work
137
160
  end
138
161
 
139
- # Return the next job queued up
162
+ # Return the next job queued up and remove it from the queue
140
163
  def next_job
141
164
  @work_queue.pop
142
165
  end
143
166
 
144
- # Terminate the thread pool
167
+ # Terminate the thread pool, stopping all worker threads
145
168
  def stop
146
169
  terminate = true
147
170
  unless @timout_thread.nil?
@@ -153,6 +176,7 @@ class ThreadPool
153
176
  @job_runners_lock.synchronize { @job_runners.each { |jr| jr.stop } }
154
177
  end
155
178
 
179
+ # Block until all worker threads have finished executing
156
180
  def join
157
181
  @job_runners_lock.synchronize { @job_runners.each { |jr| jr.join } }
158
182
  end
data/lib/rjr/web_node.rb CHANGED
@@ -1,5 +1,10 @@
1
1
  # RJR WWW Endpoint
2
2
  #
3
+ # Implements the RJR::Node interface to satisty JSON-RPC requests over the HTTP protocol
4
+ #
5
+ # The web node does not support callbacks at the moment, though at some point we may
6
+ # allow a client to specify an optional webserver to send callback requests to. (TODO)
7
+ #
3
8
  # Copyright (C) 2012 Mohammed Morsi <mo@morsi.org>
4
9
  # Licensed under the Apache License, Version 2.0
5
10
 
@@ -16,8 +21,8 @@ require 'rjr/message'
16
21
 
17
22
  module RJR
18
23
 
19
- # Web client node callback interface,
20
- # currently does nothing as web connections aren't persistant
24
+ # Web node callback interface, *note* callbacks are not supported on the web
25
+ # node and thus this currently does nothing
21
26
  class WebNodeCallback
22
27
  def initialize()
23
28
  end
@@ -26,16 +31,31 @@ class WebNodeCallback
26
31
  end
27
32
  end
28
33
 
29
- # Web node definition, listen for and invoke json-rpc requests via web requests
34
+ # @private
35
+ # Helper class intialized by eventmachine encapsulating a http connection
30
36
  class WebRequestHandler < EventMachine::Connection
31
37
  include EventMachine::HttpServer
32
38
 
33
39
  RJR_NODE_TYPE = :web
34
40
 
41
+ # WebRequestHandler initializer.
42
+ #
43
+ # specify the WebNode establishing the connection
35
44
  def initialize(*args)
36
45
  @web_node = args[0]
37
46
  end
38
47
 
48
+ # {EventMachine::Connection#process_http_request} callback, handle request messages
49
+ def process_http_request
50
+ # TODO support http protocols other than POST
51
+ msg = @http_post_content.nil? ? '' : @http_post_content
52
+ #@thread_pool << ThreadPoolJob.new { handle_request(msg) }
53
+ handle_request(msg)
54
+ end
55
+
56
+ private
57
+
58
+ # Internal helper, handle request message received
39
59
  def handle_request(message)
40
60
  msg = nil
41
61
  result = nil
@@ -66,38 +86,66 @@ class WebRequestHandler < EventMachine::Connection
66
86
  resp.content_type "application/json"
67
87
  resp.send_response
68
88
  end
89
+ end
69
90
 
70
- def process_http_request
71
- # TODO support http protocols other than POST
72
- msg = @http_post_content.nil? ? '' : @http_post_content
73
- #@thread_pool << ThreadPoolJob.new { handle_request(msg) }
74
- handle_request(msg)
91
+ # Web node definition, listen for and invoke json-rpc requests via web requests
92
+ #
93
+ # Clients should specify the hostname / port when listening for requests and
94
+ # when invoking them.
95
+ #
96
+ # *note* the RJR javascript client also supports sending / receiving json-rpc
97
+ # messages over http
98
+ #
99
+ # @example Listening for json-rpc requests over tcp
100
+ # # register rjr dispatchers (see RJR::Dispatcher)
101
+ # RJR::Dispatcher.add_handler('hello') { |name|
102
+ # "Hello #{name}!"
103
+ # }
104
+ #
105
+ # # initialize node, listen, and block
106
+ # server = RJR::WebNode.new :node_id => 'server', :host => 'localhost', :port => '7777'
107
+ # server.listen
108
+ # server.join
109
+ #
110
+ # @example Invoking json-rpc requests over http using rjr
111
+ # client = RJR::WebNode.new :node_id => 'client'
112
+ # puts client.invoke_request('http://localhost:7777', 'hello', 'mo')
113
+ #
114
+ # @example Invoking json-rpc requests over http using curl
115
+ # $ curl -X POST http://localhost:7777 -d '{"jsonrpc":"2.0","method":"hello","params":["mo"],"id":"123"}'
116
+ # > {"jsonrpc":"2.0","id":"123","result":"Hello mo!"}
117
+ #
118
+ class WebNode < RJR::Node
119
+ private
120
+ # Initialize the web subsystem
121
+ def init_node
75
122
  end
76
123
 
77
- #def receive_data(data)
78
- # puts "~~~~ #{data}"
79
- #end
80
- end
124
+ public
81
125
 
82
- class WebNode < RJR::Node
83
- # initialize the node w/ the specified params
126
+ # TCPNode initializer
127
+ # @param [Hash] args the options to create the tcp node with
128
+ # @option args [String] :host the hostname/ip which to listen on
129
+ # @option args [Integer] :port the port which to listen on
84
130
  def initialize(args = {})
85
131
  super(args)
86
132
  @host = args[:host]
87
133
  @port = args[:port]
88
134
  end
89
135
 
90
- # Initialize the web subsystem
91
- def init_node
92
- end
93
-
94
- # register connection event handler,
95
- # since web node connections aren't persistant, we don't do anything here
136
+ # Register connection event handler,
137
+ #
138
+ # *note* Since web node connections aren't persistant, we don't do anything here.
139
+ # @param [:error, :close] event the event to register the handler for
140
+ # @param [Callable] handler block param to be added to array of handlers that are called when event occurs
141
+ # @yield [LocalNode] self is passed to each registered handler when event occurs
96
142
  def on(event, &handler)
97
143
  # TODO raise error?
98
144
  end
99
145
 
100
146
  # Instruct Node to start listening for and dispatching rpc requests
147
+ #
148
+ # Implementation of {RJR::Node#listen}
101
149
  def listen
102
150
  em_run do
103
151
  init_node
@@ -106,6 +154,10 @@ class WebNode < RJR::Node
106
154
  end
107
155
 
108
156
  # Instructs node to send rpc request, and wait for / return response
157
+ # @param [String] uri location of node to send request to, should be
158
+ # in format of http://hostname:port
159
+ # @param [String] rpc_method json-rpc method to invoke on destination
160
+ # @param [Array] args array of arguments to convert to json and invoke remote method wtih
109
161
  def invoke_request(uri, rpc_method, *args)
110
162
  init_node
111
163
  message = RequestMessage.new :method => rpc_method,