rjr 0.9.0 → 0.11.7

Sign up to get free protection for your applications and to get access to all the features.
data/lib/rjr/ws_node.rb CHANGED
@@ -5,11 +5,25 @@
5
5
  # Copyright (C) 2012 Mohammed Morsi <mo@morsi.org>
6
6
  # Licensed under the Apache License, Version 2.0
7
7
 
8
+ skip_module = false
9
+ begin
8
10
  require 'em-websocket'
9
- require 'rjr/web_socket'
11
+ require 'em-websocket-client'
12
+ rescue LoadError
13
+ skip_module = true
14
+ end
15
+
16
+ if skip_module
17
+ # TODO output: "ws dependencies could not be loaded, skipping ws node definition"
18
+ require 'rjr/missing_node'
19
+ RJR::WSNode = RJR::MissingNode
10
20
 
21
+ else
22
+ require 'socket'
11
23
  require 'rjr/node'
12
24
  require 'rjr/message'
25
+ require 'rjr/dispatcher'
26
+ require 'rjr/thread_pool2'
13
27
 
14
28
  module RJR
15
29
 
@@ -31,8 +45,7 @@ class WSNodeCallback
31
45
 
32
46
  # Implementation of {RJR::NodeCallback#invoke}
33
47
  def invoke(callback_method, *data)
34
- #msg = CallbackMessage.new(:data => data)
35
- msg = RequestMessage.new :method => callback_method, :args => data, :headers => @message_headers
48
+ msg = NotificationMessage.new :method => callback_method, :args => data, :headers => @message_headers
36
49
  raise RJR::Errors::ConnectionError.new("websocket closed") if @socket.state == :closed
37
50
  # TODO surround w/ begin/rescue block incase of other socket errors?
38
51
  @socket.send(msg.to_s)
@@ -62,17 +75,57 @@ end
62
75
  # puts client.invoke_request('ws://localhost:7777', 'hello', 'mo')
63
76
  #
64
77
  class WSNode < RJR::Node
65
- RJR_NODE_TYPE = :websockets
78
+ RJR_NODE_TYPE = :ws
66
79
 
67
80
  private
68
- # Initialize the ws subsystem
69
- def init_node
81
+
82
+ # Internal helper initialize new connection
83
+ def init_node(uri, &on_init)
84
+ connection = nil
85
+ @connections_lock.synchronize {
86
+ connection = @connections.find { |c|
87
+ c.url == uri
88
+ }
89
+ if connection.nil?
90
+ connection = EventMachine::WebSocketClient.connect(uri)
91
+ connection.callback do
92
+ on_init.call(connection)
93
+ end
94
+ @connections << connection
95
+ # TODO sleep until connected?
96
+ else
97
+ on_init.call(connection)
98
+ end
99
+ }
100
+ connection
101
+ end
102
+
103
+ # Internal helper handle messages
104
+ def handle_msg(endpoint, msg)
105
+ # TODO use messageutil incase of large messages?
106
+ if RequestMessage.is_request_message?(msg)
107
+ ThreadPool2Manager << ThreadPool2Job.new { handle_request(endpoint, msg, false) }
108
+
109
+ elsif NotificationMessage.is_notification_message?(msg)
110
+ ThreadPool2Manager << ThreadPool2Job.new { handle_request(endpoint, msg, true) }
111
+
112
+ elsif ResponseMessage.is_response_message?(msg)
113
+ handle_response(msg)
114
+
115
+ end
70
116
  end
71
117
 
72
118
  # Internal helper, handle request message received
73
- def handle_request(socket, message)
74
- client_port, client_ip = Socket.unpack_sockaddr_in(socket.get_peername)
75
- msg = RequestMessage.new(:message => message, :headers => @message_headers)
119
+ def handle_request(endpoint, message, notification=false)
120
+ # XXX hack to handle client disconnection (should grap port/ip immediately on connection and use that)
121
+ client_port,client_ip = nil,nil
122
+ begin
123
+ client_port, client_ip = Socket.unpack_sockaddr_in(endpoint.get_peername)
124
+ rescue Exception=>e
125
+ end
126
+
127
+ msg = notification ? NotificationMessage.new(:message => message, :headers => @message_headers) :
128
+ RequestMessage.new(:message => message, :headers => @message_headers)
76
129
  headers = @message_headers.merge(msg.headers)
