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 +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
|