rjr 0.9.0 → 0.11.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -7,6 +7,7 @@
7
7
  # Licensed under the Apache License, Version 2.0
8
8
 
9
9
  require 'rjr/common'
10
+ require 'json'
10
11
 
11
12
  module RJR
12
13
 
@@ -14,9 +15,11 @@ module RJR
14
15
  class Request
15
16
  # name of the method which request is for
16
17
  attr_accessor :method
18
+ alias :rjr_method :method
17
19
 
18
20
  # array of arguments which to pass to the rpc method handler
19
21
  attr_accessor :method_args
22
+ alias :rjr_method_args :method_args
20
23
 
21
24
  # hash of keys/values corresponding to optional headers received as part of of the request
22
25
  attr_accessor :headers
@@ -46,15 +49,17 @@ class Request
46
49
  # @option args [Symbol] :rjr_node_type type of the rjr node which request was received on
47
50
  # @option args [Callable] :handler callable object registered to the specified method which to invoke request on with arguments
48
51
  def initialize(args = {})
49
- @method = args[:method]
50
- @method_args = args[:method_args]
51
- @headers = args[:headers]
52
+ @method = args[:method] || args['method']
53
+ @rjr_method = @method
54
+ @method_args = args[:method_args] || args['method_args']
55
+ @rjr_method_args = @method_args
56
+ @headers = args[:headers] || args['headers']
52
57
  @client_ip = args[:client_ip]
53
58
  @client_port = args[:client_port]
54
59
  @rjr_callback = args[:rjr_callback]
55
60
  @rjr_node = args[:rjr_node]
56
- @rjr_node_id = args[:rjr_node_id]
57
- @rjr_node_type = args[:rjr_node_type]
61
+ @rjr_node_id = args[:rjr_node_id] || args['rjr_node_id']
62
+ @rjr_node_type = args[:rjr_node_type] || args['rjr_node_type']
58
63
  @handler = args[:handler]
59
64
  end
60
65
 
@@ -62,6 +67,7 @@ class Request
62
67
  # method parameters in the local scope
63
68
  def handle
64
69
  RJR::Logger.info "Dispatching '#{@method}' request with parameters (#{@method_args.join(',')}) on #{@rjr_node_type}-node(#{@rjr_node_id})"
70
+ # TODO compare arity of method to number of args ?
65
71
  retval = instance_exec(*@method_args, &@handler)
66
72
  RJR::Logger.info "#{@method} request with parameters (#{@method_args.join(',')}) returning #{retval}"
67
73
  return retval
@@ -100,17 +106,17 @@ class Result
100
106
  @error_message = nil
101
107
  @error_class = nil
102
108
 
103
- if args.has_key?(:result)
109
+ if args.has_key?(:result) || args.has_key?('result')
104
110
  @success = true
105
111
  @failed = false
106
- @result = args[:result]
112
+ @result = args[:result] || args['result']
107
113
 
108
- elsif args.has_key?(:error_code)
114
+ elsif args.has_key?(:error_code) || args.has_key?('error_code')
109
115
  @success = false
110
116
  @failed = true
111
- @error_code = args[:error_code]
112
- @error_msg = args[:error_msg]
113
- @error_class = args[:error_class]
117
+ @error_code = args[:error_code] || args['error_code']
118
+ @error_msg = args[:error_msg] || args['error_msg']
119
+ @error_class = args[:error_class] || args['error_class']
114
120
 
115
121
  end
116
122
  end
@@ -177,20 +183,79 @@ class Handler
177
183
  def handle(args = {})
178
184
  return Result.method_not_found(args[:missing_name]) if @method_name.nil?
179
185
 
186
+ result = nil
180
187
  begin
181
188
  request = Request.new args.merge(:method => @method_name,
182
189
  :handler => @handler_proc)
183
190
  retval = request.handle
184
- return Result.new(:result => retval)
191
+ result = Result.new(:result => retval)
185
192
 
186
193
  rescue Exception => e
187
194
  RJR::Logger.warn ["Exception Raised in #{method_name} handler #{e}"] + e.backtrace