77
130
  result = Dispatcher.dispatch_request(msg.jr_method,
78
131
  :method_args => msg.jr_args,
@@ -83,12 +136,53 @@ class WSNode < RJR::Node
83
136
  :rjr_node_id => @node_id,
84
137
  :rjr_node_type => RJR_NODE_TYPE,
85
138
  :rjr_callback =>
86
- WSNodeCallback.new(:socket => socket,
139
+ WSNodeCallback.new(:socket => endpoint,
87
140
  :headers => headers))
88
- response = ResponseMessage.new(:id => msg.msg_id, :result => result, :headers => headers)
89
- socket.send(response.to_s)
141
+ unless notification
142
+ response = ResponseMessage.new(:id => msg.msg_id, :result => result, :headers => headers)
143
+ endpoint.send(response.to_s)
144
+ end
145
+ end
146
+
147
+ # Internal helper, handle response message received
148
+ def handle_response(data)
149
+ msg = ResponseMessage.new(:message => data, :headers => @message_headers)
150
+ res = err = nil
151
+ begin
152
+ res = Dispatcher.handle_response(msg.result)
153
+ rescue Exception => e
154
+ err = e
155
+ end
156
+
157
+ @response_lock.synchronize {
158
+ result = [msg.msg_id, res]
159
+ result << err if !err.nil?
160
+ @responses << result
161
+ @response_cv.signal
162
+ }
163
+ end
164
+
165
+ # Internal helper, block until response matching message id is received
166
+ def wait_for_result(message)
167
+ res = nil
168
+ while res.nil?
169
+ @response_lock.synchronize{
170
+ # FIXME throw err if more than 1 match found
171
+ res = @responses.select { |response| message.msg_id == response.first }.first
172
+ if !res.nil?
173
+ @responses.delete(res)
174
+
175
+ else
176
+ @response_cv.signal
177
+ @response_cv.wait @response_lock
178
+
179
+ end
180
+ }
181
+ end
182
+ return res
90
183
  end
91
184
 
185
+
92
186
  public
93
187
  # WSNode initializer
94
188
  # @param [Hash] args the options to create the web socket node with
@@ -99,6 +193,13 @@ class WSNode < RJR::Node
99
193
  @host = args[:host]
100
194
  @port = args[:port]
101
195
 
196
+ @connections = []
197
+ @connections_lock = Mutex.new
198
+
199
+ @response_lock = Mutex.new
200
+ @response_cv = ConditionVariable.new
201
+ @responses = []
202
+
102
203
  @connection_event_handlers = {:closed => [], :error => []}
103
204
  end
104
205
 
@@ -117,22 +218,11 @@ class WSNode < RJR::Node
117
218
  # Implementation of {RJR::Node#listen}
118
219
  def listen
119
220
  em_run do
120
- init_node
121
221
  EventMachine::WebSocket.start(:host => @host, :port => @port) do |ws|
122
- ws.onopen {}
123
- ws.onclose {
124
- @connection_event_handlers[:closed].each { |h|
125
- h.call self
126
- }
127
- }
128
- ws.onerror {|e|
129
- @connection_event_handlers[:error].each { |h|
130
- h.call self
131
- }
132
- }
133
- ws.onmessage { |msg|
134
- @thread_pool << ThreadPoolJob.new { handle_request(ws, msg) }
135
- }
222
+ ws.onopen { }
223
+ ws.onclose { @connection_event_handlers[:closed].each { |h| h.call self } }
224
+ ws.onerror { |e| @connection_event_handlers[:error].each { |h| h.call self } }
225
+ ws.onmessage { |msg| handle_msg(ws, msg) }
136
226
  end
137
227
  end
138
228
  end
@@ -143,17 +233,55 @@ class WSNode < RJR::Node
143
233
  # @param [String] rpc_method json-rpc method to invoke on destination
144
234
  # @param [Array] args array of arguments to convert to json and invoke remote method wtih
145
235
  def invoke_request(uri, rpc_method, *args)
146
- init_node
147
236
  message = RequestMessage.new :method => rpc_method,
148
237
  :args => args,
149
238
  :headers => @message_headers
150
- socket = WebSocket.new(uri)
151
- socket.send(message.to_s)
152
- res = socket.receive()
153
- msg = ResponseMessage.new(:message => res, :headers => @message_headers)
154
- headers = @message_headers.merge(msg.headers)
155
- return Dispatcher.handle_response(msg.result)
239
+
240
+ em_run{
241
+ init_node(uri) do |c|
242
+ c.stream { |msg| handle_msg(c, msg) }
243
+
244
+ c.send_msg message.to_s
245
+ end
246
+ }
247
+
248
+ # TODO optional timeout for response ?
249
+ result = wait_for_result(message)
250
+
251
+ if result.size > 2
252
+ raise Exception, result[2]
253
+ end
254
+ return result[1]
255
+ end
256
+
257
+ # Instructs node to send rpc notification (immadiately returns / no response is generated)
258
+ #
259
+ # @param [String] uri location of node to send notification to, should be
260
+ # in format of ws://hostname:port
261
+ # @param [String] rpc_method json-rpc method to invoke on destination
262
+ # @param [Array] args array of arguments to convert to json and invoke remote method wtih
263
+ def send_notification(uri, rpc_method, *args)
264
+ # will block until message is published
265
+ published_l = Mutex.new
266
+ published_c = ConditionVariable.new
267
+
268
+ message = NotificationMessage.new :method => rpc_method,
269
+ :args => args,
270
+ :headers => @message_headers
271
+ em_run{
272
+ init_node(uri) do |c|
273
+ c.send_msg message.to_s
274
+
275
+ # XXX same bug w/ tcp node, due to nature of event machine
276
+ # we aren't guaranteed that message is actually written to socket
277
+ # here, process must be kept alive until data is sent or will be lost
278
+ published_l.synchronize { published_c.signal }
279
+ end
280
+ }
281
+ published_l.synchronize { published_c.wait published_l }
282
+ nil
156
283
  end
157
284
  end
158
285
 
159
286
  end # module RJR
287
+ end
data/lib/rjr.rb CHANGED
@@ -6,16 +6,11 @@
6
6
  # rjr - Ruby Json Rpc
7
7
  module RJR ; end
8
8
 
9
- require 'rjr/common'
10
- require 'rjr/errors'
11
- require 'rjr/thread_pool'
12
- require 'rjr/semaphore'
13
- require 'rjr/node'
14
- require 'rjr/dispatcher'
15
- require 'rjr/message'
16
- require 'rjr/local_node'
17
9
  require 'rjr/amqp_node'
18
- require 'rjr/ws_node'
19
- require 'rjr/web_node'
20
10
  require 'rjr/tcp_node'
11
+ require 'rjr/web_node'
12
+ require 'rjr/ws_node'
13
+ require 'rjr/local_node'
21
14
  require 'rjr/multi_node'
15
+
16
+ require 'rjr/util'
@@ -49,6 +49,10 @@ end
49
49
 
50
50
 
51
51
  describe RJR::Handler do
52
+ before(:each) do
53
+ RJR::DispatcherStat.reset
54
+ end
55
+
52
56
  it "should return method not found result if method name is not specified" do
53
57
  handler = RJR::Handler.new :method => nil
54
58
  result = handler.handle
@@ -65,6 +69,15 @@ describe RJR::Handler do
65
69
  invoked.should == true
66
70
  end
67
71
 
72
+ it "should create dispatcher stat when invoking handler" do
73
+ handler = RJR::Handler.new :method => 'foobar',
74
+ :handler => lambda { 42 }
75
+ handler.handle({:method_args => [] })
76
+ RJR::DispatcherStat.stats.size.should == 1
77
+ RJR::DispatcherStat.stats.first.request.method.should == 'foobar'
78
+ RJR::DispatcherStat.stats.first.result.result.should == 42
79
+ end
80
+
68
81
  it "should return handler's return value in successful result" do
69
82
  retval = Object.new
70
83
  handler = RJR::Handler.new :method => 'foobar',
@@ -90,6 +103,59 @@ describe RJR::Handler do
90
103
  end
91
104
  end
92
105
 
106
+ describe RJR::DispatcherStat do
107
+ before(:each) do
108
+ RJR::DispatcherStat.reset
109
+ end
110
+
111
+ it "should store request and result" do
112
+ req = RJR::Request.new
113
+ res = RJR::Result.new
114
+ stat = RJR::DispatcherStat.new req, res
115
+ (stat.request == req).should be_true
116
+ stat.result.should == res
117
+ end
118
+
119
+ it "should track global stats" do
120
+ req = RJR::Request.new
121
+ res = RJR::Result.new
122
+ stat = RJR::DispatcherStat.new req, res
123
+
124
+ RJR::DispatcherStat << stat
125
+ RJR::DispatcherStat.stats.should include(stat)
126
+ end
127
+
128
+ it "should be convertable to json" do
129
+ req = RJR::Request.new :method => 'foobar', :method_args => [:a, :b],
130
+ :headers => { :foo => :bar }, :rjr_node_type => :local,
131
+ :rjr_node_id => :loc1
132
+ res = RJR::Result.new :result => 42
133
+
134
+ stat = RJR::DispatcherStat.new req, res
135
+ j = stat.to_json()
136
+ j.should include('"json_class":"RJR::DispatcherStat"')
137
+ j.should include('"method":"foobar"')
138
+ j.should include('"method_args":["a","b"]')
139
+ j.should include('"headers":{"foo":"bar"}')
140
+ j.should include('"rjr_node_type":"local"')
141
+ j.should include('"rjr_node_id":"loc1"')
142
+ j.should include('"result":42')
143
+ end
144
+
145
+ it "should be convertable from json" do
146
+ j = '{"json_class":"RJR::DispatcherStat","data":{"request":{"method":"foobar","method_args":["a","b"],"headers":{"foo":"bar"},"rjr_node_type":"local","rjr_node_id":"loc1"},"result":{"result":42,"error_code":null,"error_msg":null,"error_class":null}}}'
147
+ s = JSON.parse(j)
148
+
149
+ s.class.should == RJR::DispatcherStat
150
+ s.request.method.should == 'foobar'
151
+ s.request.method_args.should == ['a', 'b']
152
+ s.request.headers.should == { 'foo' => 'bar' }
153
+ s.request.rjr_node_type.should == 'local'
154
+ s.request.rjr_node_id.should == 'loc1'
155
+ s.result.result.should == 42
156
+ end
157
+ end
158
+
93
159
  describe RJR::Dispatcher do
94
160
  it "should dispatch request to registered handler" do
95
161
  invoked_foobar = false
@@ -111,6 +177,21 @@ describe RJR::Dispatcher do
111
177
  invoked_barfoo.should == false
112
178
  end
113
179
 
180
+ it "should allow user to determine registered handlers" do
181
+ foobar = lambda {}
182
+ barfoo = lambda {}
183
+ RJR::Dispatcher.add_handler('foobar', &foobar)
184
+ RJR::Dispatcher.add_handler('barfoo', &barfoo)
185
+
186
+ RJR::Dispatcher.has_handler_for?('foobar').should be_true
187
+ RJR::Dispatcher.has_handler_for?('barfoo').should be_true
188
+ RJR::Dispatcher.has_handler_for?('money').should be_false
189
+
190
+ RJR::Dispatcher.handler_for('foobar').handler_proc.should == foobar
191
+ RJR::Dispatcher.handler_for('barfoo').handler_proc.should == barfoo
192
+ RJR::Dispatcher.handler_for('money').should be_nil
193
+ end
194
+
114
195
  it "should allow a single handler to be subscribed to multiple methods" do
115
196
  invoked_handler = 0
116
197
  RJR::Dispatcher.init_handlers
@@ -0,0 +1,85 @@
1
+ require 'rjr/dispatcher'
2
+ require 'rjr/em_adapter'
3
+
4
+ describe EMManager do
5
+ it "should start and halt the reactor thread" do
6
+ manager = EMManager.new
7
+ manager.start
8
+ EventMachine.reactor_running?.should be_true
9
+ manager.running?.should be_true
10
+ manager.instance_variable_get(:@reactor_thread).should_not be_nil
11
+ rt = manager.instance_variable_get(:@reactor_thread)
12
+ ['sleep', 'run'].should include(manager.instance_variable_get(:@reactor_thread).status)
13
+
14
+ manager.start
15
+ rt2 = manager.instance_variable_get(:@reactor_thread)
16
+ rt.should == rt2
17
+
18
+ manager.halt
19
+ manager.join
20
+ EventMachine.reactor_running?.should be_false
21
+ manager.running?.should be_false
22
+ manager.instance_variable_get(:@reactor_thread).should be_nil
23
+ end
24
+
25
+ it "should allow the user to schedule jobs" do
26
+ manager = EMManager.new
27
+ manager.start
28
+ manager.running?.should be_true
29
+ block_ran = false
30
+ manager.schedule {
31
+ block_ran = true
32
+ }
33
+ sleep 0.5
34
+ block_ran.should == true
35
+
36
+ manager.halt
37
+ manager.join
38
+ manager.running?.should be_false
39
+ end
40
+
41
+ it "should allow the user to keep the reactor alive until forcibly stopped" do
42
+ manager = EMManager.new
43
+ manager.start
44
+ manager.running?.should be_true
45
+ manager.schedule { "foo" }
46
+
47
+ manager.running?.should be_true
48
+
49
+ # forcibly stop the reactor
50
+ manager.halt
51
+ manager.join
52
+ manager.running?.should be_false
53
+ end
54
+
55
+ it "should allow the user to schedule at job after a specified interval" do
56
+ manager = EMManager.new
57
+ manager.start
58
+ manager.running?.should be_true
59
+ block_called = false
60
+ manager.add_timer(1) { block_called = true }
61
+ sleep 0.5
62
+ block_called.should == false
63
+ sleep 1
64
+ block_called.should == true
65
+ manager.halt
66
+ manager.join
67
+ end
68
+
69
+ it "should allow the user to schedule at job repeatidly with a specified interval" do
70
+ manager = EMManager.new
71
+ manager.start
72
+ manager.running?.should be_true
73
+ times_block_called = 0
74
+ manager.add_periodic_timer(1) { times_block_called += 1 }
75
+ sleep 0.6
76
+ times_block_called.should == 0
77
+ sleep 0.6
78
+ times_block_called.should == 1
79
+ sleep 1.2
80
+ times_block_called.should == 2
81
+
82
+ manager.halt
83
+ manager.join
84
+ end
85
+ end
@@ -0,0 +1,60 @@
1
+ require 'rjr/inspect'
2
+
3
+ describe 'rjr/inspect.rb' do
4
+ describe "select_stats" do
5
+ before(:each) do
6
+ RJR::DispatcherStat.reset
7
+
8
+ req1 = RJR::Request.new :rjr_node_type => :local, :method => 'foobar'
9
+ req2 = RJR::Request.new :rjr_node_type => :tcp, :method => 'barfoo'
10
+ req3 = RJR::Request.new :rjr_node_type => :amqp, :method => 'foobar'
11
+ res1 = RJR::Result.new :result => true
12
+ res2 = RJR::Result.new :error_code => 123
13
+ res3 = RJR::Result.new :error_code => 123
14
+
15
+ @stat1 = RJR::DispatcherStat.new req1, res1
16
+ @stat2 = RJR::DispatcherStat.new req2, res2
17
+ @stat3 = RJR::DispatcherStat.new req3, res3
18
+ RJR::DispatcherStat << @stat1 << @stat2 << @stat3
19
+ end
20
+
21
+ it "should get all dispatcherstats" do
22
+ stats = select_stats
23
+ stats.size.should == 3
24
+ stats.first.should == @stat1
25
+ stats[1].should == @stat2
26
+ stats.last.should == @stat3
27
+ end
28
+
29
+ it "should get all dispatcherstats for a node" do
30
+ stats = select_stats 'on_node', 'local'
31
+ stats.size.should == 1
32
+ stats.first.should == @stat1
33
+ end
34
+
35
+ it "should get all dispatcherstats for a method" do
36
+ stats = select_stats 'for_method', 'foobar'
37
+ stats.size.should == 2
38
+ stats.first.should == @stat1
39
+ stats.last.should == @stat3
40
+ end
41
+
42
+ it "should get all successfull/failed dispatcherstats" do
43
+ stats = select_stats 'successful'
44
+ stats.size.should == 1
45
+ stats.first.should == @stat1
46
+
47
+ stats = select_stats 'failed'
48
+ stats.size.should == 2
49
+ stats.first.should == @stat2
50
+ stats.last.should == @stat3
51
+ end
52
+
53
+ it "should get dispatcher stats meeting multiple criteria" do
54
+ stats = select_stats 'for_method', 'foobar', 'successful'
55
+ stats.size.should == 1
56
+ stats.first.should == @stat1
57
+ end
58
+ end
59
+
60
+ end
@@ -143,3 +143,61 @@ describe RJR::ResponseMessage do
143
143
  }.should raise_error JSON::ParserError
