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.
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)