rjr 0.5.3

Sign up to get free protection for your applications and to get access to all the features.
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