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/bin/rjr-client +150 -0
- data/bin/rjr-client-launcher +38 -0
- data/bin/rjr-server +54 -32
- data/lib/rjr/amqp_node.rb +95 -37
- data/lib/rjr/common.rb +60 -10
- data/lib/rjr/dispatcher.rb +98 -16
- data/lib/rjr/em_adapter.rb +110 -0
- data/lib/rjr/inspect.rb +66 -0
- data/lib/rjr/local_node.rb +1 -0
- data/lib/rjr/message.rb +123 -3
- data/lib/rjr/missing_node.rb +17 -0
- data/lib/rjr/multi_node.rb +3 -0
- data/lib/rjr/node.rb +79 -67
- data/lib/rjr/tcp_node.rb +146 -53
- data/lib/rjr/tcp_node2.rb +4 -0
- data/lib/rjr/thread_pool2.rb +271 -0
- data/lib/rjr/util.rb +104 -0
- data/lib/rjr/web_node.rb +115 -23
- data/lib/rjr/ws_node.rb +162 -34
- data/lib/rjr.rb +5 -10
- data/specs/dispatcher_spec.rb +81 -0
- data/specs/em_adapter_spec.rb +85 -0
- data/specs/inspect_spec.rb +60 -0
- data/specs/message_spec.rb +58 -0
- data/specs/multi_node_spec.rb +5 -4
- data/specs/node_spec.rb +140 -4
- data/specs/tcp_node_spec.rb +1 -0
- data/specs/thread_pool_spec.rb +41 -0
- data/specs/util_spec.rb +46 -0
- data/specs/web_node_spec.rb +1 -0
- data/specs/ws_node_spec.rb +1 -1
- metadata +24 -8
- data/lib/rjr/web_socket.rb +0 -589
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/
|
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 ||=
|
54
|
-
RJR::Node.default_timeout ||=
|
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
|
-
#
|
67
|
-
#
|
68
|
-
def
|
69
|
-
|
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
|
75
|
-
#
|
99
|
+
# This schedules a thread to be run once after a specified
|
100
|
+
# interval via eventmachine
|
76
101
|
#
|
77
|
-
#
|
78
|
-
#
|
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
|
-
#
|
81
|
-
#
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
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
|
-
#
|
110
|
-
|
111
|
-
|
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
|
-
|
117
|
-
|
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
|
133
|
-
# terminating the thread pool
|
146
|
+
# Immediately terminate the node
|
134
147
|
def halt
|
135
|
-
|
136
|
-
|
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 =
|
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.
|
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
|
54
|
+
# specify the TCPNode establishing the connection
|
48
55
|
def initialize(args = {})
|
49
|
-
@rjr_node
|
50
|
-
|
51
|
-
|
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
|
-
|
56
|
-
|
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
|
-
|
66
|
-
|
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
|
-
|
69
|
-
|
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
|
-
|
80
|
-
|
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
|
-
|
94
|
-
|
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
|
-
|
109
|
-
|
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
|
-
#
|
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
|
-
|
201
|
-
|
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
|
-
#
|
205
|
-
|
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
|
-
|
221
|
-
|
222
|
-
raise res[2]
|
281
|
+
if result.size > 2
|
282
|
+
raise Exception, result[2]
|
223
283
|
end
|
224
|
-
return
|
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
|
|