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/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
|
|