144
144
  end
145
145
  end
146
+
147
+ describe RJR::NotificationMessage do
148
+ it "should accept notification parameters" do
149
+ msg = RJR::NotificationMessage.new :method => 'test',
150
+ :args => ['a', 1],
151
+ :headers => {:h => 2}
152
+ msg.jr_method.should == "test"
153
+ msg.jr_args.should =~ ['a', 1]
154
+ msg.headers.should have_key(:h)
155
+ msg.headers[:h].should == 2
156
+ end
157
+
158
+ it "should produce valid json" do
159
+ msg = RJR::NotificationMessage.new :method => 'test',
160
+ :args => ['a', 1],
161
+ :headers => {:h => 2}
162
+
163
+ msg_string = msg.to_s
164
+ msg_string.should include('"h":2')
165
+ msg_string.should include('"method":"test"')
166
+ msg_string.should include('"params":["a",1]')
167
+ msg_string.should include('"jsonrpc":"2.0"')
168
+ msg_string.should_not include('"id":"')
169
+ end
170
+
171
+ it "should parse notification message string" do
172
+ msg_string = '{"jsonrpc": "2.0", ' +
173
+ '"method": "test", "params": ["a", 1]}'
174
+ msg = RJR::NotificationMessage.new :message => msg_string
175
+ msg.json_message.should == msg_string
176
+ msg.jr_method.should == 'test'
177
+ msg.jr_args.should =~ ['a', 1]
178
+ end
179
+
180
+ it "should extract optional headers from message string" do
181
+ msg_string = '{"jsonrpc": "2.0", ' +
182
+ '"method": "test", "params": ["a", 1], ' +
183
+ '"h": 2}'
184
+ msg = RJR::NotificationMessage.new :message => msg_string, :headers => {'f' => 'g'}
185
+ msg.json_message.should == msg_string
186
+ msg.headers.should have_key 'h'
187
+ msg.headers.should have_key 'f'
188
+ msg.headers['h'].should == 2
189
+ msg.headers['f'].should == 'g'
190
+ end
191
+
192
+ it "should fail if parsing invalid message string" do
193
+ lambda {
194
+ msg = RJR::NotificationMessage.new :message => 'foobar'
195
+ }.should raise_error JSON::ParserError
196
+ end
197
+ end
198
+
199
+ describe RJR::MessageUtil do
200
+ it "should extract json messages out of a message stream" do
201
+ # TODO!
202
+ end
203
+ end
@@ -3,8 +3,8 @@ require 'rjr/amqp_node'
3
3
  require 'rjr/web_node'
