rjr 0.9.0 → 0.11.7

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
@@ -7,7 +7,8 @@
7
7
  # newly created client, returning it after block terminates
8
8
 
9
9
  require 'eventmachine'
10
- require 'rjr/thread_pool'
10
+ require 'rjr/em_adapter'
11
+ require 'rjr/thread_pool2'
11
12
 
12
13
  module RJR
13
14
 
@@ -38,103 +39,114 @@ class Node
38
39
  # requests and responses received and sent by node
39
40
  attr_accessor :message_headers
40
41
 
41
- # Nodes use internal thread pools to handle requests and free
42
- # up the eventmachine reactor to continue processing requests
43
- # @see ThreadPool
44
- attr_reader :thread_pool
45
-
46
42
  # RJR::Node initializer
43
+ #
47
44
  # @param [Hash] args options to set on request
48
45
  # @option args [String] :node_id unique id of the node *required*!!!
49
46
  # @option args [Hash<String,String>] :headers optional headers to set on all json-rpc messages
50
47
  # @option args [Integer] :threads number of handler to threads to instantiate in local worker pool
51
48
  # @option args [Integer] :timeout timeout after which worker thread being run is killed
52
49
  def initialize(args = {})
53
- RJR::Node.default_threads ||= 10
54
- RJR::Node.default_timeout ||= 5
50
+ RJR::Node.default_threads ||= 20
51
+ RJR::Node.default_timeout ||= 10
55
52
 
56
53
  @node_id = args[:node_id]
57
54
  @num_threads = args[:threads] || RJR::Node.default_threads
58
55
  @timeout = args[:timeout] || RJR::Node.default_timeout
56
+ EMAdapter.init
59
57
 
60
58
  @message_headers = {}
61
59
  @message_headers.merge!(args[:headers]) if args.has_key?(:headers)
62
-
63
- ObjectSpace.define_finalizer(self, self.class.finalize(self))
64
60
  end
65
61
 
66
- # Ruby ObjectSpace finalizer to ensure that node terminates all
67
- # operations when object is destroyed
68
- def self.finalize(node)
69
- proc { node.halt ; node.join }
62
+ # Initialize the node, should be called from the event loop
63
+ # before any operation
64
+ def init_node
65
+ EM.error_handler { |e|
66
+ puts "EventMachine raised critical error #{e} #{e.backtrace}"
67
+ # TODO dispatch to registered event handlers (unify events system)
68
+ }
70
69
  end
71
70
 
72
71
  # Run a job in event machine.
72
+ # @param [Callable] bl callback to be invoked by eventmachine
73
+ def em_run(&bl)
74
+ # Nodes use shared thread pool to handle requests and free
75
+ # up the eventmachine reactor to continue processing requests
76
+ # @see ThreadPool2, ThreadPool2Manager
77
+ ThreadPool2Manager.init @num_threads, :timeout => @timeout
78
+
79
+ # Nodes make use of an EM helper interface to schedule operations
80
+ EMAdapter.init
81
+
82
+ EMAdapter.schedule &bl
83
+ end
84
+
85
+ # Run a job async in event machine immediately
86
+ def em_run_async(&bl)
87
+ # same init as em_run
88
+ ThreadPool2Manager.init @num_threads, :timeout => @timeout
89
+ EMAdapter.init
90
+ EMAdapter.schedule {
91
+ ThreadPool2Manager << ThreadPool2Job.new { bl.call }
92
+ }
93
+ end
94
+
95
+ # TODO em_schedule
96
+
97
+ # Run an job async in event machine.
73
98
  #
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.
99
+ # This schedules a thread to be run once after a specified
100
+ # interval via eventmachine
76
101
  #
