rjr 0.12.2 → 0.15.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. data/README.md +49 -36
  2. data/Rakefile +2 -0
  3. data/bin/rjr-client +11 -9
  4. data/bin/rjr-server +12 -10
  5. data/examples/amqp.rb +29 -0
  6. data/examples/client.rb +32 -0
  7. data/examples/complete.rb +36 -0
  8. data/examples/local.rb +29 -0
  9. data/examples/server.rb +26 -0
  10. data/examples/tcp.rb +29 -0
  11. data/examples/web.rb +22 -0
  12. data/examples/ws.rb +29 -0
  13. data/lib/rjr/common.rb +7 -12
  14. data/lib/rjr/dispatcher.rb +171 -239
  15. data/lib/rjr/em_adapter.rb +33 -66
  16. data/lib/rjr/message.rb +43 -12
  17. data/lib/rjr/node.rb +197 -103
  18. data/lib/rjr/nodes/amqp.rb +216 -0
  19. data/lib/rjr/nodes/easy.rb +159 -0
  20. data/lib/rjr/nodes/local.rb +118 -0
  21. data/lib/rjr/{missing_node.rb → nodes/missing.rb} +4 -2
  22. data/lib/rjr/nodes/multi.rb +79 -0
  23. data/lib/rjr/nodes/tcp.rb +211 -0
  24. data/lib/rjr/nodes/web.rb +197 -0
  25. data/lib/rjr/nodes/ws.rb +187 -0
  26. data/lib/rjr/stats.rb +70 -0
  27. data/lib/rjr/thread_pool.rb +178 -123
  28. data/site/index.html +45 -0
  29. data/site/jquery-latest.js +9404 -0
  30. data/site/jrw.js +297 -0
  31. data/site/json.js +199 -0
  32. data/specs/dispatcher_spec.rb +244 -198
  33. data/specs/em_adapter_spec.rb +52 -80
  34. data/specs/message_spec.rb +223 -197
  35. data/specs/node_spec.rb +67 -163
  36. data/specs/nodes/amqp_spec.rb +82 -0
  37. data/specs/nodes/easy_spec.rb +13 -0
  38. data/specs/nodes/local_spec.rb +72 -0
  39. data/specs/nodes/multi_spec.rb +65 -0
  40. data/specs/nodes/tcp_spec.rb +75 -0
  41. data/specs/nodes/web_spec.rb +77 -0
  42. data/specs/nodes/ws_spec.rb +78 -0
  43. data/specs/stats_spec.rb +59 -0
  44. data/specs/thread_pool_spec.rb +44 -35
  45. metadata +40 -30
  46. data/lib/rjr/amqp_node.rb +0 -330
  47. data/lib/rjr/inspect.rb +0 -65
  48. data/lib/rjr/local_node.rb +0 -150
  49. data/lib/rjr/multi_node.rb +0 -65
  50. data/lib/rjr/tcp_node.rb +0 -323
  51. data/lib/rjr/thread_pool2.rb +0 -272
  52. data/lib/rjr/util.rb +0 -104
  53. data/lib/rjr/web_node.rb +0 -266
  54. data/lib/rjr/ws_node.rb +0 -289
  55. data/lib/rjr.rb +0 -16
  56. data/specs/amqp_node_spec.rb +0 -31
  57. data/specs/inspect_spec.rb +0 -60
  58. data/specs/local_node_spec.rb +0 -43
  59. data/specs/multi_node_spec.rb +0 -45
  60. data/specs/tcp_node_spec.rb +0 -33
  61. data/specs/util_spec.rb +0 -46
  62. data/specs/web_node_spec.rb +0 -32
  63. data/specs/ws_node_spec.rb +0 -32
  64. /data/lib/rjr/{tcp_node2.rb → nodes/tcp2.rb} +0 -0
  65. /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 = RequestMessage.gen_uuid
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
- # in the case closing '}' hasn't arrived yet
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
- # establish client connection w/ specified args and invoke block w/
7
- # newly created client, returning it after block terminates
8
-
9
- require 'eventmachine'
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/thread_pool2'
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 rjr,
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 eventmachine reactor which drives all requests.
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
- # A subclass of RJR::Node should be defined for each transport that is supported,
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
- # @!endgroup
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 *required*!!!
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 [Integer] :threads number of handler to threads to instantiate in local worker pool
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
- RJR::Node.default_threads ||= 20
51
- RJR::Node.default_timeout ||= 10
54
+ @connection_event_handlers = {:closed => [], :error => []}
55
+ @response_lock = Mutex.new
56
+ @response_cv = ConditionVariable.new
57
+ @responses = []
52
58
 
53
- @node_id = args[:node_id]
54
- @num_threads = args[:threads] || RJR::Node.default_threads
55
- @timeout = args[:timeout] || RJR::Node.default_timeout
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
- @message_headers = {}
59
- @message_headers.merge!(args[:headers]) if args.has_key?(:headers)
63
+ @tp = ThreadPool.instance.start
64
+ @em = EMAdapter.instance.start
60
65
  end
61
66
 
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
- }
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
- # 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
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
- # Nodes make use of an EM helper interface to schedule operations
80
- EMAdapter.init
88
+ ##################################################################
81
89
 
82
- EMAdapter.schedule &bl
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
- # 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
- }
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
- # TODO em_schedule
111
+ ##################################################################
96
112
 
97
- # Run an job async in event machine.
98
- #
99
- # This schedules a thread to be run once after a specified
100
- # interval via eventmachine
101
- #
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
- }
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
- # Run a job periodically via an event machine timer
114
- #
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
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
- # 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 }
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
- # Block until the eventmachine reactor and thread pool have both completed running
141
- def join
142
- ThreadPool2Manager.join
143
- EMAdapter.join
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
- # Immediately terminate the node
147
- def halt
148
- EMAdapter.halt
149
- ThreadPool2Manager.stop
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)