4
4
  require 'rjr/dispatcher'
5
5
 
6
- describe RJR::AMQPNode do
7
- it "should invoke and satisfy amqp requests" do
6
+ describe RJR::MultiNode do
7
+ it "should invoke and satisfy requests over multiple protocols" do
8
8
  foolbar_invoked = false
9
9
  barfoo_invoked = false
10
10
  RJR::Dispatcher.init_handlers
@@ -25,10 +25,11 @@ describe RJR::AMQPNode do
25
25
 
26
26
  amqp = RJR::AMQPNode.new :node_id => 'amqp', :broker => 'localhost'
27
27
  web = RJR::WebNode.new :node_id => 'web', :host => 'localhost', :port => 9876
28
- multi = RJR::MultiNode.new :node_id => 'multi', :nodes => [amqp, web]
28
+ multi = RJR::MultiNode.new :node_id => 'multi', :nodes => [amqp, web], :keep_alive => true
29
29
 
30
30
  multi.listen
31
- amqp_client = RJR::AMQPNode.new :node_id => 'client', :broker => 'localhost'
31
+
32
+ amqp_client = RJR::AMQPNode.new :node_id => 'client', :broker => 'localhost', :keep_alive => true # see comment about keepalive in amqp_node_spec
32
33
  res = amqp_client.invoke_request 'amqp-queue', 'foolbar', 'myparam1'
33
34
  res.should == 'retval1'
34
35