rjr 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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,