77
- # For use by subclasses to start listening and sending operations within
78
- # the context of event machine.
102
+ # @param [Integer] seconds interval which to wait before invoking block
103
+ # @param [Callable] bl callback to be periodically invoked by eventmachine
104
+ def em_schedule_async(seconds, &bl)
105
+ # same init as em_run
106
+ ThreadPool2Manager.init @num_threads, :timeout => @timeout
107
+ EMAdapter.init
108
+ EMAdapter.add_timer(seconds) {
109
+ ThreadPool2Manager << ThreadPool2Job.new { bl.call }
110
+ }
111
+ end
112
+
113
+ # Run a job periodically via an event machine timer
79
114
  #
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)
83
- def em_run(&bl)
84
- @@em_jobs ||= 0
85
- @@em_jobs += 1
86
-
87
- @@em_thread ||= nil
88
-
89
- unless !@thread_pool.nil? && @thread_pool.running?
90
- # threads pool to handle incoming requests
91
- @thread_pool = ThreadPool.new(@num_threads, :timeout => @timeout)
92
- end
93
-
94
- if @@em_thread.nil?
95
- @@em_thread =
96
- Thread.new{
97
- begin
98
- EventMachine.run
99
- rescue Exception => e
100
- puts "Critical exception #{e}\n#{e.backtrace.join("\n")}"
101
- ensure
102
- end
103
- }
104
- #sleep 0.5 until EventMachine.reactor_running? # XXX hacky way to do this
105
- end
106
- EventMachine.schedule bl
115
+ # @param [Integer] seconds interval which to invoke block
116
+ # @param [Callable] bl callback to be periodically invoked by eventmachine
117
+ def em_repeat(seconds, &bl)
118
+ # same init as em_run
119
+ ThreadPool2Manager.init @num_threads, :timeout => @timeout
120
+ EMAdapter.init
121
+ EMAdapter.add_periodic_timer seconds, &bl
107
122
  end
108
123
 
109
- # Returns boolean indicating if this node is still running or not
110
- def em_running?
111
- @@em_jobs > 0 && EventMachine.reactor_running?
124
+ # Run an job async via an event machine timer.
125
+ #
126
+ # This schedules a thread to be run in the thread pool on
127
+ # every invocation of a periodic event machine timer.
128
+ #
129
+ # @param [Integer] seconds interval which to invoke block
130
+ # @param [Callable] bl callback to be periodically invoked by eventmachine
131
+ def em_repeat_async(seconds, &bl)
132
+ # same init as em_schedule
133
+ ThreadPool2Manager.init @num_threads, :timeout => @timeout
134
+ EMAdapter.init
135
+ EMAdapter.add_periodic_timer(seconds){
136
+ ThreadPool2Manager << ThreadPool2Job.new { bl.call }
137
+ }
112
138
  end
113
139
 
114
140
  # Block until the eventmachine reactor and thread pool have both completed running
115
141
  def join
116
- @@em_thread.join if @@em_thread
117
- @@em_thread = nil
118
- @thread_pool.join if @thread_pool
119
- @thread_pool = nil
120
- end
121
-
122
- # Decrement the event machine job counter and if equal to zero,
123
- # immediately terminate the node
124
- def stop
125
- @@em_jobs -= 1
126
- if @@em_jobs == 0
127
- EventMachine.stop_event_loop
128
- @thread_pool.stop
129
- end
142
+ ThreadPool2Manager.join
143
+ EMAdapter.join
130
144
  end
131
145
 
132
- # Immediately terminate the node, halting the eventmachine reactor and
133
- # terminating the thread pool
146
+ # Immediately terminate the node
134
147
  def halt
135
- @@em_jobs = 0
136
- EventMachine.stop
137
- @thread_pool.stop unless @thread_pool.nil?
148
+ EMAdapter.halt
149
+ ThreadPool2Manager.stop
138
150
  end
139
151
 
140
152
  end
data/lib/rjr/tcp_node.rb CHANGED
@@ -6,10 +6,14 @@
6
6
  # Licensed under the Apache License, Version 2.0
7
7
 
8
8
  require 'uri'
