rjr 0.9.0 → 0.11.7

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