rjr 0.5.3

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.rdoc ADDED
@@ -0,0 +1,73 @@
1
+ == RJR - Ruby Json Rpc Library
2
+
3
+ Copyright (C) 2012 Mo Morsi <mo@morsi.org>
4
+
5
+ RJR is made available under the GNU AFFERO GENERAL PUBLIC LICENSE
6
+ as published by the Free Software Foundation, either version 3
7
+ of the License, or (at your option) any later version.
8
+
9
+ === Intro
10
+ To install rjr simply run:
11
+ gem install rjr
12
+
13
+ Source code is available via:
14
+ git clone http://github.com/movitto/rjr
15
+
16
+ === Using
17
+
18
+ Simply require rubygems and the rjr library
19
+
20
+ require 'rubygems'
21
+ require 'rjr'
22
+
23
+ server.rb:
24
+
25
+ # define a rpc method called 'hello' which takes
26
+ # one argument and returns it in upper case
27
+ RJR::Dispatcher.add_handler("hello") { |arg|
28
+ arg.upcase
29
+ }
30
+
31
+ # listen for this method via amqp, websockets, http, and via local calls
32
+ amqp_node = RJR::AMQPNode.new :node_id => 'server', :broker => 'localhost'
33
+ ws_node = RJR::WSNode.new :node_id => 'server', :host => 'localhost', :port => 8080
34
+ www_node = RJR::WebNode.new :node_id => 'server', :host => 'localhost', :port => 8888
35
+ local_node = RJR::LocalNode.new :node_id => 'server'
36
+
37
+ # start the server and block
38
+ multi_node = RJR::MultiNode.new :nodes => [amqp_node, ws_node, www_node, local_node]
39
+ multi_node.listen
40
+ multi_node.join
41
+
42
+
43
+ amqp_client.rb:
44
+
45
+ # invoke the method over amqp
46
+ amqp_node = RJR::AMQPNode.new :node_id => 'client', :broker => 'localhost'
47
+ puts amqp_node.invoke_request('server-queue', 'hello', 'world')
48
+
49
+
50
+ ws_client.js:
51
+
52
+ // use the js client to invoke the method via a websocket
53
+ <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
54
+ <script type="text/javascript" src="site/json.js" />
55
+ <script type="text/javascript" src="site/jrw.js" />
56
+ <script type="text/javascript">
57
+ var node = new WSNode('127.0.0.1', '8080');
58
+ node.onopen = function(){
59
+ node.invoke_request('hello', 'rjr');
60
+ };
61
+ node.onsuccess = function(result){
62
+ alert(result);
63
+ };
64
+ node.open();
65
+ </script>
66
+
67
+ Generate documentation via
68
+ rake rdoc
69
+
70
+ Also see specs for detailed usage.
71
+
72
+ === Authors
73
+ Mo Morsi <mo@morsi.org>
data/Rakefile ADDED
@@ -0,0 +1,64 @@
1
+ # rjr project Rakefile
2
+ #
3
+ # Copyright (C) 2010 Mohammed Morsi <movitto@yahoo.com>
4
+ # Licensed under the AGPLv3+ http://www.gnu.org/licenses/agpl.txt
5
+
6
+ require 'rdoc/task'
7
+ require "rspec/core/rake_task"
8
+ require 'rubygems/package_task'
9
+
10
+
11
+ GEM_NAME="rjr"
12
+ PKG_VERSION='0.5.3'
13
+
14
+ desc "Run all specs"
15
+ RSpec::Core::RakeTask.new(:spec) do |spec|
16
+ spec.pattern = 'specs/**/*_spec.rb'
17
+ spec.rspec_opts = ['--backtrace']
18
+ end
19
+
20
+ desc "run javascript tests"
21
+ task :test_js do
22
+ ENV['RUBYLIB'] = "lib"
23
+ puts "Launching js test runner"
24
+ system("tests/js/runner")
25
+ end
26
+
27
+ desc "run integration/stress tests"
28
+ task :integration do
29
+ ENV['RUBYLIB'] = "lib"
30
+ puts "Launching integration test runner"
31
+ system("tests/integration/runner")
32
+ end
33
+
34
+ Rake::RDocTask.new do |rd|
35
+ rd.main = "README.rdoc"
36
+ rd.rdoc_dir = "doc/site/api"
37
+ rd.rdoc_files.include("README.rdoc", "lib/**/*.rb")
38
+ end
39
+
40
+ PKG_FILES = FileList['lib/**/*.rb',
41
+ 'LICENSE', 'Rakefile', 'README.rdoc', 'spec/**/*.rb' ]
42
+
43
+ SPEC = Gem::Specification.new do |s|
44
+ s.name = GEM_NAME
45
+ s.version = PKG_VERSION
46
+ s.files = PKG_FILES
47
+ s.executables << 'rjr-server'
48
+
49
+ s.required_ruby_version = '>= 1.8.1'
50
+ s.required_rubygems_version = Gem::Requirement.new(">= 1.3.3")
51
+ s.add_development_dependency('rspec', '~> 1.3.0')
52
+
53
+ s.author = "Mohammed Morsi"
54
+ s.email = "mo@morsi.org"
55
+ s.date = %q{2012-04-25}
56
+ s.description = %q{Ruby Json Rpc library}
57
+ s.summary = %q{JSON RPC server and client library over amqp, websockets}
58
+ s.homepage = %q{http://morsi.org/projects/rjr}
59
+ end
60
+
61
+ Gem::PackageTask.new(SPEC) do |pkg|
62
+ pkg.need_tar = true
63
+ pkg.need_zip = true
64
+ end
data/bin/rjr-server ADDED
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/ruby
2
+ # A rjr server executable
3
+ # Executable to launch registered rjr methods
4
+ #
5
+ # Flags:
6
+ # -h --help
7
+ #
8
+ # Copyright (C) 2011 Mohammed Morsi <mo@morsi.org>
9
+ # Licensed under the AGPLv3+ http://www.gnu.org/licenses/agpl.txt
10
+
11
+ require 'rubygems'
12
+ require 'optparse'
13
+ require 'rjr'
14
+
15
+ ######################
16
+
17
+
18
+ def main()
19
+ # setup cmd line options
20
+ opts = OptionParser.new do |opts|
21
+ opts.on("-h", "--help", "Print help message") do
22
+ puts opts
23
+ exit
24
+ end
25
+ end
26
+
27
+ # parse cmd line
28
+ begin
29
+ opts.parse!(ARGV)
30
+ rescue OptionParser::InvalidOption
31
+ puts opts
32
+ exit
33
+ end
34
+
35
+ amqp_node = RJR::AMQPNode.new :node_id => 'rjr', :broker => 'localhost'
36
+ ws_node = RJR::WSNode.new :node_id => 'rjr', :host => 'localhost', :port => 8080
37
+ www_node = RJR::WebNode.new :node_id => 'rjr', :host => 'localhost', :port => 8888
38
+
39
+ RJR::Dispatcher.add_handler('hello') { |msg|
40
+ #raise Exception.new("foobar")
41
+ puts "hello #{msg}"
42
+ "world"
43
+ }
44
+
45
+ rjr_node = RJR::MultiNode.new :nodes => [amqp_node, ws_node, www_node]
46
+
47
+ rjr_node.listen
48
+ rjr_node.terminate # TODO run in signal handler
49
+ end
50
+
51
+ main()
@@ -0,0 +1,164 @@
1
+ # RJR AMQP Endpoint
2
+ #
3
+ # Copyright (C) 2012 Mohammed Morsi <mo@morsi.org>
4
+ # Licensed under the AGPLv3+ http://www.gnu.org/licenses/agpl.txt
5
+
6
+ # establish client connection w/ specified args and invoke block w/
7
+ # newly created client, returning it after block terminates
8
+
9
+ require 'amqp'
10
+ require 'thread'
11
+ require 'rjr/node'
12
+ require 'rjr/message'
13
+
14
+ module RJR
15
+
16
+ # AMQP client node callback interface,
17
+ # send data back to client via AMQP.
18
+ class AMQPNodeCallback
19
+ def initialize(args = {})
20
+ @exchange = args[:exchange]
21
+ @exchange_lock = args[:exchange_lock]
22
+ @destination = args[:destination]
23
+ @message_headers = args[:headers]
24
+ @disconnected = false
25
+
26
+ @exchange_lock.synchronize{
27
+ # FIXME should disconnect all callbacks on_return
28
+ @exchange.on_return do |basic_return, metadata, payload|
29
+ puts "#{payload} was returned! reply_code = #{basic_return.reply_code}, reply_text = #{basic_return.reply_text}"
30
+ @disconnected = true
31
+ end
32
+ }
33
+ end
34
+
35
+ def invoke(callback_method, *data)
36
+ msg = RequestMessage.new :method => callback_method, :args => data, :headers => @message_headers
37
+ raise RJR::Errors::ConnectionError.new("client unreachable") if @disconnected
38
+ @exchange_lock.synchronize{
39
+ @exchange.publish(msg.to_s, :routing_key => @destination, :mandatory => true)
40
+ }
41
+ end
42
+ end
43
+
44
+ # AMQP node definition, listen for and invoke json-rpc requests over AMQP
45
+ class AMQPNode < RJR::Node
46
+ RJR_NODE_TYPE = :amqp
47
+
48
+
49
+ private
50
+ def handle_message(metadata, msg)
51
+ if RequestMessage.is_request_message?(msg)
52
+ reply_to = metadata.reply_to
53
+
54
+ # TODO should delete handler threads as they complete & should handle timeout
55
+ @thread_pool << ThreadPoolJob.new { handle_request(reply_to, msg) }
56
+
57
+ elsif ResponseMessage.is_response_message?(msg)
58
+ # TODO test message, make sure it is a response message
59
+ msg = ResponseMessage.new(:message => msg, :headers => @message_headers)
60
+ lock = @message_locks[msg.msg_id]
61
+ if lock
62
+ headers = @message_headers.merge(msg.headers)
63
+ res = Dispatcher.handle_response(msg.result)
64
+ lock << res
65
+ lock[0].synchronize { lock[1].signal }
66
+ end
67
+
68
+ end
69
+ end
70
+
71
+ def handle_request(reply_to, message)
72
+ msg = RequestMessage.new(:message => message, :headers => @message_headers)
73
+ headers = @message_headers.merge(msg.headers) # append request message headers
74
+ result = Dispatcher.dispatch_request(msg.jr_method,
75
+ :method_args => msg.jr_args,
76
+ :headers => headers,
77
+ :rjr_node_id => @node_id,
78
+ :rjr_node_type => RJR_NODE_TYPE,
79
+ :rjr_callback =>
80
+ AMQPNodeCallback.new(:exchange => @exchange,
81
+ :exchange_lock => @exchange_lock,
82
+ :destination => reply_to,
83
+ :headers => headers))
84
+ response = ResponseMessage.new(:id => msg.msg_id, :result => result, :headers => headers)
85
+ @exchange_lock.synchronize{
86
+ @exchange.publish(response.to_s, :routing_key => reply_to)
87
+ }
88
+ end
89
+
90
+ public
91
+
92
+ # initialize the node w/ the specified params
93
+ def initialize(args = {})
94
+ super(args)
95
+ @broker = args[:broker]
96
+
97
+ # tuple of message ids to locks/condition variables for the responses
98
+ # of those messages with optional result response
99
+ @message_locks = {}
100
+ end
101
+
102
+ # Initialize the amqp subsystem
103
+ def init_node
104
+ @conn = AMQP.connect(:host => @broker)
105
+ @conn.on_tcp_connection_failure { puts "OTCF #{@node_id}" }
106
+
107
+ ### connect to qpid broker
108
+ @channel = AMQP::Channel.new(@conn)
109
+
110
+ # qpid constructs that will be created for node
111
+ @queue_name = "#{@node_id.to_s}-queue"
112
+ @queue = @channel.queue(@queue_name, :auto_delete => true)
113
+ @exchange = @channel.default_exchange
114
+ @exchange_lock = Mutex.new
115
+ end
116
+
117
+ # Instruct Node to start listening for and dispatching rpc requests
118
+ def listen
119
+ em_run do
120
+ init_node
121
+
122
+ # start receiving messages
123
+ @queue.subscribe do |metadata, msg|
124
+ handle_message(metadata, msg)
125
+ end
126
+ end
127
+ end
128
+
129
+ # Instructs node to send rpc request, and wait for / return response
130
+ def invoke_request(routing_key, rpc_method, *args)
131
+ req_mutex = Mutex.new
132
+ req_cv = ConditionVariable.new
133
+
134
+ message = RequestMessage.new :method => rpc_method,
135
+ :args => args,
136
+ :headers => @message_headers
137
+ em_run do
138
+ init_node
139
+
140
+ @message_locks[message.msg_id] = [req_mutex, req_cv]
141
+
142
+ # begin listening for result
143
+ @queue.subscribe do |metadata, msg|
144
+ handle_message(metadata, msg)
145
+ end
146
+
147
+ @exchange_lock.synchronize{
148
+ @exchange.publish(message.to_s, :routing_key => routing_key, :reply_to => @queue_name)
149
+ }
150
+ end
151
+
152
+ ## wait for result
153
+ # TODO - make this optional, eg a non-blocking operation mode
154
+ # (allowing event handler registration to be run on success / fail / etc)
155
+ req_mutex.synchronize { req_cv.wait(req_mutex) }
156
+ result = @message_locks[message.msg_id][2]
157
+ @message_locks.delete(message.msg_id)
158
+ self.stop
159
+ self.join unless self.em_running?
160
+ return result
161
+ end
162
+
163
+ end
164
+ end
data/lib/rjr/common.rb ADDED
@@ -0,0 +1,60 @@
1
+ # RJR Utility Methods
2
+ #
3
+ # Copyright (C) 2011 Mohammed Morsi <mo@morsi.org>
4
+ # Licensed under the AGPLv3+ http://www.gnu.org/licenses/agpl.txt
5
+
6
+ require 'logger'
7
+
8
+ module RJR
9
+
10
+ # Logger helper class
11
+ class Logger
12
+ private
13
+ def self._instantiate_logger
14
+ unless defined? @@logger
15
+ @@logger = ::Logger.new(STDOUT)
16
+ @@logger.level = ::Logger::FATAL
17
+ end
18
+ end
19
+
20
+ public
21
+
22
+ def self.method_missing(method_id, *args)
23
+ _instantiate_logger
24
+ @@logger.send(method_id, args)
25
+ end
26
+
27
+ def self.logger
28
+ _instantiate_logger
29
+ @@logger
30
+ end
31
+
32
+ def self.log_level=(level)
33
+ _instantiate_logger
34
+ @@logger.level = level
35
+ end
36
+ end
37
+
38
+ end # module RJR
39
+
40
+ # http://blog.jayfields.com/2006/09/ruby-instanceexec-aka-instanceeval.html
41
+ class Object
42
+ module InstanceExecHelper; end
43
+ include InstanceExecHelper
44
+ def instance_exec(*args, &block)
45
+ begin
46
+ old_critical, Thread.critical = Thread.critical, true
47
+ n = 0
48
+ n += 1 while respond_to?(mname="__instance_exec#{n}")
49
+ InstanceExecHelper.module_eval{ define_method(mname, &block) }
50
+ ensure
51
+ Thread.critical = old_critical
52
+ end
53
+ begin
54
+ ret = send(mname, *args)
55
+ ensure
56
+ InstanceExecHelper.module_eval{ remove_method(mname) } rescue nil
57
+ end
58
+ ret
59
+ end
60
+ end
@@ -0,0 +1,169 @@
1
+ # RJR Request / Response Dispatcher
2
+ #
3
+ # Copyright (C) 2012 Mohammed Morsi <mo@morsi.org>
4
+ # Licensed under the AGPLv3+ http://www.gnu.org/licenses/agpl.txt
5
+
6
+ # establish client connection w/ specified args and invoke block w/
7
+ # newly created client, returning it after block terminates
8
+
9
+ require 'rjr/common'
10
+
11
+ module RJR
12
+
13
+ class Request
14
+ attr_accessor :method
15
+ attr_accessor :method_args
16
+ attr_accessor :headers
17
+ attr_accessor :rjr_callback
18
+ attr_accessor :rjr_node_type
19
+ attr_accessor :rjr_node_id
20
+
21
+ attr_accessor :handler
22
+
23
+ def initialize(args = {})
24
+ @method = args[:method]
25
+ @method_args = args[:method_args]
26
+ @headers = args[:headers]
27
+ @rjr_callback = args[:rjr_callback]
28
+ @rjr_node_id = args[:rjr_node_id]
29
+ @rjr_node_type = args[:rjr_node_type]
30
+ @handler = args[:handler]
31
+ end
32
+
33
+ def handle
34
+ RJR::Logger.info "Dispatching '#{@method}' request with parameters (#{@method_args.join(',')}) on #{@rjr_node_type}-node(#{@rjr_node_id})"
35
+ retval = instance_exec(*@method_args, &@handler)
36
+ RJR::Logger.info "#{@method} request with parameters (#{@method_args.join(',')}) returning #{retval}"
37
+ return retval
38
+ end
39
+ end
40
+
41
+ class Result
42
+ attr_accessor :success
43
+ attr_accessor :failed
44
+ attr_accessor :result
45
+ attr_accessor :error_code
46
+ attr_accessor :error_msg
47
+ attr_accessor :error_class
48
+
49
+ def initialize(args = {})
50
+ @result = nil
51
+ @error_code = nil
52
+ @error_message = nil
53
+ @error_class = nil
54
+
55
+ if args.has_key?(:result)
56
+ @success = true
57
+ @failed = false
58
+ @result = args[:result]
59
+
60
+ elsif args.has_key?(:error_code)
61
+ @success = false
62
+ @failed = true
63
+ @error_code = args[:error_code]
64
+ @error_msg = args[:error_msg]
65
+ @error_class = args[:error_class]
66
+
67
+ end
68
+ end
69
+
70
+ def ==(other)
71
+ @success == other.success &&
72
+ @failed == other.failed &&
73
+ @result == other.result &&
74
+ @error_code == other.error_code &&
75
+ @error_msg == other.error_msg &&
76
+ @error_class == other.error_class
77
+ end
78
+
79
+ def to_s
80
+ "#{@success} #{@result} #{@error_code} #{@error_msg} #{@error_class}"
81
+ end
82
+
83
+ ######### Specific request types
84
+
85
+ def self.invalid_request
86
+ return Result.new(:error_code => -32600,
87
+ :error_msg => ' Invalid Request')
88
+ end
89
+
90
+ def self.method_not_found(name)
91
+ return Result.new(:error_code => -32602,
92
+ :error_msg => "Method '#{name}' not found")
93
+ end
94
+
95
+ end
96
+
97
+ class Handler
98
+ attr_accessor :method_name
99
+ attr_accessor :handler_proc
100
+
101
+ def initialize(args = {})
102
+ @method_name = args[:method]
103
+ @handler_proc = args[:handler]
104
+ end
105
+
106
+ def handle(args = {})
107
+ return Result.method_not_found(args[:missing_name]) if @method_name.nil?
108
+
109
+ begin
110
+ request = Request.new args.merge(:method => @method_name,
111
+ :handler => @handler_proc)
112
+ retval = request.handle
113
+ return Result.new(:result => retval)
114
+
115
+ rescue Exception => e
116
+ RJR::Logger.warn "Exception Raised in #{method_name} handler #{e}"
117
+ e.backtrace.each { |b| RJR::Logger.warn b }
118
+ # TODO store exception class to be raised later
119
+
120
+ return Result.new(:error_code => -32000,
121
+ :error_msg => e.to_s,
122
+ :error_class => e.class)
123
+
124
+ end
125
+ end
126
+ end
127
+
128
+ class Dispatcher
129
+ # clear handlers
130
+ def self.init_handlers
131
+ @@handlers = {}
132
+ end
133
+
134
+ # register a handler to the specified method
135
+ def self.add_handler(method_name, args = {}, &handler)
136
+ @@handlers ||= {}
137
+ @@handlers[method_name] = Handler.new args.merge(:method => method_name,
138
+ :handler => handler)
139
+ end
140
+
141
+ # Helper to handle request messages
142
+ def self.dispatch_request(method_name, args = {})
143
+ @@handlers ||= {}
144
+ handler = @@handlers[method_name]
145
+
146
+ if handler.nil?
147
+ @@generic_handler ||= Handler.new :method => nil
148
+ return @@generic_handler.handle(args.merge(:missing_name => method_name))
149
+ end
150
+
151
+ return handler.handle args
152
+ end
153
+
154
+ # Helper to handle response messages
155
+ def self.handle_response(result)
156
+ unless result.success
157
+ #if result.error_class
158
+ # TODO needs to be constantized first (see TODO in lib/rjr/message)
159
+ # raise result.error_class.new(result.error_msg) unless result.success
160
+ #else
161
+ raise Exception, result.error_msg
162
+ #end
163
+ end
164
+ return result.result
165
+ end
166
+
167
+ end
168
+
169
+ end # module RJR
data/lib/rjr/errors.rb ADDED
@@ -0,0 +1,21 @@
1
+ # RJR Errors
2
+ #
3
+ # Copyright (C) 2012 Mohammed Morsi <mo@morsi.org>
4
+ # Licensed under the AGPLv3+ http://www.gnu.org/licenses/agpl.txt
5
+
6
+ # establish client connection w/ specified args and invoke block w/
7
+ # newly created client, returning it after block terminates
8
+
9
+ module RJR
10
+ module Errors
11
+
12
+ def self.const_missing(error_name) # :nodoc:
13
+ if error_name.to_s =~ /Error\z/
14
+ const_set(error_name, Class.new(RuntimeError))
15
+ else
16
+ super
17
+ end
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,65 @@
1
+ # RJR Local Endpoint
2
+ #
3
+ # Copyright (C) 2012 Mohammed Morsi <mo@morsi.org>
4
+ # Licensed under the AGPLv3+ http://www.gnu.org/licenses/agpl.txt
5
+
6
+ # establish client connection w/ specified args and invoke block w/
7
+ # newly created client, returning it after block terminates
8
+
9
+ require 'rjr/node'
10
+ require 'rjr/message'
11
+
12
+ module RJR
13
+
14
+ # Local client node callback interface,
15
+ # send data back to client via local handlers
16
+ class LocalNodeCallback
17
+ def initialize(args = {})
18
+ @node = args[:node]
19
+ end
20
+
21
+ def invoke(callback_method, *data)
22
+ @node.invoke_request(callback_method, *data)
23
+ # TODO support local_node 'disconnections'
24
+ end
25
+ end
26
+
27
+ # Local node definition, listen for and invoke json-rpc
28
+ # requests via local handlers
29
+ class LocalNode < RJR::Node
30
+ RJR_NODE_TYPE = :local
31
+
32
+ # allow clients to override the node type for the local node
33
+ attr_accessor :node_type
34
+
35
+ # initialize the node w/ the specified params
36
+ def initialize(args = {})
37
+ super(args)
38
+ @node_type = RJR_NODE_TYPE
39
+ end
40
+
41
+ # Instruct Node to start listening for and dispatching rpc requests
42
+ def listen
43
+ em_run do
44
+ end
45
+ end
46
+
47
+ # Instructs node to send rpc request, and wait for / return response
48
+ def invoke_request(rpc_method, *args)
49
+ 0.upto(args.size).each { |i| args[i] = args[i].to_s if args[i].is_a?(Symbol) }
50
+ message = RequestMessage.new :method => rpc_method,
51
+ :args => args,
52
+ :headers => @message_headers
53
+ result = Dispatcher.dispatch_request(rpc_method,
54
+ :method_args => args,
55
+ :headers => @message_headers,
56
+ :rjr_node_id => @node_id,
57
+ :rjr_node_type => @node_type,
58
+ :rjr_callback =>
59
+ LocalNodeCallback.new(:node => self,
60
+ :headers => @message_headers))
61
+ return Dispatcher.handle_response(result)
62
+ end
63
+
64
+ end
65
+ end