9
+ require 'socket'
9
10
  require 'eventmachine'
10
11
 
11
12
  require 'rjr/node'
12
13
  require 'rjr/message'
14
+ require 'rjr/message'
15
+ require 'rjr/dispatcher'
16
+ require 'rjr/thread_pool2'
13
17
 
14
18
  module RJR
15
19
 
@@ -32,9 +36,9 @@ class TCPNodeCallback
32
36
 
33
37
  # Implementation of {RJR::NodeCallback#invoke}
34
38
  def invoke(callback_method, *data)
35
- msg = RequestMessage.new :method => callback_method, :args => data, :headers => @message_headers
39
+ msg = NotificationMessage.new :method => callback_method, :args => data, :headers => @message_headers
36
40
  # TODO surround w/ begin/rescue block incase of socket errors
37
- @endpoint.send_data msg.to_s
41
+ @endpoint.safe_send msg.to_s
38
42
  end
39
43
  end
40
44
 
@@ -42,42 +46,68 @@ end
42
46
  # Helper class intialized by eventmachine encapsulating a socket connection
43
47
  class TCPNodeEndpoint < EventMachine::Connection
44
48
 
49
+ attr_reader :host
50
+ attr_reader :port
51
+
45
52
  # TCPNodeEndpoint intializer
46
53
  #
47
- # specify the TCPNode establishing the connection and an optional first message to send
54
+ # specify the TCPNode establishing the connection
48
55
  def initialize(args = {})
49
- @rjr_node = args[:rjr_node]
50
-
51
- # these params should be set for clients
52
- @send_message = args[:init_message]
53
- end
56
+ @rjr_node = args[:rjr_node]
57
+ @host = args[:host]
58
+ @port = args[:port]
54
59
 
55
- # {EventMachine::Connection#post_init} callback, sends first message if specified
56
- def post_init
57
- unless @send_message.nil?
58
- send_data @send_message.to_s
59
- @send_message = nil
60
- end
60
+ # used to serialize requests to send data via a connection
61
+ @send_lock = Mutex.new
61
62
  end
62
63
 
63
64
  # {EventMachine::Connection#receive_data} callback, handle request / response messages
64
65
  def receive_data(data)
65
- if RequestMessage.is_request_message?(data)
66
- @rjr_node.thread_pool << ThreadPoolJob.new { handle_request(data) }
66
+ # a large json-rpc message may be split over multiple packets (invocations of receive_data)
67
+ # and multiple messages may be concatinated into one packet
68
+ @data ||= ""
69
+ @data += data
70
+ while extracted = MessageUtil.retrieve_json(@data)
71
+ msg, @data = *extracted
72
+ if RequestMessage.is_request_message?(msg)
73
+ ThreadPool2Manager << ThreadPool2Job.new(msg) { |m| handle_request(m, false) }
74
+
75
+ elsif NotificationMessage.is_notification_message?(msg)
76
+ ThreadPool2Manager << ThreadPool2Job.new(msg) { |m| handle_request(m, true) }
67
77
 
68
- elsif ResponseMessage.is_response_message?(data)
69
- handle_response(data)
78
+ elsif ResponseMessage.is_response_message?(msg)
79
+ handle_response(msg)
70
80
 
81
+ end
71
82
  end
72
83
  end
73
84
 
85
+ # {EventMachine::Connection#unbind} callback, connection was closed
86
+ def unbind
87
+ end
88
+
89
+ # Helper to send data safely, this should be invoked instead of send_data
90
+ # in all cases
91
+ def safe_send(data)
92
+ @send_lock.synchronize{
93
+ send_data(data)
94
+ }
95
+ end
96
+
74
97
 
75
98
  private
76
99
 
77
100
  # Internal helper, handle request message received
