rjr 0.5.4 → 0.6.1

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/Rakefile CHANGED
@@ -5,11 +5,6 @@
5
5
 
6
6
  require 'rdoc/task'
7
7
  require "rspec/core/rake_task"
8
- require 'rubygems/package_task'
9
-
10
-
11
- GEM_NAME="rjr"
12
- PKG_VERSION='0.5.4'
13
8
 
14
9
  desc "Run all specs"
15
10
  RSpec::Core::RakeTask.new(:spec) do |spec|
@@ -37,28 +32,7 @@ Rake::RDocTask.new do |rd|
37
32
  rd.rdoc_files.include("README.rdoc", "lib/**/*.rb")
38
33
  end
39
34
 
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://github.com/movitto/rjr}
59
- end
60
-
61
- Gem::PackageTask.new(SPEC) do |pkg|
62
- pkg.need_tar = true
63
- pkg.need_zip = true
35
+ desc "build the rjr gem"
36
+ task :build do
37
+ system "gem build rjr.gemspec"
64
38
  end
data/lib/rjr/amqp_node.rb CHANGED
@@ -17,27 +17,13 @@ module RJR
17
17
  # send data back to client via AMQP.
18
18
  class AMQPNodeCallback
19
19
  def initialize(args = {})
20
- @exchange = args[:exchange]
21
- @exchange_lock = args[:exchange_lock]
20
+ @node = args[:node]
22
21
  @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
22
  end
34
23
 
35
24
  def invoke(callback_method, *data)
36
25
  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
- }
26
+ @node.publish msg.to_s, :routing_key => @destination, :mandatory => true
41
27
  end
42
28
  end
43
29
 
@@ -45,25 +31,14 @@ end
45
31
  class AMQPNode < RJR::Node
46
32
  RJR_NODE_TYPE = :amqp
47
33
 
48
-
49
34
  private
50
35
  def handle_message(metadata, msg)
51
36
  if RequestMessage.is_request_message?(msg)
52
37
  reply_to = metadata.reply_to
53
-
54
- # TODO should delete handler threads as they complete & should handle timeout
55
38
  @thread_pool << ThreadPoolJob.new { handle_request(reply_to, msg) }
56
39
 
57
40
  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
41
+ handle_response(msg)
67
42
 
68
43
  end
69
44
  end
@@ -74,16 +49,34 @@ class AMQPNode < RJR::Node
74
49
  result = Dispatcher.dispatch_request(msg.jr_method,
75
50
  :method_args => msg.jr_args,
76
51
  :headers => headers,
52
+ :client_ip => nil, # since client doesn't directly connect to server, we can't leverage
53
+ :client_port => nil, # client ip / port for requests received via the amqp node type
54
+ :rjr_node => self,
77
55
  :rjr_node_id => @node_id,
78
56
  :rjr_node_type => RJR_NODE_TYPE,
79
57
  :rjr_callback =>
80
- AMQPNodeCallback.new(:exchange => @exchange,
81
- :exchange_lock => @exchange_lock,
58
+ AMQPNodeCallback.new(:node => self,
59
+ :exchange => @exchange,
60
+ :amqp_lock => @amqp_lock,
82
61
  :destination => reply_to,
83
62
  :headers => headers))
84
63
  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)
64
+ publish response.to_s, :routing_key => reply_to
65
+ end
66
+
67
+ def handle_response(message)
68
+ msg = ResponseMessage.new(:message => message, :headers => @message_headers)
69
+ res = err = nil
70
+ begin
71
+ res = Dispatcher.handle_response(msg.result)
72
+ rescue Exception => e
73
+ err = e
74
+ end
75
+
76
+ @response_lock.synchronize{
77
+ @result = [res]
78
+ @result << err if !err.nil?
79
+ @response_cv.signal
87
80
  }
88
81
  end
89
82
 
@@ -93,14 +86,14 @@ class AMQPNode < RJR::Node
93
86
  def initialize(args = {})
94
87
  super(args)
95
88
  @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 = {}
89
+ @connection_event_handlers = {:closed => [], :error => []}
90
+ @response_lock = Mutex.new
91
+ @response_cv = ConditionVariable.new
100
92
  end
101
93
 
102
94
  # Initialize the amqp subsystem
103
95
  def init_node
96
+ return unless @conn.nil? || !@conn.connected?
104
97
  @conn = AMQP.connect(:host => @broker)
105
98
  @conn.on_tcp_connection_failure { puts "OTCF #{@node_id}" }
106
99
 
@@ -111,7 +104,57 @@ class AMQPNode < RJR::Node
111
104
  @queue_name = "#{@node_id.to_s}-queue"
112
105
  @queue = @channel.queue(@queue_name, :auto_delete => true)
113
106
  @exchange = @channel.default_exchange
114
- @exchange_lock = Mutex.new
107
+
108
+ @listening = false
109
+ @disconnected = false
110
+
111
+ @exchange.on_return do |basic_return, metadata, payload|
112
+ puts "#{payload} was returned! reply_code = #{basic_return.reply_code}, reply_text = #{basic_return.reply_text}"
113
+ @disconnected = true # FIXME member will be set on wrong class
114
+ connection_event(:error)
115
+ connection_event(:closed)
116
+ end
117
+ end
118
+
119
+ # publish a message using the amqp exchange
120
+ def publish(*args)
121
+ raise RJR::Errors::ConnectionError.new("client unreachable") if @disconnected
122
+ @exchange.publish *args
123
+ end
124
+
125
+ # subscribe to messages using the amqp queue
126
+ def subscribe(*args, &bl)
127
+ return if @listening
128
+ @listening = true
129
+ @queue.subscribe do |metadata, msg|
130
+ bl.call metadata, msg
131
+ end
132
+ end
133
+
134
+ def wait_for_result(message)
135
+ res = nil
136
+ @response_lock.synchronize{
137
+ @response_cv.wait @response_lock
138
+ res = @result
139
+ }
140
+ return res
141
+ end
142
+
143
+ # register connection event handler
144
+ def on(event, &handler)
145
+ if @connection_event_handlers.keys.include?(event)
146
+ @connection_event_handlers[event] << handler
147
+ end
148
+ end
149
+
150
+ # run connection event handlers for specified event
151
+ # TODO these are only run when we fail to send message to queue, need to detect when that queue is shutdown & other events
152
+ def connection_event(event)
153
+ if @connection_event_handlers.keys.include?(event)
154
+ @connection_event_handlers[event].each { |h|
155
+ h.call self
156
+ }
157
+ end
115
158
  end
