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