rjr 0.12.2 → 0.15.1
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/README.md +49 -36
- data/Rakefile +2 -0
- data/bin/rjr-client +11 -9
- data/bin/rjr-server +12 -10
- data/examples/amqp.rb +29 -0
- data/examples/client.rb +32 -0
- data/examples/complete.rb +36 -0
- data/examples/local.rb +29 -0
- data/examples/server.rb +26 -0
- data/examples/tcp.rb +29 -0
- data/examples/web.rb +22 -0
- data/examples/ws.rb +29 -0
- data/lib/rjr/common.rb +7 -12
- data/lib/rjr/dispatcher.rb +171 -239
- data/lib/rjr/em_adapter.rb +33 -66
- data/lib/rjr/message.rb +43 -12
- data/lib/rjr/node.rb +197 -103
- data/lib/rjr/nodes/amqp.rb +216 -0
- data/lib/rjr/nodes/easy.rb +159 -0
- data/lib/rjr/nodes/local.rb +118 -0
- data/lib/rjr/{missing_node.rb → nodes/missing.rb} +4 -2
- data/lib/rjr/nodes/multi.rb +79 -0
- data/lib/rjr/nodes/tcp.rb +211 -0
- data/lib/rjr/nodes/web.rb +197 -0
- data/lib/rjr/nodes/ws.rb +187 -0
- data/lib/rjr/stats.rb +70 -0
- data/lib/rjr/thread_pool.rb +178 -123
- data/site/index.html +45 -0
- data/site/jquery-latest.js +9404 -0
- data/site/jrw.js +297 -0
- data/site/json.js +199 -0
- data/specs/dispatcher_spec.rb +244 -198
- data/specs/em_adapter_spec.rb +52 -80
- data/specs/message_spec.rb +223 -197
- data/specs/node_spec.rb +67 -163
- data/specs/nodes/amqp_spec.rb +82 -0
- data/specs/nodes/easy_spec.rb +13 -0
- data/specs/nodes/local_spec.rb +72 -0
- data/specs/nodes/multi_spec.rb +65 -0
- data/specs/nodes/tcp_spec.rb +75 -0
- data/specs/nodes/web_spec.rb +77 -0
- data/specs/nodes/ws_spec.rb +78 -0
- data/specs/stats_spec.rb +59 -0
- data/specs/thread_pool_spec.rb +44 -35
- metadata +40 -30
- data/lib/rjr/amqp_node.rb +0 -330
- data/lib/rjr/inspect.rb +0 -65
- data/lib/rjr/local_node.rb +0 -150
- data/lib/rjr/multi_node.rb +0 -65
- data/lib/rjr/tcp_node.rb +0 -323
- data/lib/rjr/thread_pool2.rb +0 -272
- data/lib/rjr/util.rb +0 -104
- data/lib/rjr/web_node.rb +0 -266
- data/lib/rjr/ws_node.rb +0 -289
- data/lib/rjr.rb +0 -16
- data/specs/amqp_node_spec.rb +0 -31
- data/specs/inspect_spec.rb +0 -60
- data/specs/local_node_spec.rb +0 -43
- data/specs/multi_node_spec.rb +0 -45
- data/specs/tcp_node_spec.rb +0 -33
- data/specs/util_spec.rb +0 -46
- data/specs/web_node_spec.rb +0 -32
- data/specs/ws_node_spec.rb +0 -32
- /data/lib/rjr/{tcp_node2.rb → nodes/tcp2.rb} +0 -0
- /data/lib/rjr/{udp_node.rb → nodes/udp.rb} +0 -0
data/lib/rjr/message.rb
CHANGED
@@ -2,24 +2,19 @@
|
|
2
2
|
#
|
3
3
|
# Representations of json-rpc messages in accordance with the standard
|
4
4
|
#
|
5
|
-
# Copyright (C) 2012 Mohammed Morsi <mo@morsi.org>
|
5
|
+
# Copyright (C) 2012-2013 Mohammed Morsi <mo@morsi.org>
|
6
6
|
# Licensed under the Apache License, Version 2.0
|
7
7
|
|
8
8
|
# establish client connection w/ specified args and invoke block w/
|
9
9
|
# newly created client, returning it after block terminates
|
10
10
|
|
11
11
|
require 'json'
|
12
|
+
require 'rjr/common'
|
12
13
|
|
13
14
|
module RJR
|
14
15
|
|
15
16
|
# Message sent from client to server to invoke a json-rpc method
|
16
17
|
class RequestMessage
|
17
|
-
# Helper method to generate a random id
|
18
|
-
def self.gen_uuid
|
19
|
-
["%02x"*4, "%02x"*2, "%02x"*2, "%02x"*2, "%02x"*6].join("-") %
|
20
|
-
Array.new(16) {|x| rand(0xff) }
|
21
|
-
end
|
22
|
-
|
23
18
|
# Message string received from the source
|
24
19
|
attr_accessor :json_message
|
25
20
|
|
@@ -51,8 +46,8 @@ class RequestMessage
|
|
51
46
|
def initialize(args = {})
|
52
47
|
if args.has_key?(:message)
|
53
48
|
begin
|
54
|
-
request = JSON.parse(args[:message])
|
55
49
|
@json_message = args[:message]
|
50
|
+
request = JSON.parse(@json_message)
|
56
51
|
@jr_method = request['method']
|
57
52
|
@jr_args = request['params']
|
58
53
|
@msg_id = request['id']
|
@@ -71,7 +66,7 @@ class RequestMessage
|
|
71
66
|
@jr_method = args[:method]
|
72
67
|
@jr_args = args[:args]
|
73
68
|
@headers = args[:headers]
|
74
|
-
@msg_id =
|
69
|
+
@msg_id = gen_uuid
|
75
70
|
|
76
71
|
end
|
77
72
|
end
|
@@ -132,8 +127,8 @@ class ResponseMessage
|
|
132
127
|
# @option args [RJR::Result] :result result of json-rpc method invocation
|
133
128
|
def initialize(args = {})
|
134
129
|
if args.has_key?(:message)
|
135
|
-
response = JSON.parse(args[:message])
|
136
130
|
@json_message = args[:message]
|
131
|
+
response = JSON.parse(@json_message)
|
137
132
|
@msg_id = response['id']
|
138
133
|
@result = Result.new
|
139
134
|
@result.success = response.has_key?('result')
|
@@ -235,8 +230,8 @@ class NotificationMessage
|
|
235
230
|
def initialize(args = {})
|
236
231
|
if args.has_key?(:message)
|
237
232
|
begin
|
238
|
-
notification = JSON.parse(args[:message])
|
239
233
|
@json_message = args[:message]
|
234
|
+
notification = JSON.parse(@json_message)
|
240
235
|
@jr_method = notification['method']
|
241
236
|
@jr_args = notification['params']
|
242
237
|
@headers = args.has_key?(:headers) ? {}.merge!(args[:headers]) : {}
|
@@ -295,7 +290,7 @@ class MessageUtil
|
|
295
290
|
# to the issue of multiple messages appearing in one tcp data packet.
|
296
291
|
#
|
297
292
|
# TODO efficiency can probably be optimized
|
298
|
-
#
|
293
|
+
# in the case closing '}' hasn't arrived yet
|
299
294
|
def self.retrieve_json(data)
|
300
295
|
return nil if data.nil? || data.empty?
|
301
296
|
start = 0
|
@@ -318,6 +313,42 @@ class MessageUtil
|
|
318
313
|
return data[start..mi], data[(mi+1)..-1]
|
319
314
|
end
|
320
315
|
|
316
|
+
# Mechanism to register / retrieve preformatted message
|
317
|
+
#
|
318
|
+
# @param [Symbol] id id of message to get / set
|
319
|
+
# @param [String] msg optional preformatted message to store
|
320
|
+
# @return [String] json rpc message
|
321
|
+
def self.message(id, msg=nil)
|
322
|
+
@rjr_messages ||= {}
|
323
|
+
@rjr_messages[id] = msg unless msg.nil?
|
324
|
+
@rjr_messages[id]
|
325
|
+
end
|
326
|
+
|
327
|
+
# Clear preformatted messages
|
328
|
+
def self.clear
|
329
|
+
@rjr_messages = {}
|
330
|
+
end
|
331
|
+
|
332
|
+
# Return random message from registry.
|
333
|
+
#
|
334
|
+
# Optionally specify the transport which the message must accept
|
335
|
+
# (TODO turn this into a generic selection callback)
|
336
|
+
def self.rand_msg(transport = nil)
|
337
|
+
@rjr_messages ||= {}
|
338
|
+
messages = @rjr_messages.select { |mid,m| m[:transports].nil? || transport.nil? ||
|
339
|
+
m[:transports].include?(transport) }
|
340
|
+
messages[messages.keys[rand(messages.keys.size)]]
|
341
|
+
end
|
342
|
+
|
343
|
+
end # MessageUtil
|
344
|
+
|
345
|
+
# Module providing helper methods for messages
|
346
|
+
module MessageMixins
|
347
|
+
|
348
|
+
# Wrapper around MessageUtil.message
|
349
|
+
def define_message(name, &bl)
|
350
|
+
MessageUtil.message(name, bl.call)
|
351
|
+
end
|
321
352
|
end
|
322
353
|
|
323
354
|
end
|
data/lib/rjr/node.rb
CHANGED
@@ -1,36 +1,38 @@
|
|
1
|
-
# RJR Node
|
1
|
+
# RJR Base Node Interface
|
2
2
|
#
|
3
|
-
# Copyright (C) 2012 Mohammed Morsi <mo@morsi.org>
|
3
|
+
# Copyright (C) 2012-2013 Mohammed Morsi <mo@morsi.org>
|
4
4
|
# Licensed under the Apache License, Version 2.0
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
require '
|
6
|
+
require 'thread'
|
7
|
+
require 'socket'
|
8
|
+
require 'rjr/common'
|
9
|
+
require 'rjr/message'
|
10
|
+
require 'rjr/dispatcher'
|
10
11
|
require 'rjr/em_adapter'
|
11
|
-
require 'rjr/
|
12
|
+
require 'rjr/thread_pool'
|
12
13
|
|
13
14
|
module RJR
|
14
15
|
|
15
|
-
# Base RJR Node interface. Nodes are the central transport mechanism of
|
16
|
+
# Base RJR Node interface. Nodes are the central transport mechanism of RJR,
|
16
17
|
# this class provides the core methods common among all transport types and
|
17
|
-
# mechanisms to start and run the
|
18
|
+
# mechanisms to start and run the subsystems which drives all requests.
|
19
|
+
#
|
20
|
+
# A subclass of RJR::Node should be defined for each transport that is supported.
|
21
|
+
# Each subclass should define
|
22
|
+
# * RJR_NODE_TYPE - unique id of the transport
|
23
|
+
# * listen method - begin listening for new requests and return
|
24
|
+
# * send_message(msg, connection) - send message using the specified connection (transport dependent)
|
25
|
+
# * invoke - establish connection, send message, and wait for / return result
|
26
|
+
# * notify - establish connection, send message, and immediately return
|
27
|
+
#
|
28
|
+
# Not all methods necessarily have to be implemented depending on the context /
|
29
|
+
# use of the node, and the base node class provides many utility methods which
|
30
|
+
# to assist in message processing (see below).
|
18
31
|
#
|
19
|
-
#
|
20
|
-
# implementing the 'listen' operation to listen for new requests and 'invoke_request'
|
21
|
-
# to issue them.
|
32
|
+
# See nodes residing in lib/rjr/nodes/ for specific examples.
|
22
33
|
class Node
|
23
|
-
class << self
|
24
|
-
# @!group Config options
|
25
|
-
|
26
|
-
# Default number of threads to instantiate in local worker pool
|
27
|
-
attr_accessor :default_threads
|
28
|
-
|
29
|
-
# Default timeout after which worker threads are killed
|
30
|
-
attr_accessor :default_timeout
|
31
34
|
|
32
|
-
|
33
|
-
end
|
35
|
+
###################################################################
|
34
36
|
|
35
37
|
# Unique string identifier of the node
|
36
38
|
attr_reader :node_id
|
@@ -39,115 +41,207 @@ class Node
|
|
39
41
|
# requests and responses received and sent by node
|
40
42
|
attr_accessor :message_headers
|
41
43
|
|
44
|
+
# Dispatcher to use to satisfy requests
|
45
|
+
attr_accessor :dispatcher
|
46
|
+
|
42
47
|
# RJR::Node initializer
|
43
48
|
#
|
44
49
|
# @param [Hash] args options to set on request
|
45
|
-
# @option args [String] :node_id unique id of the node
|
50
|
+
# @option args [String] :node_id unique id of the node
|
46
51
|
# @option args [Hash<String,String>] :headers optional headers to set on all json-rpc messages
|
47
|
-
# @option args [
|
48
|
-
# @option args [Integer] :timeout timeout after which worker thread being run is killed
|
52
|
+
# @option args [Dispatcher] :dispatcher dispatcher to assign to the node
|
49
53
|
def initialize(args = {})
|
50
|
-
|
51
|
-
|
54
|
+
@connection_event_handlers = {:closed => [], :error => []}
|
55
|
+
@response_lock = Mutex.new
|
56
|
+
@response_cv = ConditionVariable.new
|
57
|
+
@responses = []
|
52
58
|
|
53
|
-
@node_id
|
54
|
-
@
|
55
|
-
@
|
56
|
-
EMAdapter.init
|
59
|
+
@node_id = args[:node_id]
|
60
|
+
@dispatcher = args[:dispatcher] || RJR::Dispatcher.new
|
61
|
+
@message_headers = args.has_key?(:headers) ? {}.merge(args[:headers]) : {}
|
57
62
|
|
58
|
-
@
|
59
|
-
@
|
63
|
+
@tp = ThreadPool.instance.start
|
64
|
+
@em = EMAdapter.instance.start
|
60
65
|
end
|
61
66
|
|
62
|
-
#
|
63
|
-
#
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
67
|
+
# Block until the eventmachine reactor and thread pool have both completed running
|
68
|
+
#
|
69
|
+
# @return self
|
70
|
+
def join
|
71
|
+
@tp.join
|
72
|
+
@em.join
|
73
|
+
self
|
69
74
|
end
|
70
75
|
|
71
|
-
#
|
72
|
-
#
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
76
|
+
# Immediately terminate the node
|
77
|
+
#
|
78
|
+
# *Warning* this does what it says it does. All running threads, and reactor
|
79
|
+
# jobs are immediately killed
|
80
|
+
#
|
81
|
+
# @return self
|
82
|
+
def halt
|
83
|
+
@em.stop_event_loop
|
84
|
+
@tp.stop
|
85
|
+
self
|
86
|
+
end
|
78
87
|
|
79
|
-
|
80
|
-
EMAdapter.init
|
88
|
+
##################################################################
|
81
89
|
|
82
|
-
|
90
|
+
# Register connection event handler
|
91
|
+
# @param [:error, :close] event the event to register the handler for
|
92
|
+
# @param [Callable] handler block param to be added to array of handlers that are called when event occurs
|
93
|
+
# @yield [Node] self is passed to each registered handler when event occurs
|
94
|
+
def on(event, &handler)
|
95
|
+
if @connection_event_handlers.keys.include?(event)
|
96
|
+
@connection_event_handlers[event] << handler
|
97
|
+
end
|
83
98
|
end
|
84
99
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
100
|
+
private
|
101
|
+
|
102
|
+
# Internal helper, run connection event handlers for specified event
|
103
|
+
def connection_event(event)
|
104
|
+
if @connection_event_handlers.keys.include?(event)
|
105
|
+
@connection_event_handlers[event].each { |h|
|
106
|
+
h.call self
|
107
|
+
}
|
108
|
+
end
|
93
109
|
end
|
94
110
|
|
95
|
-
|
111
|
+
##################################################################
|
96
112
|
|
97
|
-
#
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
ThreadPool2Manager << ThreadPool2Job.new { bl.call }
|
110
|
-
}
|
113
|
+
# Internal helper, handle message received
|
114
|
+
def handle_message(msg, connection = {})
|
115
|
+
if RequestMessage.is_request_message?(msg)
|
116
|
+
@tp << ThreadPoolJob.new(msg) { |m| handle_request(m, false, connection) }
|
117
|
+
|
118
|
+
elsif NotificationMessage.is_notification_message?(msg)
|
119
|
+
@tp << ThreadPoolJob.new(msg) { |m| handle_request(m, true, connection) }
|
120
|
+
|
121
|
+
elsif ResponseMessage.is_response_message?(msg)
|
122
|
+
handle_response(msg)
|
123
|
+
|
124
|
+
end
|
111
125
|
end
|
112
126
|
|
113
|
-
#
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
127
|
+
# Internal helper, handle request message received
|
128
|
+
def handle_request(data, notification=false, connection={})
|
129
|
+
# get client for the specified connection
|
130
|
+
# TODO should grap port/ip immediately on connection and use that
|
131
|
+
client_port,client_ip = nil,nil
|
132
|
+
begin
|
133
|
+
# XXX skip if an 'indirect' node type or local
|
134
|
+
unless [:amqp, :local].include?(self.class::RJR_NODE_TYPE)
|
135
|
+
client_port, client_ip =
|
136
|
+
Socket.unpack_sockaddr_in(connection.get_peername)
|
137
|
+
end
|
138
|
+
rescue Exception=>e
|
139
|
+
end
|
140
|
+
|
141
|
+
msg = notification ?
|
142
|
+
NotificationMessage.new(:message => data,
|
143
|
+
:headers => @message_headers) :
|
144
|
+
RequestMessage.new(:message => data,
|
145
|
+
:headers => @message_headers)
|
146
|
+
|
147
|
+
result =
|
148
|
+
@dispatcher.dispatch(:rjr_method => msg.jr_method,
|
149
|
+
:rjr_method_args => msg.jr_args,
|
150
|
+
:headers => msg.headers,
|
151
|
+
:rjr_client_ip => client_ip,
|
152
|
+
:rjr_client_port => client_port,
|
153
|
+
:rjr_node => self,
|
154
|
+
:rjr_node_id => @node_id,
|
155
|
+
:rjr_node_type => self.class::RJR_NODE_TYPE,
|
156
|
+
:rjr_callback =>
|
157
|
+
NodeCallback.new(:node => self,
|
158
|
+
:connection => connection))
|
159
|
+
|
160
|
+
unless notification
|
161
|
+
response = ResponseMessage.new(:id => msg.msg_id,
|
162
|
+
:result => result,
|
163
|
+
:headers => msg.headers)
|
164
|
+
self.send_msg(response.to_s, connection)
|
165
|
+
return response
|
166
|
+
end
|
122
167
|
end
|
123
168
|
|
124
|
-
#
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
169
|
+
# Internal helper, handle response message received
|
170
|
+
def handle_response(data)
|
171
|
+
msg = ResponseMessage.new(:message => data, :headers => self.message_headers)
|
172
|
+
res = err = nil
|
173
|
+
begin
|
174
|
+
res = @dispatcher.handle_response(msg.result)
|
175
|
+
rescue Exception => e
|
176
|
+
err = e
|
177
|
+
end
|
178
|
+
|
179
|
+
@response_lock.synchronize {
|
180
|
+
result = [msg.msg_id, res]
|
181
|
+
result << err if !err.nil?
|
182
|
+
@responses << result
|
183
|
+
@response_cv.broadcast
|
137
184
|
}
|
138
185
|
end
|
139
186
|
|
140
|
-
#
|
141
|
-
def
|
142
|
-
|
143
|
-
|
187
|
+
# Internal helper, block until response matching message id is received
|
188
|
+
def wait_for_result(message)
|
189
|
+
res = nil
|
190
|
+
while res.nil?
|
191
|
+
@response_lock.synchronize{
|
192
|
+
# FIXME throw err if more than 1 match found
|
193
|
+
res = @responses.find { |response| message.msg_id == response.first }
|
194
|
+
if !res.nil?
|
195
|
+
@responses.delete(res)
|
196
|
+
|
197
|
+
else
|
198
|
+
# FIXME if halt is invoked while this is sleeping, all other threads
|
199
|
+
# may be deleted resulting in this sleeping indefinetly and a deadlock
|
200
|
+
|
201
|
+
# TODO wait for a finite # of seconds, record time we started waiting
|
202
|
+
# before while loop and on every iteration check to see if we've been
|
203
|
+
# waiting longer than an optional timeout. If so throw an error (also
|
204
|
+
# need mechanism to discard result if it comes in later).
|
205
|
+
# finite # of seconds we wait and optional timeout should be
|
206
|
+
# configurable on node class
|
207
|
+
@response_cv.wait @response_lock
|
208
|
+
|
209
|
+
end
|
210
|
+
}
|
211
|
+
end
|
212
|
+
return res
|
144
213
|
end
|
145
214
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
215
|
+
end # class Node
|
216
|
+
|
217
|
+
# Node callback interface, used to invoke json-rpc methods
|
218
|
+
# against a remote node via node connection previously established
|
219
|
+
#
|
220
|
+
# After a node sends a json-rpc request to another, the either node may send
|
221
|
+
# additional requests to each other via the connection already established until
|
222
|
+
# it is closed on either end
|
223
|
+
class NodeCallback
|
224
|
+
|
225
|
+
# NodeCallback initializer
|
226
|
+
# @param [Hash] args the options to create the node callback with
|
227
|
+
# @option args [node] :node node used to send messages
|
228
|
+
# @option args [connection] :connection connection to be used in channel selection
|
229
|
+
def initialize(args = {})
|
230
|
+
@node = args[:node]
|
231
|
+
@connection = args[:connection]
|
150
232
|
end
|
151
233
|
|
234
|
+
def notify(callback_method, *data)
|
235
|
+
# XXX return if node type does not support
|
236
|
+
# pesistent conntections (throw err instead?)
|
237
|
+
return if @node.class::RJR_NODE_TYPE == :web
|
238
|
+
|
239
|
+
msg = NotificationMessage.new :method => callback_method,
|
240
|
+
:args => data, :headers => @node.message_headers
|
241
|
+
|
242
|
+
# TODO surround w/ begin/rescue block incase of socket errors / raise RJR::ConnectionError
|
243
|
+
@node.send_msg msg.to_s, @connection
|
244
|
+
end
|
152
245
|
end
|
246
|
+
|
153
247
|
end # module RJR
|
@@ -0,0 +1,216 @@
|
|
1
|
+
# RJR AMQP Node
|
2
|
+
#
|
3
|
+
# Implements the RJR::Node interface to satisty JSON-RPC requests over the AMQP protocol
|
4
|
+
#
|
5
|
+
# Copyright (C) 2012-2013 Mohammed Morsi <mo@morsi.org>
|
6
|
+
# Licensed under the Apache License, Version 2.0
|
7
|
+
|
8
|
+
skip_module = false
|
9
|
+
begin
|
10
|
+
require 'amqp'
|
11
|
+
rescue LoadError
|
12
|
+
skip_module = true
|
13
|
+
end
|
14
|
+
|
15
|
+
if skip_module
|
16
|
+
# TODO output: "amqp gem could not be loaded, skipping amqp node definition"
|
17
|
+
require 'rjr/nodes/missing'
|
18
|
+
RJR::Nodes::AMQP = RJR::Nodes::Missing
|
19
|
+
|
20
|
+
else
|
21
|
+
require 'thread'
|
22
|
+
require 'rjr/node'
|
23
|
+
require 'rjr/message'
|
24
|
+
|
25
|
+
module RJR
|
26
|
+
module Nodes
|
27
|
+
|
28
|
+
# AMQP node definition, implements the {RJR::Node} interface to
|
29
|
+
# listen for and invoke json-rpc requests over the
|
30
|
+
# Advanced Message Queuing Protocol.
|
31
|
+
#
|
32
|
+
# Clients should specify the amqp broker to connect to when initializing
|
33
|
+
# a node and specify the remote queue when invoking requests.
|
34
|
+
#
|
35
|
+
# @example Listening for json-rpc requests over amqp
|
36
|
+
# # initialize node,
|
37
|
+
# server = RJR::Nodes::AMQP.new :node_id => 'server', :broker => 'localhost'
|
38
|
+
#
|
39
|
+
# # register rjr dispatchers (see RJR::Dispatcher)
|
40
|
+
# server.dispatcher.handle('hello') do |name|
|
41
|
+
# "Hello #{name}!"
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# # listen, and block
|
45
|
+
# server.listen
|
46
|
+
# server.join
|
47
|
+
#
|
48
|
+
# @example Invoking json-rpc requests over amqp
|
49
|
+
# client = RJR::Nodes::AMQP.new :node_id => 'client', :broker => 'localhost'
|
50
|
+
# puts client.invoke('server-queue', 'hello', 'mo') # the queue name is set to "#{node_id}-queue"
|
51
|
+
#
|
52
|
+
class AMQP < RJR::Node
|
53
|
+
RJR_NODE_TYPE = :amqp
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
# Internal helper, initialize the amqp subsystem
|
58
|
+
def init_node(&on_init)
|
59
|
+
if !@conn.nil? && @conn.connected?
|
60
|
+
on_init.call
|
61
|
+
return
|
62
|
+
end
|
63
|
+
|
64
|
+
@conn = ::AMQP.connect(:host => @broker) do |*args|
|
65
|
+
on_init.call
|
66
|
+
end
|
67
|
+
@conn.on_tcp_connection_failure { puts "OTCF #{@node_id}" }
|
68
|
+
|
69
|
+
# TODO move the rest into connect callback ?
|
70
|
+
|
71
|
+
### connect to qpid broker
|
72
|
+
@channel = ::AMQP::Channel.new(@conn)
|
73
|
+
|
74
|
+
# qpid constructs that will be created for node
|
75
|
+
@queue_name = "#{@node_id.to_s}-queue"
|
76
|
+
@queue = @channel.queue(@queue_name, :auto_delete => true)
|
77
|
+
@exchange = @channel.default_exchange
|
78
|
+
|
79
|
+
@listening = false
|
80
|
+
#@disconnected = false
|
81
|
+
|
82
|
+
@exchange.on_return do |basic_return, metadata, payload|
|
83
|
+
puts "#{payload} was returned! reply_code = #{basic_return.reply_code}, reply_text = #{basic_return.reply_text}"
|
84
|
+
#@disconnected = true # FIXME member will be set on wrong class
|
85
|
+
# TODO these are only run when we fail to send message to queue, need to detect when that queue is shutdown & other events
|
86
|
+
connection_event(:error)
|
87
|
+
connection_event(:closed)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Internal helper, subscribe to messages using the amqp queue
|
92
|
+
def subscribe
|
93
|
+
if @listening
|
94
|
+
return
|
95
|
+
end
|
96
|
+
|
97
|
+
@amqp_lock.synchronize {
|
98
|
+
@listening = true
|
99
|
+
@queue.subscribe do |metadata, msg|
|
100
|
+
# swap reply to and routing key
|
101
|
+
handle_message(msg, {:routing_key => metadata.reply_to, :reply_to => @queue_name})
|
102
|
+
end
|
103
|
+
}
|
104
|
+
nil
|
105
|
+
end
|
106
|
+
|
107
|
+
public
|
108
|
+
|
109
|
+
# AMQPNode initializer
|
110
|
+
#
|
111
|
+
# @param [Hash] args the options to create the amqp node with
|
112
|
+
# @option args [String] :broker the amqp message broker which to connect to
|
113
|
+
def initialize(args = {})
|
114
|
+
super(args)
|
115
|
+
@broker = args[:broker]
|
116
|
+
@amqp_lock = Mutex.new
|
117
|
+
end
|
118
|
+
|
119
|
+
# Publish a message using the amqp exchange
|
120
|
+
#
|
121
|
+
# Implementation of {RJR::Node#send_msg}
|
122
|
+
def send_msg(msg, metadata, &on_publish)
|
123
|
+
@amqp_lock.synchronize {
|
124
|
+
#raise RJR::Errors::ConnectionError.new("client unreachable") if @disconnected
|
125
|
+
routing_key = metadata[:routing_key]
|
126
|
+
reply_to = metadata[:reply_to]
|
127
|
+
@exchange.publish msg,
|
128
|
+
:routing_key => routing_key,
|
129
|
+
:reply_to => reply_to do |*cargs|
|
130
|
+
on_publish.call unless on_publish.nil?
|
131
|
+
end
|
132
|
+
}
|
133
|
+
nil
|
134
|
+
end
|
135
|
+
|
136
|
+
# Instruct Node to start listening for and dispatching rpc requests.
|
137
|
+
#
|
138
|
+
# Implementation of {RJR::Node#listen}
|
139
|
+
def listen
|
140
|
+
@em.schedule do
|
141
|
+
init_node {
|
142
|
+
subscribe # start receiving messages
|
143
|
+
}
|
144
|
+
end
|
145
|
+
self
|
146
|
+
end
|
147
|
+
|
148
|
+
# Instructs node to send rpc request, and wait for and return response.
|
149
|
+
#
|
150
|
+
# Implementation of {RJR::Node#invoke}
|
151
|
+
#
|
152
|
+
# Do not invoke directly from em event loop or callback as will block the message
|
153
|
+
# subscription used to receive responses
|
154
|
+
#
|
155
|
+
# @param [String] routing_key destination queue to send request to
|
156
|
+
# @param [String] rpc_method json-rpc method to invoke on destination
|
157
|
+
# @param [Array] args array of arguments to convert to json and invoke remote method wtih
|
158
|
+
# @return [Object] the json result retrieved from destination converted to a ruby object
|
159
|
+
# @raise [Exception] if the destination raises an exception, it will be converted to json and re-raised here
|
160
|
+
def invoke(routing_key, rpc_method, *args)
|
161
|
+
message = RequestMessage.new :method => rpc_method,
|
162
|
+
:args => args,
|
163
|
+
:headers => @message_headers
|
164
|
+
@em.schedule do
|
165
|
+
init_node {
|
166
|
+
subscribe # begin listening for result
|
167
|
+
send_msg(message.to_s, :routing_key => routing_key, :reply_to => @queue_name)
|
168
|
+
}
|
169
|
+
end
|
170
|
+
|
171
|
+
# TODO optional timeout for response
|
172
|
+
result = wait_for_result(message)
|
173
|
+
|
174
|
+
if result.size > 2
|
175
|
+
raise Exception, result[2]
|
176
|
+
end
|
177
|
+
return result[1]
|
178
|
+
end
|
179
|
+
|
180
|
+
# FIXME add method to instruct node to send rpc request, and immediately
|
181
|
+
# return / ignoring response & also add method to collect response
|
182
|
+
# at a later time
|
183
|
+
|
184
|
+
# Instructs node to send rpc notification (immadiately returns / no response is generated)
|
185
|
+
#
|
186
|
+
# Implementation of {RJR::Node#notify}
|
187
|
+
#
|
188
|
+
# @param [String] routing_key destination queue to send request to
|
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
|
191
|
+
def notify(routing_key, rpc_method, *args)
|
192
|
+
# will block until message is published
|
193
|
+
published_l = Mutex.new
|
194
|
+
published_c = ConditionVariable.new
|
195
|
+
|
196
|
+
invoked = false
|
197
|
+
message = NotificationMessage.new :method => rpc_method,
|
198
|
+
:args => args,
|
199
|
+
:headers => @message_headers
|
200
|
+
@em.schedule do
|
201
|
+
init_node {
|
202
|
+
send_msg(message.to_s, :routing_key => routing_key, :reply_to => @queue_name){
|
203
|
+
published_l.synchronize { invoked = true ; published_c.signal }
|
204
|
+
}
|
205
|
+
}
|
206
|
+
end
|
207
|
+
published_l.synchronize { published_c.wait published_l unless invoked }
|
208
|
+
nil
|
209
|
+
end
|
210
|
+
|
211
|
+
end # class AMQP
|
212
|
+
|
213
|
+
end # module Nodes
|
214
|
+
end # module RJR
|
215
|
+
|
216
|
+
end # (!skip_module)
|