116
159
 
117
160
  # Instruct Node to start listening for and dispatching rpc requests
@@ -120,44 +163,36 @@ class AMQPNode < RJR::Node
120
163
  init_node
121
164
 
122
165
  # start receiving messages
123
- @queue.subscribe do |metadata, msg|
124
- handle_message(metadata, msg)
125
- end
166
+ subscribe { |metadata, msg|
167
+ handle_message(metadata, msg)
168
+ }
126
169
  end
127
170
  end
128
171
 
129
172
  # Instructs node to send rpc request, and wait for / return response
130
173
  def invoke_request(routing_key, rpc_method, *args)
131
- req_mutex = Mutex.new
132
- req_cv = ConditionVariable.new
133
-
134
174
  message = RequestMessage.new :method => rpc_method,
135
175
  :args => args,
136
176
  :headers => @message_headers
137
177
  em_run do
138
178
  init_node
139
179
 
140
- @message_locks[message.msg_id] = [req_mutex, req_cv]
141
-
142
180
  # begin listening for result
143
- @queue.subscribe do |metadata, msg|
181
+ subscribe { |metadata, msg|
144
182
  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
183
  }
184
+
185
+ publish message.to_s, :routing_key => routing_key, :reply_to => @queue_name
150
186
  end
151
187
 
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)
188
+ result = wait_for_result(message)
158
189
  self.stop
159
190
  self.join unless self.em_running?
160
- return result
191
+
192
+ if result.size > 1
193
+ raise result[1]
194
+ end
195
+ return result.first
161
196
  end
162
197
 
163
198
  end
data/lib/rjr/common.rb CHANGED
@@ -12,8 +12,10 @@ class Logger
12
12
  private
13
13
  def self._instantiate_logger
14
14
  unless defined? @@logger
15
+ #STDOUT.sync = true
15
16
  @@logger = ::Logger.new(STDOUT)
16
17
  @@logger.level = ::Logger::FATAL
18
+ @@logger_mutex = Mutex.new
17
19
  end
18
20
  end
19
21
 
@@ -21,7 +23,15 @@ class Logger
21
23
 
22
24
  def self.method_missing(method_id, *args)
23
25
  _instantiate_logger
24
- @@logger.send(method_id, args)
26
+ @@logger_mutex.synchronize {
27
+ if args.first.is_a?(Array)
28
+ args.first.each{ |a|
29
+ @@logger.send(method_id, a)
30
+ }
31
+ else
32
+ @@logger.send(method_id, args)
33
+ end
34
+ }
25
35
  end
26
36
 
27
37
  def self.logger
@@ -37,6 +47,7 @@ end
37
47
 
38
48
  end # module RJR
39
49
 
50
+ if RUBY_VERSION < "1.9"
40
51
  # http://blog.jayfields.com/2006/09/ruby-instanceexec-aka-instanceeval.html
41
52
  class Object
42
53
  module InstanceExecHelper; end
@@ -58,3 +69,4 @@ class Object
58
69
  ret
59
70
  end
60
71
  end
72
+ end
@@ -24,7 +24,10 @@ class Request
24
24
  @method = args[:method]
25
25
  @method_args = args[:method_args]
26
26
  @headers = args[:headers]
27
+ @client_ip = args[:client_ip]
28
+ @client_port = args[:client_port]
27
29
  @rjr_callback = args[:rjr_callback]
30
+ @rjr_node = args[:rjr_node]
28
31
  @rjr_node_id = args[:rjr_node_id]
29
32
  @rjr_node_type = args[:rjr_node_type]
30
33
  @handler = args[:handler]
@@ -113,9 +116,7 @@ class Handler
113
116
  return Result.new(:result => retval)
114
117
 
115
118
  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
+ RJR::Logger.warn ["Exception Raised in #{method_name} handler #{e}"] + e.backtrace
119
120
 
