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 +144 -0
- data/Rakefile +5 -5
- data/bin/rjr-server +2 -1
- data/lib/rjr.rb +3 -0
- data/lib/rjr/amqp_node.rb +103 -52
- data/lib/rjr/common.rb +19 -3
- data/lib/rjr/dispatcher.rb +103 -7
- data/lib/rjr/errors.rb +5 -3
- data/lib/rjr/local_node.rb +38 -12
- data/lib/rjr/message.rb +56 -1
- data/lib/rjr/multi_node.rb +31 -4
- data/lib/rjr/node.rb +57 -8
- data/lib/rjr/tcp_node.rb +65 -11
- data/lib/rjr/thread_pool.rb +33 -9
- data/lib/rjr/web_node.rb +72 -20
- data/lib/rjr/ws_node.rb +54 -12
- metadata +35 -2
- data/README.rdoc +0 -73
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
|
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::
|
30
|
-
|
31
|
-
|
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
data/lib/rjr.rb
CHANGED
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
|
17
|
-
#
|
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,
|
31
|
-
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
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
|
-
#
|
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
|
-
#
|
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
|