rjr 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,144 @@
1
+ ## RJR - Ruby Json Rpc Library ##
2
+
3
+ Copyright (C) 2012 Mo Morsi <mo@morsi.org>
4
+
5
+ RJR is made available under the Apache License, Version 2.0
6
+
7
+ RJR is an implementation of the {http://en.wikipedia.org/wiki/JSON-RPC JSON-RPC}
8
+ Version 2.0 Specification. It allows a developer to register custom JSON-RPC
9
+ method handlers which may be invoked simultaneously over a variety of transport
10
+ mechanisms.
11
+
12
+ Currently supported transports include:
13
+ tcp, amqp, http (post), websockets, local method calls, (udp coming soon)
14
+
15
+ ### Intro ###
16
+ To install rjr simply run:
17
+ gem install rjr
18
+
19
+ Source code is available via:
20
+ git clone http://github.com/movitto/rjr
21
+
22
+ ### Using ###
23
+
24
+ Simply require rubygems and the rjr library
25
+
26
+ require 'rubygems'
27
+ require 'rjr'
28
+
29
+ server.rb:
30
+
31
+ # define a rpc method called 'hello' which takes
32
+ # one argument and returns it in upper case
33
+ RJR::Dispatcher.add_handler("hello") { |arg|
34
+ arg.upcase
35
+ }
36
+
37
+ # listen for this method via amqp, websockets, http, and via local calls
38
+ amqp_node = RJR::AMQPNode.new :node_id => 'server', :broker => 'localhost'
39
+ ws_node = RJR::WSNode.new :node_id => 'server', :host => 'localhost', :port => 8080
40
+ www_node = RJR::WebNode.new :node_id => 'server', :host => 'localhost', :port => 8888
41
+ local_node = RJR::LocalNode.new :node_id => 'server'
42
+
43
+ # start the server and block
44
+ multi_node = RJR::MultiNode.new :nodes => [amqp_node, ws_node, www_node, local_node]
45
+ multi_node.listen
46
+ multi_node.join
47
+
48
+
49
+ amqp_client.rb:
50
+
51
+ # invoke the method over amqp
52
+ amqp_node = RJR::AMQPNode.new :node_id => 'client', :broker => 'localhost'
53
+ puts amqp_node.invoke_request('server-queue', 'hello', 'world')
54
+
55
+
56
+ ws_client.js:
57
+
58
+ // use the js client to invoke the method via a websocket
59
+ <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
60
+ <script type="text/javascript" src="site/json.js" />
61
+ <script type="text/javascript" src="site/jrw.js" />
62
+ <script type="text/javascript">
63
+ var node = new WSNode('127.0.0.1', '8080');
64
+ node.onopen = function(){
65
+ node.invoke_request('hello', 'rjr');
66
+ };
67
+ node.onsuccess = function(result){
68
+ alert(result);
69
+ };
70
+ node.open();
71
+ </script>
72
+
73
+ ### Reference ###
74
+
75
+ The source repository can be found {https://github.com/movitto/rjr here}
76
+
77
+ Online API documentation and examples can be found {http://rubydoc.info/github/movitto/rjr here}
78
+
79
+ Generate documentation via
80
+
81
+ rake yard
82
+
83
+ Also see specs for detailed usage.
84
+
85
+ ### Advanced ###
86
+
87
+ RJR uses {http://rubyeventmachine.com/ eventmachine} to process server requests.
88
+ Upon being received requests are handed off to a thread pool to free up the reactor.
89
+ It is up to the developer to ensure resources accessed in the method handlers
90
+ are protected from concurrent access.
91
+
92
+ Various metadata fields are made available to json-rpc method handlers through
93
+ instance variables. These include:
94
+
95
+
96
+ <pre>* @rjr_node
97
+ * @rjr_node_id
98
+ * @rjr_node_type
99
+ * @rjr_callback
100
+ * @headers
101
+ * @client_ip
102
+ * @client_port
103
+ * @method
104
+ * @method_args
105
+ * @handler
106
+ </pre>
107
+
108
+ RJR implements a callback interface through which methods may be invoked on a client
109
+ after an initial server connection is established. Store and/or invoke @rjr_callback to make
110
+ use of this.
111
+
112
+ RJR::Dispatcher.add_handler("register_callback") { |*args|
113
+ $my_registry.invoke_me_later {
114
+ # rjr callback will already be setup to send messages to the correct client
115
+ @rjr_callback.invoke 'callback_method', 'with', 'custom', 'params'
116
+ }
117
+ }
118
+
119
+ RJR also permits arbitrary headers being set on JSON-RPC requests and responses. These
120
+ will be stored in the json send to/from nodes, at the same level/scope as the message
121
+ 'id', 'method', and 'params' properties. Developers using RJR may set and leverage these headers
122
+ in their registered handlers to store additional metadata to extend the JSON-RPC protocol and
123
+ support any custom subsystems (an auth subsystem for example)
124
+
125
+ RJR::Dispatcher.add_handler("login") { |*args|
126
+ if $my_user_registry.find(:user => args.first, :pass => args.last)
127
+ @headers['session-id'] = $my_user_registry.create_session.id
128
+ end
129
+ }
130
+
131
+ RJR::Dispatcher.add_handler("do_secure_action") { |*args|
132
+ if $my_user_registry.find(:session_id => @headers['session-id']).nil?
133
+ raise PermissionError, "invalid session"
134
+ end
135
+ # ...
136
+ }
137
+
138
+ Of course any custom headers set/used will only be of use to JSON-RPC nodes running
139
+ RJR as this is not standard JSON-RPC.
140
+
141
+
142
+ ### Authors ###
143
+ Mo Morsi <mo@morsi.org>
144
+ Ladislav Smola <ladislav.smola@it-logica.cz>
data/Rakefile CHANGED
@@ -3,7 +3,7 @@
3
3
  # Copyright (C) 2010-2012 Mohammed Morsi <mo@morsi.org>
4
4
  # Licensed under the Apache License, Version 2.0
5
5
 
6
- require 'rdoc/task'
6
+ require "yard"
7
7
  require "rspec/core/rake_task"
8
8
 
9
9
  desc "Run all specs"
@@ -26,12 +26,12 @@ task :integration do
26
26
  system("tests/integration/runner")
27
27
  end
28
28
 
29
- Rake::RDocTask.new do |rd|
30
- rd.main = "README.rdoc"
31
- rd.rdoc_dir = "doc/site/api"
32
- rd.rdoc_files.include("README.rdoc", "lib/**/*.rb")
29
+ YARD::Rake::YardocTask.new do |t|
30
+ #t.files = ['lib/**/*.rb', OTHER_PATHS] # optional
31
+ #t.options = ['--any', '--extra', '--opts'] # optional
33
32
  end
34
33
 
34
+
35
35
  desc "build the rjr gem"
36
36
  task :build do
37
37
  system "gem build rjr.gemspec"
data/bin/rjr-server CHANGED
@@ -45,7 +45,8 @@ def main()
45
45
  rjr_node = RJR::MultiNode.new :nodes => [amqp_node, ws_node, www_node]
46
46
 
47
47
  rjr_node.listen
48
- rjr_node.terminate # TODO run in signal handler
48
+ rjr_node.join
49
+ rjr_node.stop # TODO run in signal handler
49
50
  end
50
51
 
51
52
  main()
data/lib/rjr.rb CHANGED
@@ -3,6 +3,9 @@
3
3
  # Copyright (C) 2010-2012 Mohammed Morsi <mo@morsi.org>
4
4
  # Licensed under the Apache License, Version 2.0
5
5
 
6
+ # rjr - Ruby Json Rpc
7
+ module RJR ; end
8
+
6
9
  require 'rjr/common'
7
10
  require 'rjr/errors'
8
11
  require 'rjr/thread_pool'
data/lib/rjr/amqp_node.rb CHANGED
@@ -1,11 +1,10 @@
1
1
  # RJR AMQP Endpoint
2
2
  #
3
+ # Implements the RJR::Node interface to satisty JSON-RPC requests over the AMQP protocol
4
+ #
3
5
  # Copyright (C) 2012 Mohammed Morsi <mo@morsi.org>
4
6
  # Licensed under the Apache License, Version 2.0
5
7
 
6
- # establish client connection w/ specified args and invoke block w/
7
- # newly created client, returning it after block terminates
8
-
9
8
  require 'amqp'
10
9
  require 'thread'
11
10
  require 'rjr/node'
@@ -13,25 +12,57 @@ require 'rjr/message'
13
12
 
14
13
  module RJR
15
14
 
16
- # AMQP client node callback interface,
17
- # send data back to client via AMQP.
15
+ # AMQP node callback interface, used to invoke json-rpc methods on a
16
+ # remote node which previously invoked a method on the local one.
17
+ #
18
+ # After a node sends a json-rpc request to another, the either node may send
19
+ # additional requests to each other via amqp through this callback interface
20
+ # until the queues are closed
18
21
  class AMQPNodeCallback
22
+
23
+ # AMQPNodeCallback initializer
24
+ # @param [Hash] args the options to create the amqp node callback with
25
+ # @option args [AMQPNode] :node amqp node used to send/receive messages
26
+ # @option args [String] :destination name of the queue to invoke callbacks on
19
27
  def initialize(args = {})
20
28
  @node = args[:node]
21
29
  @destination = args[:destination]
22
30
  end
23
31
 
32
+ # Implementation of {RJR::NodeCallback#invoke}
24
33
  def invoke(callback_method, *data)
25
34
  msg = RequestMessage.new :method => callback_method, :args => data, :headers => @message_headers
26
35
  @node.publish msg.to_s, :routing_key => @destination, :mandatory => true
27
36
  end
28
37
  end
29
38
 
30
- # AMQP node definition, listen for and invoke json-rpc requests over AMQP
31
- class AMQPNode < RJR::Node
39
+ # AMQP node definition, implements the {RJR::Node} interface to
40
+ # listen for and invoke json-rpc requests over
41
+ # {http://en.wikipedia.org/wiki/Advanced_Message_Queuing_Protocol AMQP}.
42
+ #
43
+ # Clients should specify the amqp broker to connect to when initializing
44
+ # a node and specify the remote queue when invoking requests.
45
+ #
46
+ # @example Listening for json-rpc requests over amqp
47
+ # # register rjr dispatchers (see RJR::Dispatcher)
48
+ # RJR::Dispatcher.add_handler('hello') { |name|
49
+ # "Hello #{name}!"
50
+ # }
51
+ #
52
+ # # initialize node, listen, and block
53
+ # server = RJR::AMQPNode.new :node_id => 'server', :broker => 'localhost'
54
+ # server.listen
55
+ # server.join
56
+ #
57
+ # @example Invoking json-rpc requests over amqp
58
+ # client = RJR::AMQPNode.new :node_id => 'client', :broker => 'localhost'
59
+ # puts client.invoke_request('server-queue', 'hello', 'mo') # the queue name is set to "#{node_id}-queue"
60
+ class AMQPNode < RJR::Node
32
61
  RJR_NODE_TYPE = :amqp
33
62
 
34
63
  private
64
+
65
+ # Internal helper, handle message pulled off queue
35
66
  def handle_message(metadata, msg)
36
67
  if RequestMessage.is_request_message?(msg)
37
68
  reply_to = metadata.reply_to
@@ -43,6 +74,7 @@ class AMQPNode < RJR::Node
43
74
  end
44
75
  end
45
76
 
77
+ # Internal helper, handle request message pulled off queue
46
78
  def handle_request(reply_to, message)
47
79
  msg = RequestMessage.new(:message => message, :headers => @message_headers)
48
80
  headers = @message_headers.merge(msg.headers) # append request message headers
@@ -63,6 +95,7 @@ class AMQPNode < RJR::Node
63
95
  publish response.to_s, :routing_key => reply_to
64
96
  end
65
97
 
98
+ # Internal helper, handle response message pulled off queue
66
99
  def handle_response(message)
67
100
  msg = ResponseMessage.new(:message => message, :headers => @message_headers)
68
101
  res = err = nil
@@ -80,20 +113,6 @@ class AMQPNode < RJR::Node
80
113
  }
81
114
  end
82
115
 
83
- public
84
-
85
- # initialize the node w/ the specified params
86
- def initialize(args = {})
87
- super(args)
88
- @broker = args[:broker]
89
- @connection_event_handlers = {:closed => [], :error => []}
90
- @response_lock = Mutex.new
91
- @response_cv = ConditionVariable.new
92
- @response_check_cv = ConditionVariable.new
93
- @responses = []
94
- @amqp_lock = Mutex.new
95
- end
96
-
97
116
  # Initialize the amqp subsystem
98
117
  def init_node
99
118
  return unless @conn.nil? || !@conn.connected?
@@ -119,27 +138,7 @@ class AMQPNode < RJR::Node
119
138
  end
120
139
  end
121
140
 
122
- # publish a message using the amqp exchange
123
- def publish(*args)
124
- @amqp_lock.synchronize {
125
- #raise RJR::Errors::ConnectionError.new("client unreachable") if @disconnected
126
- @exchange.publish *args
127
- }
128
- nil
129
- end
130
-
131
- # subscribe to messages using the amqp queue
132
- def subscribe(*args, &bl)
133
- return if @listening
134
- @amqp_lock.synchronize {
135
- @listening = true
136
- @queue.subscribe do |metadata, msg|
137
- bl.call metadata, msg
138
- end
139
- }
140
- nil
141
- end
142
-
141
+ # Internal helper, block until response matching message id is received
143
142
  def wait_for_result(message)
144
143
  res = nil
145
144
  while res.nil?
@@ -150,6 +149,8 @@ class AMQPNode < RJR::Node
150
149
  unless res.nil?
151
150
  @responses.delete(res)
152
151
  else
152
+ # we can't just go back to waiting for message here, need to give
153
+ # other nodes a chance to check it first
153
154
  @response_cv.signal
154
155
  @response_check_cv.wait @response_lock
155
156
  end
@@ -159,14 +160,7 @@ class AMQPNode < RJR::Node
159
160
  return res
160
161
  end
161
162
 
162
- # register connection event handler
163
- def on(event, &handler)
164
- if @connection_event_handlers.keys.include?(event)
165
- @connection_event_handlers[event] << handler
166
- end
167
- end
168
-
169
- # run connection event handlers for specified event
163
+ # Internal helper, run connection event handlers for specified event
170
164
  # TODO these are only run when we fail to send message to queue, need to detect when that queue is shutdown & other events
171
165
  def connection_event(event)
172
166
  if @connection_event_handlers.keys.include?(event)
@@ -176,7 +170,59 @@ class AMQPNode < RJR::Node
176
170
  end
177
171
  end
178
172
 
179
- # Instruct Node to start listening for and dispatching rpc requests
173
+ # Internal helper, subscribe to messages using the amqp queue
174
+ def subscribe(*args, &bl)
175
+ return if @listening
176
+ @amqp_lock.synchronize {
177
+ @listening = true
178
+ @queue.subscribe do |metadata, msg|
179
+ bl.call metadata, msg
180
+ end
181
+ }
182
+ nil
183
+ end
184
+
185
+
186
+ public
187
+
188
+ # AMQPNode initializer
189
+ # @param [Hash] args the options to create the amqp node with
190
+ # @option args [String] :broker the amqp message broker which to connect to
191
+ def initialize(args = {})
192
+ super(args)
193
+ @broker = args[:broker]
194
+ @connection_event_handlers = {:closed => [], :error => []}
195
+ @response_lock = Mutex.new
196
+ @response_cv = ConditionVariable.new
197
+ @response_check_cv = ConditionVariable.new
198
+ @responses = []
199
+ @amqp_lock = Mutex.new
200
+ end
201
+
202
+ # Publish a message using the amqp exchange (*do* *not* *use*).
203
+ #
204
+ # XXX hack should be private, declared publically so as to be able to be used by {RJR::AMQPNodeCallback}
205
+ def publish(*args)
206
+ @amqp_lock.synchronize {
207
+ #raise RJR::Errors::ConnectionError.new("client unreachable") if @disconnected
208
+ @exchange.publish *args
209
+ }
210
+ nil
211
+ end
212
+
213
+ # Register connection event handler
214
+ # @param [:error, :close] event the event to register the handler for
215
+ # @param [Callable] handler block param to be added to array of handlers that are called when event occurs
216
+ # @yield [AMQPNode] self is passed to each registered handler when event occurs
217
+ def on(event, &handler)
218
+ if @connection_event_handlers.keys.include?(event)
219
+ @connection_event_handlers[event] << handler
220
+ end
221
+ end
222
+
223
+ # Instruct Node to start listening for and dispatching rpc requests.
224
+ #
225
+ # Implementation of {RJR::Node#listen}
180
226
  def listen
181
227
  em_run do
182
228
  init_node
@@ -188,7 +234,12 @@ class AMQPNode < RJR::Node
188
234
  end
189
235
  end
190
236
 
191
- # Instructs node to send rpc request, and wait for / return response
237
+ # Instructs node to send rpc request, and wait for and return response
238
+ # @param [String] routing_key destination queue to send request to
239
+ # @param [String] rpc_method json-rpc method to invoke on destination
240
+ # @param [Array] args array of arguments to convert to json and invoke remote method wtih
241
+ # @return [Object] the json result retrieved from destination converted to a ruby object
242
+ # @raise [Exception] if the destination raises an exception, it will be converted to json and re-raised here
192
243
  def invoke_request(routing_key, rpc_method, *args)
193
244
  message = RequestMessage.new :method => rpc_method,
194
245
  :args => args,
data/lib/rjr/common.rb CHANGED
@@ -1,13 +1,22 @@
1
1
  # RJR Utility Methods
2
2
  #
3
- # Copyright (C) 2011 Mohammed Morsi <mo@morsi.org>
3
+ # Assortment of helper methods and methods that don't fit elsewhere
4
+ #
5
+ # Copyright (C) 2011-2012 Mohammed Morsi <mo@morsi.org>
4
6
  # Licensed under the Apache License, Version 2.0
5
7
 
6
8
  require 'logger'
7
9
 
8
10
  module RJR
9
11
 
10
- # Logger helper class
12
+ # Logger helper class.
13
+ #
14
+ # Encapsulates the standard ruby logger in a thread safe manner. Dispatches
15
+ # class methods to an internally tracked logger to provide global access.
16
+ #
17
+ # @example
18
+ # RJR::Logger.info 'my message'
19
+ # RJR::Logger.warn 'my warning'
11
20
  class Logger
12
21
  private
13
22
  def self._instantiate_logger
@@ -39,6 +48,8 @@ class Logger
39
48
  @@logger
40
49
  end
41
50
 
51
+ # Set log level.
52
+ # @param level one of the standard rails log levels (default fatal)
42
53
  def self.log_level=(level)
43
54
  _instantiate_logger
44
55
  @@logger.level = level
@@ -48,10 +59,15 @@ end
48
59
  end # module RJR
49
60
 
50
61
  if RUBY_VERSION < "1.9"
51
- # http://blog.jayfields.com/2006/09/ruby-instanceexec-aka-instanceeval.html
62
+ # We extend object in ruby 1.9 to define 'instance_exec'
63
+ #
64
+ # {http://blog.jayfields.com/2006/09/ruby-instanceexec-aka-instanceeval.html Further reference}
52
65
  class Object
53
66
  module InstanceExecHelper; end
54
67
  include InstanceExecHelper
68
+ # Execute the specified block in the scope of the local object
69
+ # @param [Array] args array of args to be passed to block
70
+ # @param [Callable] block callable object to bind and invoke in the local namespace
55
71
  def instance_exec(*args, &block)
56
72
  begin
57
73
  old_critical, Thread.critical = Thread.critical, true