120
121
  return Result.new(:error_code => -32000,
121
122
  :error_msg => e.to_s,
@@ -132,10 +133,13 @@ class Dispatcher
132
133
  end
133
134
 
134
135
  # register a handler to the specified method
135
- def self.add_handler(method_name, args = {}, &handler)
136
+ def self.add_handler(method_names, args = {}, &handler)
137
+ method_names = Array(method_names) unless method_names.is_a?(Array)
136
138
  @@handlers ||= {}
137
- @@handlers[method_name] = Handler.new args.merge(:method => method_name,
138
- :handler => handler)
139
+ method_names.each { |method_name|
140
+ @@handlers[method_name] = Handler.new args.merge(:method => method_name,
141
+ :handler => handler)
142
+ }
139
143
  end
140
144
 
141
145
  # Helper to handle request messages
@@ -38,6 +38,13 @@ class LocalNode < RJR::Node
38
38
  @node_type = RJR_NODE_TYPE
39
39
  end
40
40
 
41
+ # register connection event handler,
42
+ # until we support manual disconnections of the local node, we don't
43
+ # have to do anything here
44
+ def on(event, &handler)
45
+ # TODO raise error (for the time being)?
46
+ end
47
+
41
48
  # Instruct Node to start listening for and dispatching rpc requests
42
49
  def listen
43
50
  em_run do
@@ -50,15 +57,29 @@ class LocalNode < RJR::Node
50
57
  message = RequestMessage.new :method => rpc_method,
51
58
  :args => args,
52
59
  :headers => @message_headers
53
- result = Dispatcher.dispatch_request(rpc_method,
54
- :method_args => args,
60
+
61
+ # we serialize / unserialze messages to ensure local node complies
62
+ # to same json-rpc restrictions as other nodes
63
+ message = RequestMessage.new :message => message.to_s,
64
+ :headers => @message_headers
65
+
66
+ result = Dispatcher.dispatch_request(message.jr_method,
67
+ :method_args => message.jr_args,
55
68
  :headers => @message_headers,
69
+ :rjr_node => self,
56
70
  :rjr_node_id => @node_id,
57
71
  :rjr_node_type => @node_type,
58
72
  :rjr_callback =>
59
73
  LocalNodeCallback.new(:node => self,
60
74
  :headers => @message_headers))
61
- return Dispatcher.handle_response(result)
75
+ response = ResponseMessage.new(:id => message.msg_id,
76
+ :result => result,
77
+ :headers => @message_headers)
78
+
79
+ # same comment on serialization/unserialization as above
80
+ response = ResponseMessage.new(:message => response.to_s,
81
+ :headers => @message_headers)
82
+ return Dispatcher.handle_response(response.result)
62
83
  end
63
84
 
64
85
  end
data/lib/rjr/message.rb CHANGED
@@ -40,7 +40,6 @@ class RequestMessage
40
40
 
41
41
  rescue Exception => e
42
42
  #puts "Exception Parsing Request #{e}"
43
- # TODO
44
43
  raise e
45
44
  end
46
45
 
@@ -55,6 +54,7 @@ class RequestMessage
55
54
 
56
55
  def self.is_request_message?(message)
57
56
  begin
57
+ # TODO log error
58
58
  JSON.parse(message).has_key?('method')
59
59
  rescue Exception => e
60
60
  false
@@ -95,7 +95,7 @@ class ResponseMessage
95
95
  elsif response.has_key?('error')
96
96
  @result.error_code = response['error']['code']
97
97
  @result.error_msg = response['error']['message']
98
- @result.error_class = response['error']['class'] # TODO safely constantize this
98
+ @result.error_class = response['error']['class'] # TODO safely constantize this ?
99
99
 
100
100
  end
101
101
 
@@ -117,8 +117,10 @@ class ResponseMessage
117
117
 
118
118
  def self.is_response_message?(message)
119
119
  begin
120
- JSON.parse(message).has_key?('result')
120
+ json = JSON.parse(message)
121
+ json.has_key?('result') || json.has_key?('error')
121
122
  rescue Exception => e
123
+ # TODO log error
122
124
  false
123
125
  end
124
126
  end
data/lib/rjr/node.rb CHANGED
@@ -25,10 +25,6 @@ class Node
25
25
 
26
26
  @message_headers = {}
27
27
  @message_headers.merge!(args[:headers]) if args.has_key?(:headers)
28
-
29
- # threads pool to handle incoming requests
30
- # FIXME make the # of threads and timeout configurable)
31
- @thread_pool = ThreadPool.new(10, :timeout => 5)
32
28
  end
33
29
 
34
30
  # run job in event machine
@@ -38,6 +34,12 @@ class Node
38
34
 
39
35
  @@em_thread ||= nil
40
36
 
37
+ unless !@thread_pool.nil? && @thread_pool.running?
38
+ # threads pool to handle incoming requests
39
+ # FIXME make the # of threads and timeout configurable)
40
+ @thread_pool = ThreadPool.new(10, :timeout => 5)
41
+ end
42
+
41
43
  if @@em_thread.nil?
42
44
  @@em_thread =
43
45
  Thread.new{
@@ -54,7 +56,7 @@ class Node
54
56
  end
55
57
 
56
58
  def em_running?
57
- EventMachine.reactor_running?
59
+ @@em_jobs > 0 && EventMachine.reactor_running?
58
60
  end
59
61
 
60
62
  def join
@@ -67,7 +69,7 @@ class Node
67
69
  def stop
68
70
  @@em_jobs -= 1
69
71
  if @@em_jobs == 0
70
- EventMachine.stop
72
+ EventMachine.stop_event_loop
71
73
  @thread_pool.stop
72
74
  end
73
75
  end
@@ -77,7 +77,7 @@ class ThreadPool
77
77
  res
78
78
  end
79
79
 
80
- # TODO should not invoke after stop is called
80
+ # should not invoke after stop is called
81
81
  def check_timeout(timeout)
82
82
  @timeout_lock.synchronize {
83
83
  if !@time_started.nil? && Time.now - @time_started > timeout
data/lib/rjr/web_node.rb CHANGED
@@ -40,11 +40,15 @@ class WebRequestHandler < EventMachine::Connection
40
40
  msg = nil
41
41
  result = nil
42
42
  begin
43
+ client_port, client_ip = Socket.unpack_sockaddr_in(get_peername)
43
44
  msg = RequestMessage.new(:message => message, :headers => @web_node.message_headers)
44
45
  headers = @web_node.message_headers.merge(msg.headers)
45
46
  result = Dispatcher.dispatch_request(msg.jr_method,
46
47
  :method_args => msg.jr_args,
47
48
  :headers => headers,
49
+ :client_ip => client_ip,
50
+ :client_port => client_port,
51
+ :rjr_node => @web_node,
48
52
  :rjr_node_id => @web_node.node_id,
49
53
  :rjr_node_type => RJR_NODE_TYPE,
50
54
  :rjr_callback => WebNodeCallback.new())
@@ -65,7 +69,6 @@ class WebRequestHandler < EventMachine::Connection
65
69
 
66
70
  def process_http_request
67
71
  # TODO support http protocols other than POST
68
- # TODO should delete handler threads as they complete & should handle timeout
69
72
  msg = @http_post_content.nil? ? '' : @http_post_content
70
73
  #@thread_pool << ThreadPoolJob.new { handle_request(msg) }
71
74
  handle_request(msg)
@@ -88,6 +91,12 @@ class WebNode < RJR::Node
88
91
  def init_node
89
92
  end
90
93
 
94
+ # register connection event handler,
95
+ # since web node connections aren't persistant, we don't do anything here
96
+ def on(event, &handler)
97
+ # TODO raise error?
98
+ end
99
+
91
100
  # Instruct Node to start listening for and dispatching rpc requests
92
101
  def listen
93
102
  em_run do
data/lib/rjr/ws_node.rb CHANGED
@@ -20,10 +20,6 @@ class WSNodeCallback
20
20
  def initialize(args = {})
21
21
  @socket = args[:socket]
22
22
  @message_headers = args[:headers]
23
-
24
- # FIXME onclose, invalidate this callback / terminate outstanding handlers
25
- #@socket.onclose {}
26
- #@socket.onerror { |error|}
27
23
  end
28
24
 
29
25
  def invoke(callback_method, *data)
@@ -40,11 +36,15 @@ class WSNode < RJR::Node
40
36
 
41
37
  private
42
38
  def handle_request(socket, message)
39
+ client_port, client_ip = Socket.unpack_sockaddr_in(socket.get_peername)
43
40
  msg = RequestMessage.new(:message => message, :headers => @message_headers)
44
41
  headers = @message_headers.merge(msg.headers)
45
42
  result = Dispatcher.dispatch_request(msg.jr_method,
46
43
  :method_args => msg.jr_args,
47
44
  :headers => headers,
45
+ :client_ip => client_ip,
46
+ :client_port => client_port,
47
+ :rjr_node => self,
48
48
  :rjr_node_id => @node_id,
49
49
  :rjr_node_type => RJR_NODE_TYPE,
50
50
  :rjr_callback =>
@@ -60,6 +60,15 @@ class WSNode < RJR::Node
60
60
  super(args)
61
61
  @host = args[:host]
62
62
  @port = args[:port]
63
+
64
+ @connection_event_handlers = {:closed => [], :error => []}
65
+ end
66
+
67
+ # register connection event handler
68
+ def on(event, &handler)
69
+ if @connection_event_handlers.keys.include?(event)
70
+ @connection_event_handlers[event] << handler
71
+ end
63
72
  end
64
73
 
65
74
  # Initialize the ws subsystem
@@ -71,11 +80,18 @@ class WSNode < RJR::Node
71
80
  em_run do
72
81
  init_node
73
82
  EventMachine::WebSocket.start(:host => @host, :port => @port) do |ws|
74
- ws.onopen { }
75
- ws.onclose { }
76
- ws.onerror {|e|}
83
+ ws.onopen {}
84
+ ws.onclose {
85
+ @connection_event_handlers[:closed].each { |h|
86
+ h.call self
87
+ }
88
+ }
89
+ ws.onerror {|e|
90
+ @connection_event_handlers[:error].each { |h|
91
+ h.call self
92
+ }
93
+ }
77
94
  ws.onmessage { |msg|
78
- # TODO should delete handler threads as they complete & should handle timeout
79
95
  @thread_pool << ThreadPoolJob.new { handle_request(ws, msg) }
80
96
  }
81
97
  end
data/lib/rjr.rb CHANGED
@@ -3,18 +3,15 @@
3
3
  # Copyright (C) 2010 Mohammed Morsi <movitto@yahoo.com>
4
4
  # Licensed under the AGPLv3+ http://www.gnu.org/licenses/agpl.txt
5
5
 
6
- lib = File.dirname(__FILE__)
7
- $: << lib + '/rjr/'
8
-
9
- require lib + '/rjr/common'
10
- require lib + '/rjr/errors'
11
- require lib + '/rjr/thread_pool'
12
- require lib + '/rjr/semaphore'
13
- require lib + '/rjr/node'
14
- require lib + '/rjr/dispatcher'
15
- require lib + '/rjr/message'
16
- require lib + '/rjr/local_node'
17
- require lib + '/rjr/amqp_node'
18
- require lib + '/rjr/ws_node'
19
- require lib + '/rjr/web_node'
20
- require lib + '/rjr/multi_node'
6
+ require 'rjr/common'
7
+ require 'rjr/errors'
8
+ require 'rjr/thread_pool'
9
+ require 'rjr/semaphore'
10
+ require 'rjr/node'
11
+ require 'rjr/dispatcher'
12
+ require 'rjr/message'
13
+ require 'rjr/local_node'
14
+ require 'rjr/amqp_node'
15
+ require 'rjr/ws_node'
16
+ require 'rjr/web_node'
17
+ require 'rjr/multi_node'
@@ -0,0 +1,31 @@
1
+ require 'rjr/amqp_node'
2
+ require 'rjr/dispatcher'
3
+
4
+ describe RJR::AMQPNode do
5
+ it "should invoke and satisfy amqp requests" do
6
+ server = RJR::AMQPNode.new :node_id => 'amqp', :broker => 'localhost'
7
+ client = RJR::AMQPNode.new :node_id => 'client', :broker => 'localhost'
8
+
9
+ foozbar_invoked = false
10
+ RJR::Dispatcher.init_handlers
11
+ RJR::Dispatcher.add_handler('foozbar') { |param|
12
+ @client_ip.should == nil
13
+ @client_port.should == nil
14
+ @rjr_node.should == server
15
+ @rjr_node_id.should == 'amqp'
16
+ @rjr_node_type.should == :amqp
17
+ param.should == 'myparam'
18
+ foozbar_invoked = true
19
+ 'retval'
20
+ }
21
+
22
+ server.listen
23
+ res = client.invoke_request 'amqp-queue', 'foozbar', 'myparam'
24
+ server.halt
25
+ server.join
26
+ res.should == 'retval'
27
+ foozbar_invoked.should == true
28
+ end
29
+
30
+ # TODO ensure closed / error event handlers are invoked
31
+ end
@@ -0,0 +1,144 @@
1
+ require 'rjr/dispatcher'
2
+
3
+ describe RJR::Request do
4
+ it "invokes registered handler in request context" do
5
+ invoked = false
6
+ rjr_callback = Object.new
7
+ request = RJR::Request.new :method => 'foobar',
8
+ :method_args => ['a', 123],
9
+ :headers => {'qqq' => 'www'},
10
+ :rjr_callback => rjr_callback,
11
+ :rjr_node_id => 'test',
12
+ :rjr_node_type => 'test_type',
13
+ :handler => lambda { |p1, p2|
14
+ invoked = true
15
+ @method.should == 'foobar'
16
+ p1.should == 'a'
17
+ p2.should == 123
18
+ @headers['qqq'].should == 'www'
19
+ @rjr_callback.should == rjr_callback
20
+ @rjr_node_id.should == 'test'
21
+ @rjr_node_type.should == 'test_type'
22
+ }
23
+ request.handle
24
+ invoked.should == true
25
+ end
26
+ end
27
+
28
+ describe RJR::Result do
29
+ it "should handle successful results" do
30
+ result = RJR::Result.new :result => 'foobar'
31
+ result.success.should == true
32
+ result.failed.should == false
33
+ result.result.should == 'foobar'
34
+ result.error_code.should == nil
35
+ result.error_msg.should == nil
36
+ result.error_class.should == nil
37
+ end
38
+
39
+ it "should handle errors" do
40
+ result = RJR::Result.new :error_code => 123, :error_msg => 'abc', :error_class => ArgumentError
41
+ result.success.should == false
42
+ result.failed.should == true
43
+ result.result.should == nil
44
+ result.error_code.should == 123
45
+ result.error_msg.should == 'abc'
46
+ result.error_class.should == ArgumentError
47
+ end
48
+ end
49
+
50
+
51
+ describe RJR::Handler do
52
+ it "should return method not found result if method name is not specified" do
53
+ handler = RJR::Handler.new :method => nil
54
+ result = handler.handle
55
+ result.should == RJR::Result.method_not_found(nil)
56
+ end
57
+
58
+ it "should invoke registered handler for request" do
59
+ invoked = false
60
+ handler = RJR::Handler.new :method => 'foobar',
61
+ :handler => lambda {
62
+ invoked = true
63
+ }
64
+ handler.handle({:method_args => [] })
65
+ invoked.should == true
66
+ end
67
+
68
+ it "should return handler's return value in successful result" do
69
+ retval = Object.new
70
+ handler = RJR::Handler.new :method => 'foobar',
71
+ :handler => lambda {
72
+ retval
73
+ }
74
+ res = handler.handle({:method_args => [] })
75
+ res.success.should == true
76
+ res.result.should == retval
77
+ end
78
+
79
+ it "should catch handler errors and return error result" do
80
+ handler = RJR::Handler.new :method => 'foobar',
81
+ :method_args => [],
82
+ :handler => lambda {
83
+ raise ArgumentError, "uh oh!"
84
+ }
85
+ res = handler.handle({:method_args => [] })
86
+ res.failed.should == true
87
+ res.error_code.should == -32000
88
+ res.error_msg.should == "uh oh!"
89
+ res.error_class.should == ArgumentError
90
+ end
91
+ end
92
+
93
+ describe RJR::Dispatcher do
94
+ it "should dispatch request to registered handler" do
95
+ invoked_foobar = false
96
+ invoked_barfoo = false
97
+ RJR::Dispatcher.init_handlers
98
+ RJR::Dispatcher.add_handler('foobar') { |param1, param2|
99
+ invoked_foobar = true
100
+ param1.should == "param1"
101
+ param2.should == "param2"
102
+ "retval"
103
+ }
104
+ RJR::Dispatcher.add_handler('barfoo') { |param1, param2|
105
+ invoked_barfoo = true
106
+ }
107
+ res = RJR::Dispatcher.dispatch_request('foobar', :method_args => ['param1', 'param2'])
108
+ res.success.should == true
109
+ res.result.should == "retval"
110
+ invoked_foobar.should == true
111
+ invoked_barfoo.should == false
112
+ end
113
+
114
+ it "should allow a single handler to be subscribed to multiple methods" do
115
+ invoked_handler = 0
116
+ RJR::Dispatcher.init_handlers
117
+ RJR::Dispatcher.add_handler(['foobar', 'barfoo']) { |param1, param2|
118
+ invoked_handler += 1
119
+ }
120
+ RJR::Dispatcher.dispatch_request('foobar', :method_args => ['param1', 'param2'])
121
+ RJR::Dispatcher.dispatch_request('barfoo', :method_args => ['param1', 'param2'])
122
+ invoked_handler.should == 2
123
+ end
124
+
125
+ it "should return method not found result if handler for specified message is missing" do
126
+ RJR::Dispatcher.init_handlers
127
+ res = RJR::Dispatcher.dispatch_request('foobar')
128
+ res.should == RJR::Result.method_not_found('foobar')
129
+ end
130
+
131
+ it "should handle success response" do
132
+ res = RJR::Result.new :result => 'woot'
133
+ processed = RJR::Dispatcher.handle_response(res)
134
+ processed.should == "woot"
135
+ end
136
+
137
+ it "should handle error response" do
138
+ lambda{
139
+ res = RJR::Result.new :error_code => 123, :error_msg => "bah", :error_class => ArgumentError
140
+ RJR::Dispatcher.handle_response(res)
141
+ }.should raise_error(Exception, "bah")
142
+ #}.should raise_error(ArgumentError, "bah")
143
+ end
144
+ end
@@ -0,0 +1,43 @@
1
+ require 'rjr/local_node'
2
+ require 'rjr/dispatcher'
3
+
4
+ describe RJR::LocalNode do
5
+ it "should invoke requests against local handler" do
6
+ node = RJR::LocalNode.new :node_id => 'aaa'
7
+ foobar_invoked = false
8
+ RJR::Dispatcher.init_handlers
9
+ RJR::Dispatcher.add_handler('foobar') { |param|
10
+ @rjr_node.should == node
11
+ @rjr_node_id.should == 'aaa'
12
+ @rjr_node_type.should == :local
13
+ param.should == 'myparam'
14
+ foobar_invoked = true
15
+ 'retval'
16
+ }
17
+
18
+ res = node.invoke_request 'foobar', 'myparam'
19
+ foobar_invoked.should == true
20
+ res.should == 'retval'
21
+ end
22
+
23
+ it "should invoke callbacks against local handlers" do
24
+ foobar_invoked = false
25
+ callback_invoked = false
26
+ RJR::Dispatcher.init_handlers
27
+ RJR::Dispatcher.add_handler('foobar') {
28
+ foobar_invoked = true
29
+ @rjr_callback.invoke('callback', 'cp')
30
+ }
31
+ RJR::Dispatcher.add_handler('callback') { |*params|
32
+ params.length.should == 1
33
+ params[0].should == 'cp'
34
+ callback_invoked = true
35
+ }
36
+
37
+ node = RJR::LocalNode.new
38
+ node.invoke_request 'foobar', 'myparam'
39
+ end
40
+
41
+ # TODO make sure object attributes not serialized to json
42
+ # are not available on remote end of local node invocation/response
43
+ end
@@ -0,0 +1,145 @@
1
+ require 'rjr/dispatcher'
2
+ require 'rjr/message'
3
+
4
+ describe RJR::RequestMessage do
5
+ it "should accept request parameters" do
6
+ msg = RJR::RequestMessage.new :method => 'test',
7
+ :args => ['a', 1],
8
+ :headers => {:h => 2}
9
+ msg.jr_method.should == "test"
10
+ msg.jr_args.should =~ ['a', 1]
11
+ msg.headers.should have_key(:h)
12
+ msg.headers[:h].should == 2
13
+ msg.msg_id.should =~ /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/
14
+ end
15
+
16
+ it "should produce valid json" do
17
+ msg = RJR::RequestMessage.new :method => 'test',
18
+ :args => ['a', 1],
19
+ :headers => {:h => 2}
20
+
21
+ msg_string = msg.to_s
22
+ msg_string.should include('"h":2')
23
+ msg_string.should include('"method":"test"')
24
+ msg_string.should include('"params":["a",1]')
25
+ msg_string.should include('"jsonrpc":"2.0"')
26
+ msg_string.should include('"id":"'+msg.msg_id+'"')
27
+ end
28
+
29
+ it "should parse request message string" do
30
+ msg_uuid = RJR::RequestMessage.gen_uuid
31
+ msg_string = '{"jsonrpc": "2.0", ' +
32
+ '"id": "' + msg_uuid + '", ' +
33
+ '"method": "test", "params": ["a", 1]}'
34
+ msg = RJR::RequestMessage.new :message => msg_string
35
+ msg.json_message.should == msg_string
36
+ msg.jr_method.should == 'test'
37
+ msg.jr_args.should =~ ['a', 1]
38
+ msg.msg_id.should == msg_uuid
39
+ end
40
+
41
+ it "should extract optional headers from message string" do
42
+ msg_uuid = RJR::RequestMessage.gen_uuid
43
+ msg_string = '{"jsonrpc": "2.0", ' +
44
+ '"id": "' + msg_uuid + '", ' +
45
+ '"method": "test", "params": ["a", 1], ' +
46
+ '"h": 2}'
47
+ msg = RJR::RequestMessage.new :message => msg_string, :headers => {'f' => 'g'}
48
+ msg.json_message.should == msg_string
49
+ msg.headers.should have_key 'h'
50
+ msg.headers.should have_key 'f'
51
+ msg.headers['h'].should == 2
52
+ msg.headers['f'].should == 'g'
53
+ end
54
+
55
+ it "should fail if parsing invalid message string" do
56
+ lambda {
57
+ msg = RJR::RequestMessage.new :message => 'foobar'
58
+ }.should raise_error JSON::ParserError
59
+ end
60
+ end
61
+
62
+ describe RJR::ResponseMessage do
63
+ it "should accept response parameters" do
64
+ msg_id = RJR::RequestMessage.gen_uuid
65
+ msg = RJR::ResponseMessage.new :id => msg_id,
66
+ :result => RJR::Result.new(:result => 'success'),
67
+ :headers => {:h => 2}
68
+ msg.msg_id.should == msg_id
69
+ msg.result.result == 'success'
70
+ msg.headers.should have_key(:h)
71
+ msg.headers[:h].should == 2
72
+ end
73
+
74
+ it "should produce valid result response json" do
75
+ msg_id = RJR::RequestMessage.gen_uuid
76
+ msg = RJR::ResponseMessage.new :id => msg_id,
77
+ :result => RJR::Result.new(:result => 'success'),
78
+ :headers => {:h => 2}
79
+ msg_string = msg.to_s
80
+ msg_string.should include('"id":"'+msg_id+'"')
81
+ msg_string.should include('"result":"success"')
82
+ msg_string.should include('"h":2')
83
+ msg_string.should include('"jsonrpc":"2.0"')
84
+ end
85
+
86
+ it "should produce valid error response json" do
87
+ msg_id = RJR::RequestMessage.gen_uuid
88
+ msg = RJR::ResponseMessage.new :id => msg_id,
89
+ :result => RJR::Result.new(:error_code => 404,
90
+ :error_msg => 'not found',
91
+ :error_class => ArgumentError),
92
+ :headers => {:h => 2}
93
+ msg_string = msg.to_s
94
+ msg_string.should include('"id":"'+msg_id+'"')
95
+ msg_string.should include('"h":2')
96
+ msg_string.should include('"jsonrpc":"2.0"')
97
+ msg_string.should include('"error":{')
98
+ msg_string.should include('"code":404')
99
+ msg_string.should include('"message":"not found"')
100
+ end
101
+
102
+
103
+ it "should parse result response message string" do
104
+ msg_id = RJR::RequestMessage.gen_uuid
105
+ msg_string = '{"id":"' + msg_id + '", ' +
106
+ '"result":"success","jsonrpc":"2.0"}'
107
+ msg = RJR::ResponseMessage.new :message => msg_string
108
+ msg.json_message.should == msg_string
109
+ msg.msg_id.should == msg_id
110
+ msg.result.success.should == true
111
+ msg.result.failed.should == false
112
+ msg.result.result.should == "success"
113
+ end
114
+
115
+ it "should parse error response message string" do
116
+
117
+ msg_id = RJR::RequestMessage.gen_uuid
118
+ msg_string = '{"id":"' + msg_id + '", ' +
119
+ '"error":{"code":404,"message":"not found","class":"ArgumentError"}, "jsonrpc":"2.0"}'
120
+ msg = RJR::ResponseMessage.new :message => msg_string
121
+ msg.json_message.should == msg_string
122
+ msg.msg_id.should == msg_id
123
+ msg.result.success.should == false
124
+ msg.result.failed.should == true
125
+ msg.result.error_code.should == 404
126
+ msg.result.error_msg.should == "not found"
127
+ msg.result.error_class.should == 'ArgumentError'
128
+ end
129
+
130
+ it "should extract optional headers from message string" do
131
+ msg_id = RJR::RequestMessage.gen_uuid
132
+ msg_string = '{"id":"' + msg_id + '", ' +
133
+ '"result":"success","h":2,"jsonrpc":"2.0"}'
134
+ msg = RJR::ResponseMessage.new :message => msg_string
135
+ msg.json_message.should == msg_string
136
+ msg.headers.should have_key 'h'
137
+ msg.headers['h'].should == 2
138
+ end
139
+
140
+ it "should fail if parsing invalid message string" do
141
+ lambda {
142
+ msg = RJR::ResponseMessage.new :message => 'foobar'
143
+ }.should raise_error JSON::ParserError
144
+ end
145
+ end
@@ -0,0 +1,44 @@
1
+ require 'rjr/multi_node'
2
+ require 'rjr/amqp_node'
3
+ require 'rjr/web_node'
4
+ require 'rjr/dispatcher'
5
+
6
+ describe RJR::AMQPNode do
7
+ it "should invoke and satisfy amqp requests" do
8
+ foolbar_invoked = false
9
+ barfoo_invoked = false
10
+ RJR::Dispatcher.init_handlers
11
+ RJR::Dispatcher.add_handler('foolbar') { |param|
12
+ @rjr_node_id.should == 'amqp'
13
+ @rjr_node_type.should == :amqp
14
+ param.should == 'myparam1'
15
+ foolbar_invoked = true
16
+ 'retval1'
17
+ }
18
+ RJR::Dispatcher.add_handler('barfoo') { |param|
19
+ @rjr_node_id.should == 'web'
20
+ @rjr_node_type.should == :web
21
+ param.should == 'myparam2'
22
+ barfoo_invoked = true
23
+ 'retval2'
24
+ }
25
+
26
+ amqp = RJR::AMQPNode.new :node_id => 'amqp', :broker => 'localhost'
27
+ web = RJR::WebNode.new :node_id => 'web', :host => 'localhost', :port => 9876
28
+ multi = RJR::MultiNode.new :node_id => 'multi', :nodes => [amqp, web]
29
+
30
+ multi.listen
31
+ amqp_client = RJR::AMQPNode.new :node_id => 'client', :broker => 'localhost'
32
+ res = amqp_client.invoke_request 'amqp-queue', 'foolbar', 'myparam1'
33
+ res.should == 'retval1'
34
+
35
+ web_client = RJR::WebNode.new
36
+ res = web_client.invoke_request 'http://localhost:9876', 'barfoo', 'myparam2'
37
+ res.should == 'retval2'
38
+
39
+ multi.halt
40
+ multi.join
41
+ foolbar_invoked.should == true
42
+ barfoo_invoked.should == true
43
+ end
44
+ end
@@ -0,0 +1,33 @@
1
+ require 'rjr/node'
2
+
3
+ describe RJR::Node do
4
+ it "should initialize properly from params" do
5
+ node = RJR::Node.new :node_id => 'foobar',
6
+ :headers => {:h => 123}
7
+ node.node_id.should == 'foobar'
8
+ node.message_headers[:h].should == 123
9
+ end
10
+
11
+ it "should start eventmachine and allow multiple blocks to be invoked in its context" do
12
+ block1_called = false
13
+ block2_called = false
14
+
15
+ node = RJR::Node.new :node_id => 'foobar',
16
+ :headers => {:h => 123}
17
+ node.em_run {
18
+ node.instance_variable_get(:@thread_pool).running?.should be_true
19
+
20
+ EventMachine.reactor_running?.should be_true
21
+ block1_called = true
22
+ node.em_run {
23
+ EventMachine.reactor_running?.should be_true
24
+ block2_called = true
25
+ node.halt
26
+ }
27
+ }
28
+ node.join
29
+
30
+ block1_called.should be_true
31
+ block2_called.should be_true
32
+ end
33
+ end
@@ -0,0 +1,31 @@
1
+ require 'rjr/web_node'
2
+ require 'rjr/dispatcher'
3
+
4
+ describe RJR::WebNode do
5
+ it "should invoke and satisfy http requests" do
6
+ server = RJR::WebNode.new :node_id => 'www', :host => 'localhost', :port => 9678
7
+ client = RJR::WebNode.new
8
+
9
+ foobar_invoked = false
10
+ RJR::Dispatcher.init_handlers
11
+ RJR::Dispatcher.add_handler('foobar') { |param|
12
+ @client_ip.should == "127.0.0.1"
13
+ #@client_port.should == 9678
14
+ @rjr_node.should == server
15
+ @rjr_node_id.should == 'www'
16
+ @rjr_node_type.should == :web
17
+ param.should == 'myparam'
18
+ foobar_invoked = true
19
+ 'retval'
20
+ }
21
+
22
+ server.listen
23
+
24
+ res = client.invoke_request 'http://localhost:9678', 'foobar', 'myparam'
25
+ res.should == 'retval'
26
+ server.halt
27
+
28
+ server.join
29
+ foobar_invoked.should == true
30
+ end
31
+ end
@@ -0,0 +1,32 @@
1
+ require 'rjr/ws_node'
2
+ require 'rjr/dispatcher'
3
+
4
+ describe RJR::WSNode do
5
+ it "should invoke and satisfy websocket requests" do
6
+ server = RJR::WSNode.new :node_id => 'ws', :host => 'localhost', :port => 9876
7
+ client = RJR::WSNode.new
8
+
9
+ foobar_invoked = false
10
+ RJR::Dispatcher.init_handlers
11
+ RJR::Dispatcher.add_handler('foobar') { |param|
12
+ @client_ip.should == "127.0.0.1"
13
+ #@client_port.should == 9678
14
+ @rjr_node.should == server
15
+ @rjr_node_id.should == 'ws'
16
+ @rjr_node_type.should == :websockets
17
+ param.should == 'myparam'
18
+ foobar_invoked = true
19
+ 'retval'
20
+ }
21
+
22
+ server.listen
23
+ sleep 1
24
+ res = client.invoke_request 'ws://localhost:9876', 'foobar', 'myparam'
25
+ res.should == 'retval'
26
+ server.halt
27
+ server.join
28
+ foobar_invoked.should == true
29
+ end
30
+
31
+ # TODO ensure closed / error event handlers are invoked
32
+ end
metadata CHANGED
@@ -1,47 +1,39 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: rjr
3
- version: !ruby/object:Gem::Version
4
- hash: 3
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.6.1
5
5
  prerelease:
6
- segments:
7
- - 0
8
- - 5
9
- - 4
10
- version: 0.5.4
11
6
  platform: ruby
12
- authors:
7
+ authors:
13
8
  - Mohammed Morsi
14
9
  autorequire:
15
10
  bindir: bin
16
11
  cert_chain: []
17
-
18
- date: 2012-04-25 00:00:00 Z
19
- dependencies:
20
- - !ruby/object:Gem::Dependency
12
+ date: 2012-04-25 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
21
15
  name: rspec
22
- prerelease: false
23
- requirement: &id001 !ruby/object:Gem::Requirement
16
+ requirement: !ruby/object:Gem::Requirement
24
17
  none: false
25
- requirements:
18
+ requirements:
26
19
  - - ~>
27
- - !ruby/object:Gem::Version
28
- hash: 27
29
- segments:
30
- - 1
31
- - 3
32
- - 0
20
+ - !ruby/object:Gem::Version
33
21
  version: 1.3.0
34
22
  type: :development
35
- version_requirements: *id001
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 1.3.0
36
30
  description: Ruby Json Rpc library
37
31
  email: mo@morsi.org
38
- executables:
32
+ executables:
39
33
  - rjr-server
40
34
  extensions: []
41
-
42
35
  extra_rdoc_files: []
43
-
44
- files:
36
+ files:
45
37
  - lib/rjr/semaphore.rb
46
38
  - lib/rjr/udp_node.rb
47
39
  - lib/rjr/errors.rb
@@ -58,46 +50,40 @@ files:
58
50
  - lib/rjr/dispatcher.rb
59
51
  - lib/rjr/amqp_node.rb
60
52
  - lib/rjr.rb
53
+ - specs/dispatcher_spec.rb
54
+ - specs/node_spec.rb
55
+ - specs/amqp_node_spec.rb
56
+ - specs/multi_node_spec.rb
57
+ - specs/local_node_spec.rb
58
+ - specs/web_node_spec.rb
59
+ - specs/ws_node_spec.rb
60
+ - specs/message_spec.rb
61
61
  - LICENSE
62
62
  - Rakefile
63
63
  - README.rdoc
64
64
  - bin/rjr-server
65
65
  homepage: http://github.com/movitto/rjr
66
66
  licenses: []
67
-
68
67
  post_install_message:
69
68
  rdoc_options: []
70
-
71
- require_paths:
69
+ require_paths:
72
70
  - lib
73
- required_ruby_version: !ruby/object:Gem::Requirement
71
+ required_ruby_version: !ruby/object:Gem::Requirement
74
72
  none: false
75
- requirements:
76
- - - ">="
77
- - !ruby/object:Gem::Version
78
- hash: 53
79
- segments:
80
- - 1
81
- - 8
82
- - 1
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
83
76
  version: 1.8.1
84
- required_rubygems_version: !ruby/object:Gem::Requirement
77
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
78
  none: false
86
- requirements:
87
- - - ">="
88
- - !ruby/object:Gem::Version
89
- hash: 29
90
- segments:
91
- - 1
92
- - 3
93
- - 3
79
+ requirements:
80
+ - - ! '>='
81
+ - !ruby/object:Gem::Version
94
82
  version: 1.3.3
95
83
  requirements: []
96
-
97
84
  rubyforge_project:
98
- rubygems_version: 1.7.2
85
+ rubygems_version: 1.8.24
99
86
  signing_key:
100
87
  specification_version: 3
101
- summary: JSON RPC server and client library over amqp, websockets
88
+ summary: JSON RPC server and client library over amqp, websockets, http, etc
102
89
  test_files: []
103
-