78
- def handle_request(data)
79
- client_port, client_ip = Socket.unpack_sockaddr_in(get_peername)
80
- msg = RequestMessage.new(:message => data, :headers => @rjr_node.message_headers)
101
+ def handle_request(data, notification=false)
102
+ # XXX hack to handle client disconnection (should grap port/ip immediately on connection and use that)
103
+ client_port,client_ip = nil,nil
104
+ begin
105
+ client_port, client_ip = Socket.unpack_sockaddr_in(get_peername)
106
+ rescue Exception=>e
107
+ end
108
+
109
+ msg = notification ? NotificationMessage.new(:message => data, :headers => @rjr_node.message_headers) :
110
+ RequestMessage.new(:message => data, :headers => @rjr_node.message_headers)
81
111
  headers = @rjr_node.message_headers.merge(msg.headers)
82
112
  result = Dispatcher.dispatch_request(msg.jr_method,
83
113
  :method_args => msg.jr_args,
@@ -90,8 +120,10 @@ class TCPNodeEndpoint < EventMachine::Connection
90
120
  :rjr_callback =>
91
121
  TCPNodeCallback.new(:endpoint => self,
92
122
  :headers => headers))
93
- response = ResponseMessage.new(:id => msg.msg_id, :result => result, :headers => headers)
94
- send_data(response.to_s)
123
+ unless notification
124
+ response = ResponseMessage.new(:id => msg.msg_id, :result => result, :headers => headers)
125
+ safe_send(response.to_s)
126
+ end
95
127
  end
96
128
 
97
129
  # Internal helper, handle response message received
@@ -105,8 +137,9 @@ class TCPNodeEndpoint < EventMachine::Connection
105
137
  end
106
138
 
107
139
  @rjr_node.response_lock.synchronize {
108
- @rjr_node.responses << [msg.msg_id, res]
109
- @rjr_node.responses.last << err unless err.nil?
140
+ result = [msg.msg_id, res]
141
+ result << err if !err.nil?
142
+ @rjr_node.responses << result
110
143
  @rjr_node.response_cv.signal
111
144
  }
112
145
  end
@@ -135,13 +168,49 @@ end
135
168
  class TCPNode < RJR::Node
136
169
  RJR_NODE_TYPE = :tcp
137
170
 
171
+ attr_accessor :connections
172
+
138
173
  attr_accessor :response_lock
139
174
  attr_accessor :response_cv
140
175
  attr_accessor :responses
141
176
 
142
177
  private
143
- # Initialize the tcp subsystem
144
- def init_node
178
+ # Internal helper, initialize new connection
179
+ def init_node(args={}, &on_init)
180
+ host,port = args[:host], args[:port]
181
+ connection = nil
182
+ @connections_lock.synchronize {
183
+ connection = @connections.find { |c|
184
+ port == c.port && host == c.host
185
+ }
186
+ if connection.nil?
187
+ connection =
188
+ EventMachine::connect host, port,
189
+ TCPNodeEndpoint, args
190
+ @connections << connection
191
+ end
192
+ }
193
+ on_init.call(connection)
194
+ end
195
+
196
+ # Internal helper, block until response matching message id is received
197
+ def wait_for_result(message)
198
+ res = nil
199
+ while res.nil?
200
+ @response_lock.synchronize{
201
+ # FIXME throw err if more than 1 match found
202
+ res = @responses.select { |response| message.msg_id == response.first }.first
203
+ if !res.nil?
204
+ @responses.delete(res)
205
+
206
+ else
207
+ @response_cv.signal
208
+ @response_cv.wait @response_lock
209
+
210
+ end
211
+ }
212
+ end
213
+ return res
145
214
  end
146
215
 
147
216
  public
@@ -154,10 +223,11 @@ class TCPNode < RJR::Node
154
223
  @host = args[:host]
155
224
  @port = args[:port]
156
225
 
226
+ @connections = []
227
+ @connections_lock = Mutex.new
228
+
157
229
  @response_lock = Mutex.new
158
230
  @response_cv = ConditionVariable.new
