rjr 0.9.0 → 0.11.7

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.
@@ -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