188
-
189
- return Result.new(:error_code => -32000,
190
- :error_msg => e.to_s,
191
- :error_class => e.class)
195
+ result = Result.new(:error_code => -32000,
196
+ :error_msg => e.to_s,
197
+ :error_class => e.class)
192
198
 
193
199
  end
200
+
201
+ DispatcherStat << DispatcherStat.new(request, result)
202
+ return result
203
+ end
204
+ end
205
+
206
+ # Tracks high level dispatcher states
207
+ class DispatcherStat
208
+ # Request invoked
209
+ attr_reader :request
210
+
211
+ # Result returned
212
+ attr_reader :result
213
+
214
+ # Initialized the stat w/ the corresponding request/result
215
+ def initialize(request, result)
216
+ @request = request
217
+ @result = result
218
+ end
219
+
220
+ # Global stats registry
221
+ def self.stats
222
+ @stats ||= []
223
+ end
224
+
225
+ # Reinit the stats registry
226
+ def self.reset
227
+ @stats = []
228
+ end
229
+
230
+ # Add stat to the global registry
231
+ def self.<<(s)
232
+ @stats ||= []
233
+ @stats << s
234
+ self
235
+ end
236
+
237
+ # Convert stat to json representation and return it
238
+ def to_json(*a)
239
+ {
240
+ 'json_class' => self.class.name,
241
+ 'data' =>
242
+ {:request => {:method => request.method,
243
+ :method_args => request.method_args,
244
+ :headers => request.headers,
245
+ :rjr_node_type => request.rjr_node_type,
246
+ :rjr_node_id => request.rjr_node_id
247
+ },
248
+ :result => {:result => result.result,
249
+ :error_code => result.error_code,
250
+ :error_msg => result.error_msg,
251
+ :error_class => result.error_class} }
252
+ }.to_json(*a)
253
+ end
254
+
255
+ # Create new stat from json representation
256
+ def self.json_create(o)
257
+ stat = new(Request.new(o['data']['request']), Result.new(o['data']['result']))
258
+ return stat
194
259
  end
195
260
  end
196
261
 
@@ -231,11 +296,28 @@ class Dispatcher
231
296
  method_names = Array(method_names) unless method_names.is_a?(Array)
232
297
  @@handlers ||= {}
233
298
  method_names.each { |method_name|
299
+ # TODO support registering multiple handlers per method? (and in dispatch_request below)
234
300
  @@handlers[method_name] = Handler.new args.merge(:method => method_name,
235
301
  :handler => handler)
236
302
  }
237
303
  end
238
304
 
305
+ # Clear registered method handlers
306
+ def self.clear!
307
+ @@handlers = {}
308
+ end
309
+
310
+ # Return boolean indicating if handler for the specifed method has been registered
311
+ def self.has_handler_for?(method_name)
312
+ @@handlers ||= {}
313
+ !@@handlers.find { |k,v| k == method_name }.nil?
314
+ end
315
+
316
+ # Return the handler for the specified method
317
+ def self.handler_for(method_name)
318
+ @@handlers[method_name]
319
+ end
320
+
239
321
  # Helper used by RJR nodes to dispatch requests received via transports to
240
322
  # registered handlers.
241
323
  def self.dispatch_request(method_name, args = {})