159
- @response_cv = ConditionVariable.new
160
- @response_check_cv = ConditionVariable.new
161
231
  @responses = []
162
232
 
163
233
  @connection_event_handlers = {:closed => [], :error => []}
@@ -178,12 +248,15 @@ class TCPNode < RJR::Node
178
248
  # Implementation of {RJR::Node#listen}
179
249
  def listen
180
250
  em_run {
181
- init_node
182
251
  EventMachine::start_server @host, @port, TCPNodeEndpoint, { :rjr_node => self }
183
252
  }
184
253
  end
185
254
 
186
- # Instructs node to send rpc request, and wait for / return response
255
+ # Instructs node to send rpc request, and wait for / return response.
256
+ #
257
+ # Do not invoke directly from em event loop or callback as will block the message
258
+ # subscription used to receive responses
259
+ #
187
260
  # @param [String] uri location of node to send request to, should be
188
261
  # in format of jsonrpc://hostname:port
189
262
  # @param [String] rpc_method json-rpc method to invoke on destination
@@ -196,32 +269,52 @@ class TCPNode < RJR::Node
196
269
  :args => args,
197
270
  :headers => @message_headers
198
271
  em_run{
199
- init_node
200
- EventMachine::connect host, port, TCPNodeEndpoint, { :rjr_node => self,
201
- :init_message => message }
272
+ init_node(:host => host, :port => port,
273
+ :rjr_node => self) { |c|
274
+ c.safe_send message.to_s
275
+ }
202
276
  }
203
277
 
204
- # wait for matching response
205
- res = nil
206
- while res.nil?
207
- @response_lock.synchronize {
208
- @response_cv.wait response_lock
209
- res = @responses.select { |response| message.msg_id == response.first }.first
210
- unless res.nil?
211
- @responses.delete(res)
212
- else
213
- @response_cv.signal
214
- @response_check_cv.wait @response_lock
215
- end
216
- @response_check_cv.signal
217
- }
218
- end
278
+ # TODO optional timeout for response ?
279
+ result = wait_for_result(message)
219
280
 
220
- # raise error or return result
221
- if res.size > 2
222
- raise res[2]
281
+ if result.size > 2
282
+ raise Exception, result[2]
223
283
  end
224
- return res[1]
284
+ return result[1]
285
+ end
286
+
287
+ # Instructs node to send rpc notification (immadiately returns / no response is generated)
288
+ #
289
+ # @param [String] uri location of node to send notification to, should be
290
+ # in format of jsonrpc://hostname:port
291
+ # @param [String] rpc_method json-rpc method to invoke on destination
292
+ # @param [Array] args array of arguments to convert to json and invoke remote method wtih
293
+ def send_notification(uri, rpc_method, *args)
294
+ # will block until message is published
295
+ published_l = Mutex.new
296
+ published_c = ConditionVariable.new
297
+
298
+ uri = URI.parse(uri)
299
+ host,port = uri.host, uri.port
300
+
301
+ conn = nil
302
+ message = NotificationMessage.new :method => rpc_method,
303
+ :args => args,
304
+ :headers => @message_headers
305
+ em_run{
306
+ init_node(:host => host, :port => port,
307
+ :rjr_node => self) { |c|
308
+ conn = c
309
+ c.safe_send message.to_s
310
+ # XXX big bug w/ tcp node, this should be invoked only when
311
+ # we are sure event machine sent message
312
+ published_l.synchronize { published_c.signal }
313
+ }
314
+ }
315
+ published_l.synchronize { published_c.wait published_l }
316
+ #sleep 0.01 until conn.get_outbound_data_size == 0
317
+ nil
225
318
  end
226
319
  end
227
320
 
@@ -0,0 +1,4 @@
1
+ # TODO alternate tcp node that uses client other than
2
+ # event machine to guarantee messages being sent to server but
3
+ # forsaking client side request callbacks as a tradeoff (if necessary?)
4
+ # (not to replace first tcp node, to be used in conjunction)