rjr 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md 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