@@ -0,0 +1,110 @@
1
+ # EventMachine Adapter
2
+ #
3
+ # Copyright (C) 2012 Mohammed Morsi <mo@morsi.org>
4
+ # Licensed under the Apache License, Version 2.0
5
+
6
+ require 'singleton'
7
+ require 'eventmachine'
8
+
9
+ # EventMachine wrapper / helper interface, ties reactor
10
+ # lifecycle to an instance of this class.
11
+ #
12
+ # TODO move to the RJR namespace
13
+ class EMManager
14
+
15
+ # Run reactor in its own interally managed thread
16
+ attr_accessor :reactor_thread
17
+
18
+ # EMManager initializer
19
+ def initialize
20
+ @em_lock = Mutex.new
21
+ end
22
+
23
+ # Start the eventmachine reactor thread if not running
24
+ def start
25
+ @em_lock.synchronize{
26
+ # TODO on event of the process ending this thread will be
27
+ # shutdown before a local finalizer can be run,
28
+ # would be good to gracefully shut this down / wait for completion
29
+ @reactor_thread = Thread.new {
30
+ begin
31
+ EventMachine.run
32
+ rescue Exception => e
33
+ # TODO option to autorestart the reactor on errors ?
34
+ puts "Critical exception #{e}\n#{e.backtrace.join("\n")}"
35
+ ensure
36
+ @reactor_thread = nil
37
+ end
38
+ } unless @reactor_thread
39
+ }
40
+ sleep 0.01 until EventMachine.reactor_running? # XXX hack but needed
41
+ end
42
+
43
+ # Schedule a new job to be run in event machine
44
+ # @param [Callable] bl callback to be invoked by eventmachine
45
+ def schedule(&bl)
46
+ EventMachine.schedule &bl
47
+ end
48
+
49
+ # Schedule a job to be run once after a specified interval in event machine
50
+ # @param [Integer] seconds int interval which to wait before invoking specified block
51
+ # @param [Callable] bl callback to be invoked by eventmachine
52
+ def add_timer(seconds, &bl)
53
+ EventMachine.add_timer(seconds, &bl)
54
+ end
55
+
56
+ # Schedule a block to be run periodically in event machine
57
+ # @param [Integer] seconds int interval which to invoke specified block
58
+ # @param [Callable] bl callback to be invoked by eventmachine
59
+ def add_periodic_timer(seconds, &bl)
60
+ EventMachine.add_periodic_timer(seconds, &bl)
61
+ end
62
+
63
+ # Return boolean indicating if event machine reactor is running
64
+ def running?
65
+ @em_lock.synchronize{
66
+ EventMachine.reactor_running?
67
+ }
68
+ end
69
+
70
+ # Block until reactor thread is terminated
71
+ def join
72
+ th = nil
73
+ @em_lock.synchronize{
74
+ th = @reactor_thread
75
+ }
76
+ th.join unless th.nil?
77
+ end
78
+
79
+ # Terminate the event machine reactor under all conditions
80
+ def halt
81
+ @em_lock.synchronize{
82
+ EventMachine.stop_event_loop
83
+ }
84
+ end
85
+ end
86
+
87
+
88
+ # Provides an interface which to access a shared EMManager
89
+ #
90
+ # EMManager operations may be invoked on this class after
91
+ # the 'init' method is called
92
+ #
93
+ # EMAdapter.init
94
+ # EMAdapter.start
95
+ class EMAdapter
96
+ # Initialize EM subsystem
97
+ def self.init
98
+ if @em_manager.nil?
99
+ @em_manager = EMManager.new
100
+ end
101
+
102
+ @em_manager.start
103
+ end
104
+
105
+ # Delegates all methods invoked on calls to EMManager
106
+ def self.method_missing(method_id, *args, &bl)
107
+ @em_manager.send method_id, *args, &bl
108
+ end
109
+
110
+ end
@@ -0,0 +1,66 @@
1
+ # JSON-RPC method definitions providing access to inspect the internal
2
+ # node state.
3
+ #
4
+ # Note this isn't included in the top level rjr module by default,
5
+ # manually include this module to incorporate these additional rjr method
6
+ # definitions into your node
7
+ #
8
+ # Copyright (C) 2013 Mohammed Morsi <mo@morsi.org>
9
+ # Licensed under the Apache License, Version 2.0
10
+
11
+ require 'rjr/util'
12
+ include RJR::Definitions
13
+
14
+ # Helper method to process user params / select stats
15
+ def select_stats(*filter)
16
+ lf = []
17
+ while q = filter.shift
18
+ lf <<
19
+ case q
20
+ when 'on_node' then
21
+ n = filter.shift
22
+ lambda { |ds| ds.request.rjr_node_type.to_s == n}
23
+
24
+ when "for_method" then
25
+ m = filter.shift
26
+ lambda { |ds| ds.request.rjr_method == m}
27
+
28
+ when 'successful' then
29
+ lambda { |ds| ds.result.success }
30
+
31
+ when 'failed' then
32
+ lambda { |ds| ds.result.failed }
33
+
34
+ end
35
+ end
36
+
37
+ RJR::DispatcherStat.stats.select { |ds| lf.all? { |lf| lf.call(ds) } }
38
+ end
39
+
40
+ rjr_method \
41
+ "rjr::dispatches" =>
42
+ # Retrieve all the dispatches this node served matching the specified criteri
43
+ lambda { |*filter| select_stats(*filter) },
44
+
45
+ "rjr::num_dispatches" =>
46
+ # Retrieve the number of dispatches this node served matching the specified criteria
47
+ lambda { |*filter| select_stats(*filter).size },
48
+
49
+ "rjr::status" =>
50
+ # Retrieve the overall status of this node
51
+ lambda {
52
+ {
53
+ # event machine
54
+ :event_machine => { :running => EMAdapter.running?,
55
+ :has_jobs => EMAdapter.has_jobs?,
56
+ :thread_status => EMAdapter.reactor_thread.status,
57
+ :jobs => EMAdapter.em_jobs },
58
+
59
+ # thread pool
60
+ :thread_pool => { :running => ThreadPool2Manager.thread_pool.running?,
61
+ :inspect => ThreadPool2Manager.thread_pool.inspect },
62
+ }
63
+ }
64
+
65
+ #:log =>
66
+ # lambda {},
@@ -7,6 +7,7 @@
7
7
 
8
8
  require 'rjr/node'
9
9
  require 'rjr/message'
10
+ require 'rjr/dispatcher'
10
11
 
11
12
  module RJR
12
13
 
data/lib/rjr/message.rb CHANGED
@@ -82,8 +82,9 @@ class RequestMessage
82
82
  # @return [true,false] indicating if message is request message
83
83
  def self.is_request_message?(message)
84
84
  begin
85
- # TODO log error
86
- JSON.parse(message).has_key?('method')
85
+ # FIXME log error
86
+ parsed = JSON.parse(message)
87
+ parsed.has_key?('method') && parsed.has_key?('id')
87
88
  rescue Exception => e
88
89
  false
89
90
  end
@@ -174,7 +175,8 @@ class ResponseMessage
174
175
  json = JSON.parse(message)
175
176
  json.has_key?('result') || json.has_key?('error')
176
177
  rescue Exception => e
177
- # TODO log error
178
+ # FIXME log error
179
+ #puts e.to_s
178
180
  false
179
181
  end
180
182
  end
@@ -200,4 +202,122 @@ class ResponseMessage
200
202
  end
201
203
  end
202
204
 
205
+ # Message sent to a jsonrpc node to invoke a rpc method but
206
+ # indicate the result should _not_ be returned
207
+ class NotificationMessage
208
+ # Message string received from the source
209
+ attr_accessor :json_message
210
+
211
+ # Method source is invoking on the destination
212
+ attr_accessor :jr_method
213
+
214
+ # Arguments source is passing to destination method
215
+ attr_accessor :jr_args
216
+
217
+ # Optional headers to add to json outside of standard json-rpc request
218
+ attr_accessor :headers
219
+
220
+ # RJR Notification Message initializer
221
+ #
222
+ # This should be invoked with one of two argument sets. If creating a new message
223
+ # to send to the server, specify :method, :args, and :headers to include in the message
224
+ # If handling an new request message sent from the client, simply specify :message and
225
+ # optionally any additional headers (they will be merged with the headers contained in
226
+ # the message)
227
+ #
228
+ # No message id will be generated in accordance w/ the jsonrpc standard
229
+ #
230
+ # @param [Hash] args options to set on request
231
+ # @option args [String] :message json string received from sender
232
+ # @option args [Hash] :headers optional headers to set in request and subsequent messages
233
+ # @option args [String] :method method to invoke on server
234
+ # @option args [Array<Object>] :args to pass to server method, all must be convertable to/from json
235
+ def initialize(args = {})
236
+ if args.has_key?(:message)
237
+ begin
238
+ notification = JSON.parse(args[:message])
239
+ @json_message = args[:message]
240
+ @jr_method = notification['method']
241
+ @jr_args = notification['params']
242
+ @headers = args.has_key?(:headers) ? {}.merge!(args[:headers]) : {}
243
+
244
+ notification.keys.select { |k|
245
+ !['jsonrpc', 'method', 'params'].include?(k)
246
+ }.each { |k| @headers[k] = notification[k] }
247
+
248
+ rescue Exception => e
249
+ #puts "Exception Parsing Notification #{e}"
250
+ raise e
251
+ end
252
+
253
+ elsif args.has_key?(:method)
254
+ @jr_method = args[:method]
255
+ @jr_args = args[:args]
256
+ @headers = args[:headers]
257
+
258
+ end
259
+ end
260
+
261
+ # Class helper to determine if the specified string is a valid json-rpc
262
+ # notification
263
+ #
264
+ # @param [String] message string message to check
265
+ # @return [true,false] indicating if message is a notification message
266
+ def self.is_notification_message?(message)
267
+ begin
268
+ # FIXME log error
269
+ parsed = JSON.parse(message)
270
+ parsed.has_key?('method') && !parsed.has_key?('id')
271
+ rescue Exception => e
272
+ false
273
+ end
274
+ end
275
+
276
+ # Convert notification message to string json format
277
+ def to_s
278
+ notification = { 'jsonrpc' => '2.0',
279
+ 'method' => @jr_method,
280
+ 'params' => @jr_args }
281
+ notification.merge!(@headers) unless @headers.nil?
282
+ notification.to_json.to_s
283
+ end
284
+
285
+ end
286
+
287
+ # Helper utilities for messages
288
+ class MessageUtil
289
+ # Retrieve and return a single json message from a data string.
290
+ #
291
+ # Returns the message and remaining portion of the data string,
292
+ # if message is found, else nil
293
+ #
294
+ # XXX really don't like having to do this, but a quick solution
295
+ # to the issue of multiple messages appearing in one tcp data packet.
296
+ #
297
+ # TODO efficiency can probably be optimized
298
+ # in the case closing '}' hasn't arrived yet
299
+ def self.retrieve_json(data)
300
+ return nil if data.nil? || data.empty?
301
+ start = 0
302
+ start += 1 until start == data.length || data[start] == '{'
303
+ on = mi = 0
304
+ start.upto(data.length - 1).each { |i|
305
+ if data[i] == '{'
306
+ on += 1
307
+ elsif data[i] == '}'
308
+ on -= 1
309
+ end
310
+
311
+ if on == 0
312
+ mi = i
313
+ break
314
+ end
315
+ }
316
+
317
+ return nil if mi == 0
318
+ return data[start..mi], data[(mi+1)..-1]
319
+ end
320
+
321
+ end
322
+
203
323
  end
@@ -0,0 +1,17 @@
1
+ # RJR Missing Node Endpoint
2
+ #
3
+ # Provides a entity able to be associated with a rjr endpoint
4
+ # if the corresponding node cannot be loaded for whatever reason
5
+ #
6
+ # Copyright (C) 2012 Mohammed Morsi <mo@morsi.org>
7
+ # Licensed under the Apache License, Version 2.0
8
+
9
+ require 'rjr/node'
10
+
11
+ module RJR
12
+ class MissingNode < RJR::Node
13
+ def method_missing(method_id, *args, &bl)
14
+ raise "rjr node #{node_id} is missing a dependency - cannot invoke #{method_id}"
15
+ end
16
+ end
17
+ end
@@ -34,6 +34,9 @@ module RJR
34
34
  # # invoke requests as you normally would via any protocol
35
35
  #
36
36
  class MultiNode < RJR::Node
37
+ # Return the nodes
38
+ attr_reader :nodes
39
+
37
40
  # MultiNode initializer
38
41
  # @param [Hash] args the options to create the tcp node with
39
42
  # @option args [Array<RJR::Node>] :nodes array of nodes to use to listen to new requests on