rjr 0.12.2 → 0.15.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.
Files changed (65) hide show
  1. data/README.md +49 -36
  2. data/Rakefile +2 -0
  3. data/bin/rjr-client +11 -9
  4. data/bin/rjr-server +12 -10
  5. data/examples/amqp.rb +29 -0
  6. data/examples/client.rb +32 -0
  7. data/examples/complete.rb +36 -0
  8. data/examples/local.rb +29 -0
  9. data/examples/server.rb +26 -0
  10. data/examples/tcp.rb +29 -0
  11. data/examples/web.rb +22 -0
  12. data/examples/ws.rb +29 -0
  13. data/lib/rjr/common.rb +7 -12
  14. data/lib/rjr/dispatcher.rb +171 -239
  15. data/lib/rjr/em_adapter.rb +33 -66
  16. data/lib/rjr/message.rb +43 -12
  17. data/lib/rjr/node.rb +197 -103
  18. data/lib/rjr/nodes/amqp.rb +216 -0
  19. data/lib/rjr/nodes/easy.rb +159 -0
  20. data/lib/rjr/nodes/local.rb +118 -0
  21. data/lib/rjr/{missing_node.rb → nodes/missing.rb} +4 -2
  22. data/lib/rjr/nodes/multi.rb +79 -0
  23. data/lib/rjr/nodes/tcp.rb +211 -0
  24. data/lib/rjr/nodes/web.rb +197 -0
  25. data/lib/rjr/nodes/ws.rb +187 -0
  26. data/lib/rjr/stats.rb +70 -0
  27. data/lib/rjr/thread_pool.rb +178 -123
  28. data/site/index.html +45 -0
  29. data/site/jquery-latest.js +9404 -0
  30. data/site/jrw.js +297 -0
  31. data/site/json.js +199 -0
  32. data/specs/dispatcher_spec.rb +244 -198
  33. data/specs/em_adapter_spec.rb +52 -80
  34. data/specs/message_spec.rb +223 -197
  35. data/specs/node_spec.rb +67 -163
  36. data/specs/nodes/amqp_spec.rb +82 -0
  37. data/specs/nodes/easy_spec.rb +13 -0
  38. data/specs/nodes/local_spec.rb +72 -0
  39. data/specs/nodes/multi_spec.rb +65 -0
  40. data/specs/nodes/tcp_spec.rb +75 -0
  41. data/specs/nodes/web_spec.rb +77 -0
  42. data/specs/nodes/ws_spec.rb +78 -0
  43. data/specs/stats_spec.rb +59 -0
  44. data/specs/thread_pool_spec.rb +44 -35
  45. metadata +40 -30
  46. data/lib/rjr/amqp_node.rb +0 -330
  47. data/lib/rjr/inspect.rb +0 -65
  48. data/lib/rjr/local_node.rb +0 -150
  49. data/lib/rjr/multi_node.rb +0 -65
  50. data/lib/rjr/tcp_node.rb +0 -323
  51. data/lib/rjr/thread_pool2.rb +0 -272
  52. data/lib/rjr/util.rb +0 -104
  53. data/lib/rjr/web_node.rb +0 -266
  54. data/lib/rjr/ws_node.rb +0 -289
  55. data/lib/rjr.rb +0 -16
  56. data/specs/amqp_node_spec.rb +0 -31
  57. data/specs/inspect_spec.rb +0 -60
  58. data/specs/local_node_spec.rb +0 -43
  59. data/specs/multi_node_spec.rb +0 -45
  60. data/specs/tcp_node_spec.rb +0 -33
  61. data/specs/util_spec.rb +0 -46
  62. data/specs/web_node_spec.rb +0 -32
  63. data/specs/ws_node_spec.rb +0 -32
  64. /data/lib/rjr/{tcp_node2.rb → nodes/tcp2.rb} +0 -0
  65. /data/lib/rjr/{udp_node.rb → nodes/udp.rb} +0 -0
@@ -0,0 +1,197 @@
1
+ # RJR WWW Endpoint
2
+ #
3
+ # Implements the RJR::Node interface to satisty JSON-RPC requests over the HTTP protocol
4
+ #
5
+ # The web node does not support callbacks at the moment,
6
+ # though would like to look into how to implement this
7
+ #
8
+ # Copyright (C) 2012-2013 Mohammed Morsi <mo@morsi.org>
9
+ # Licensed under the Apache License, Version 2.0
10
+
11
+ skip_module = false
12
+ begin
13
+ require 'evma_httpserver'
14
+ require 'em-http-request'
15
+ # TODO also support fallback clients ? (curb / net/http / etc)
16
+ rescue LoadError
17
+ skip_module = true
18
+ end
19
+
20
+ if skip_module
21
+ # TODO output: "em-http-request/evma_httpserver gems could not be loaded, skipping web node definition"
22
+ require 'rjr/nodes/missing'
23
+ RJR::Nodes::Web = RJR::Nodes::Missing
24
+
25
+ else
26
+
27
+ require 'thread'
28
+ require 'eventmachine'
29
+
30
+ require 'rjr/node'
31
+ require 'rjr/message'
32
+
33
+ module RJR
34
+ module Nodes
35
+
36
+ # @private
37
+ # Helper class intialized by eventmachine encapsulating a http connection
38
+ class WebConnection < EventMachine::Connection
39
+ include EventMachine::HttpServer
40
+
41
+ # WebConnection initializer.
42
+ #
43
+ # specify the web node establishing the connection
44
+ def initialize(args = {})
45
+ @rjr_node = args[:rjr_node]
46
+ end
47
+
48
+ # {EventMachine::Connection#process_http_request} callback, handle request messages
49
+ def process_http_request
50
+ # TODO support http protocols other than POST
51
+ msg = @http_post_content.nil? ? '' : @http_post_content
52
+ @rjr_node.send(:handle_message, msg, self) # XXX private method
53
+
54
+ # XXX we still have to send a response back to client to satisfy
55
+ # the http standard, even if this is a notification. handle_message
56
+ # does not do this.
57
+ @rjr_node.send_msg "", self if NotificationMessage.is_notification_message?(msg)
58
+ end
59
+ end
60
+
61
+ # Web node definition, listen for and invoke json-rpc requests via web requests
62
+ #
63
+ # Clients should specify the hostname / port when listening for requests and
64
+ # when invoking them.
65
+ #
66
+ # *note* the RJR javascript client also supports sending / receiving json-rpc
67
+ # messages over http
68
+ #
69
+ # @example Listening for json-rpc requests over tcp
70
+ # # initialize node
71
+ # server = RJR::Nodes::Web.new :node_id => 'server', :host => 'localhost', :port => '7777'
72
+ #
73
+ # # register rjr dispatchers (see RJR::Dispatcher)
74
+ # server.dispatcher.handle('hello') do |name|
75
+ # "Hello #{name}!"
76
+ # end
77
+ #
78
+ # # listen, and block
79
+ # server.listen
80
+ # server.join
81
+ #
82
+ # @example Invoking json-rpc requests over http using rjr
83
+ # client = RJR::Nodes::Web.new :node_id => 'client'
84
+ # puts client.invoke('http://localhost:7777', 'hello', 'mo')
85
+ #
86
+ # @example Invoking json-rpc requests over http using curl
87
+ # $ curl -X POST http://localhost:7777 -d '{"jsonrpc":"2.0","method":"hello","params":["mo"],"id":"123"}'
88
+ # > {"jsonrpc":"2.0","id":"123","result":"Hello mo!"}
89
+ #
90
+ class Web < RJR::Node
91
+
92
+ RJR_NODE_TYPE = :web
93
+
94
+ public
95
+
96
+ # Web initializer
97
+ # @param [Hash] args the options to create the tcp node with
98
+ # @option args [String] :host the hostname/ip which to listen on
99
+ # @option args [Integer] :port the port which to listen on
100
+ def initialize(args = {})
101
+ super(args)
102
+ @host = args[:host]
103
+ @port = args[:port]
104
+ end
105
+
106
+ # Send data using specified http connection
107
+ #
108
+ # Implementation of {RJR::Node#send_msg}
109
+ def send_msg(data, connection)
110
+ # we are assuming that since http connections
111
+ # are not persistant, we should be sending a
112
+ # response message here
113
+
114
+ resp = EventMachine::DelegatedHttpResponse.new(connection)
115
+ #resp.status = response.result.success ? 200 : 500
116
+ resp.status = 200
117
+ resp.content = data.to_s
118
+ resp.content_type "application/json"
119
+ resp.send_response
120
+ end
121
+
122
+ # Instruct Node to start listening for and dispatching rpc requests
123
+ #
124
+ # Implementation of {RJR::Node#listen}
125
+ def listen
126
+ @em.schedule do
127
+ EventMachine::start_server(@host, @port, WebConnection, :rjr_node => self)
128
+ end
129
+ self
130
+ end
131
+
132
+ # Instructs node to send rpc request, and wait for / return response
133
+ #
134
+ # Implementation of {RJR::Node#invoke}
135
+ #
136
+ # Do not invoke directly from em event loop or callback as will block the message
137
+ # subscription used to receive responses
138
+ #
139
+ # @param [String] uri location of node to send request to, should be
140
+ # in format of http://hostname:port
141
+ # @param [String] rpc_method json-rpc method to invoke on destination
142
+ # @param [Array] args array of arguments to convert to json and invoke remote method wtih
143
+ def invoke(uri, rpc_method, *args)
144
+ message = RequestMessage.new :method => rpc_method,
145
+ :args => args,
146
+ :headers => @message_headers
147
+ cb = lambda { |http|
148
+ # TODO handle errors
149
+ handle_message(http.response, http)
150
+ }
151
+
152
+ @em.schedule do
153
+ http = EventMachine::HttpRequest.new(uri).post :body => message.to_s
154
+ http.errback &cb
155
+ http.callback &cb
156
+ end
157
+
158
+ # will block until response message is received
159
+ # TODO optional timeout for response ?
160
+ result = wait_for_result(message)
161
+ if result.size > 2
162
+ raise Exception, result[2]
163
+ end
164
+ return result[1]
165
+ end
166
+
167
+ # Instructs node to send rpc notification (immadiately returns / no response is generated)
168
+ #
169
+ # Implementation of {RJR::Node#notify}
170
+ #
171
+ # @param [String] uri location of node to send request to, should be
172
+ # in format of http://hostname:port
173
+ # @param [String] rpc_method json-rpc method to invoke on destination
174
+ # @param [Array] args array of arguments to convert to json and invoke remote method wtih
175
+ def notify(uri, rpc_method, *args)
176
+ # will block until message is published
177
+ published_l = Mutex.new
178
+ published_c = ConditionVariable.new
179
+
180
+ invoked = false
181
+ message = NotificationMessage.new :method => rpc_method,
182
+ :args => args,
183
+ :headers => @message_headers
184
+ cb = lambda { |arg| published_l.synchronize { invoked = true ; published_c.signal }}
185
+ @em.schedule do
186
+ http = EventMachine::HttpRequest.new(uri).post :body => message.to_s
187
+ http.errback &cb
188
+ http.callback &cb
189
+ end
190
+ published_l.synchronize { published_c.wait published_l unless invoked }
191
+ nil
192
+ end
193
+ end
194
+
195
+ end # module Nodes
196
+ end # module RJR
197
+ end # !skip_module
@@ -0,0 +1,187 @@
1
+ # RJR WebSockets Endpoint
2
+ #
3
+ # Implements the RJR::Node interface to satisty JSON-RPC requests
4
+ # over the websockets protocol
5
+ #
6
+ # Copyright (C) 2012-2013 Mohammed Morsi <mo@morsi.org>
7
+ # Licensed under the Apache License, Version 2.0
8
+
9
+ skip_module = false
10
+ begin
11
+ require 'em-websocket'
12
+ require 'em-websocket-client'
13
+ rescue LoadError
14
+ skip_module = true
15
+ end
16
+
17
+ if skip_module
18
+ # TODO output: "ws dependencies could not be loaded, skipping ws node definition"
19
+ require 'rjr/nodes/missing'
20
+ RJR::Nodes::WS = RJR::Nodes::Missing
21
+
22
+ else
23
+ require 'thread'
24
+
25
+ require 'rjr/node'
26
+ require 'rjr/message'
27
+
28
+ module RJR
29
+ module Nodes
30
+
31
+ # Web socket node definition, listen for and invoke json-rpc requests via web sockets
32
+ #
33
+ # Clients should specify the hostname / port when listening for and invoking requests.
34
+ #
35
+ # *note* the RJR javascript client also supports sending / receiving json-rpc
36
+ # messages over web sockets
37
+ #
38
+ # @example Listening for json-rpc requests over tcp
39
+ # # initialize node
40
+ # server = RJR::Nodes::WS.new :node_id => 'server', :host => 'localhost', :port => '7777'
41
+ #
42
+ # # register rjr dispatchers (see RJR::Dispatcher)
43
+ # server.dispatcher.handle('hello') do |name|
44
+ # "Hello #{name}!"
45
+ # end
46
+ #
47
+ # # listen, and block
48
+ # server.listen
49
+ # server.join
50
+ #
51
+ # @example Invoking json-rpc requests over web sockets using rjr
52
+ # client = RJR::Nodes::WS.new :node_id => 'client'
53
+ # puts client.invoke_request('ws://localhost:7777', 'hello', 'mo')
54
+ #
55
+ class WS < RJR::Node
56
+ RJR_NODE_TYPE = :ws
57
+
58
+ private
59
+
60
+ # Internal helper initialize new client
61
+ def init_client(uri, &on_init)
62
+ connection = nil
63
+ @connections_lock.synchronize {
64
+ connection = @connections.find { |c|
65
+ c.url == uri
66
+ }
67
+ if connection.nil?
68
+ connection = EventMachine::WebSocketClient.connect(uri)
69
+ connection.callback do
70
+ on_init.call(connection)
71
+ end
72
+ @connections << connection
73
+ # TODO sleep until connected?
74
+ else
75
+ on_init.call(connection)
76
+ end
77
+ }
78
+ connection
79
+ end
80
+
81
+ public
82
+
83
+ # WS initializer
84
+ # @param [Hash] args the options to create the web socket node with
85
+ # @option args [String] :host the hostname/ip which to listen on
86
+ # @option args [Integer] :port the port which to listen on
87
+ def initialize(args = {})
88
+ super(args)
89
+ @host = args[:host]
90
+ @port = args[:port]
91
+
92
+ @connections = []
93
+ @connections_lock = Mutex.new
94
+ end
95
+
96
+ # Send data using specified websocket safely
97
+ #
98
+ # Implementation of {RJR::Node#send_msg}
99
+ def send_msg(data, ws)
100
+ ws.send(data)
101
+ end
102
+
103
+ # Instruct Node to start listening for and dispatching rpc requests
104
+ #
105
+ # Implementation of {RJR::Node#listen}
106
+ def listen
107
+ @em.schedule do
108
+ EventMachine::WebSocket.start(:host => @host, :port => @port) do |ws|
109
+ ws.onopen { }
110
+ ws.onclose { @connection_event_handlers[:closed].each { |h| h.call self } }
111
+ ws.onerror { |e| @connection_event_handlers[:error].each { |h| h.call self } }
112
+ ws.onmessage { |msg| handle_message(msg, ws) }
113
+ end
114
+ end
115
+ self
116
+ end
117
+
118
+ # Instructs node to send rpc request, and wait for / return response
119
+ #
120
+ # Implementation of {RJR::Node#invoke}
121
+ #
122
+ # Do not invoke directly from em event loop or callback as will block the message
123
+ # subscription used to receive responses
124
+ #
125
+ # @param [String] uri location of node to send request to, should be
126
+ # in format of ws://hostname:port
127
+ # @param [String] rpc_method json-rpc method to invoke on destination
128
+ # @param [Array] args array of arguments to convert to json and invoke remote method wtih
129
+ def invoke(uri, rpc_method, *args)
130
+ message = RequestMessage.new :method => rpc_method,
131
+ :args => args,
132
+ :headers => @message_headers
133
+
134
+ @em.schedule {
135
+ init_client(uri) do |c|
136
+ c.stream { |msg| handle_message(msg, c) }
137
+
138
+ c.send_msg message.to_s
139
+ end
140
+ }
141
+
142
+ # TODO optional timeout for response ?
143
+ result = wait_for_result(message)
144
+
145
+ if result.size > 2
146
+ raise Exception, result[2]
147
+ end
148
+ return result[1]
149
+ end
150
+
151
+ # Instructs node to send rpc notification (immadiately returns / no response is generated)
152
+ #
153
+ # Implementation of {RJR::Node#notify}
154
+ #
155
+ # @param [String] uri location of node to send notification to, should be
156
+ # in format of ws://hostname:port
157
+ # @param [String] rpc_method json-rpc method to invoke on destination
158
+ # @param [Array] args array of arguments to convert to json and invoke remote method wtih
159
+ def notify(uri, rpc_method, *args)
160
+ # will block until message is published
161
+ published_l = Mutex.new
162
+ published_c = ConditionVariable.new
163
+
164
+ invoked = false
165
+ message = NotificationMessage.new :method => rpc_method,
166
+ :args => args,
167
+ :headers => @message_headers
168
+ @em.schedule {
169
+ init_client(uri) do |c|
170
+ c.stream { |msg| handle_message(msg, c) }
171
+
172
+ c.send_msg message.to_s
173
+
174
+ # XXX same issue w/ tcp node, due to nature of event machine
175
+ # we aren't guaranteed that message is actually written to socket
176
+ # here, process must be kept alive until data is sent or will be lost
177
+ published_l.synchronize { invoked = true ; published_c.signal }
178
+ end
179
+ }
180
+ published_l.synchronize { published_c.wait published_l unless invoked }
181
+ nil
182
+ end
183
+ end
184
+
185
+ end # module Nodes
186
+ end # module RJR
187
+ end # (!skip_module)
data/lib/rjr/stats.rb ADDED
@@ -0,0 +1,70 @@
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 'eventmachine'
12
+
13
+ # Helper method to process user params / select stats
14
+ # from a dispatcher
15
+ def select_stats(dispatcher, *filter)
16
+ lf = []
17
+ while q = filter.shift
18
+ lf <<
19
+ case q
20
+ when 'on_node' then
21
+ n = filter.shift
22
+ lambda { |req| req.rjr_node_type.to_s == n}
23
+
24
+ when "for_method" then
25
+ m = filter.shift
26
+ lambda { |req| req.rjr_method == m}
27
+
28
+ when 'successful' then
29
+ lambda { |req| req.result.success }
30
+
31
+ when 'failed' then
32
+ lambda { |req| req.result.failed }
33
+
34
+ end
35
+ end
36
+
37
+ dispatcher.requests.select { |ds| lf.all? { |lfi| lfi.call(ds) } }
38
+ end
39
+
40
+ # Add stats methods to specified dispatcher
41
+ def dispatch_stats(dispatcher)
42
+ # Retrieve all the dispatches this node served matching the specified criteri
43
+ dispatcher.handle "rjr::dispatches" do |filter|
44
+ select_stats(*filter)
45
+ end
46
+
47
+ # Retrieve the number of dispatches this node served matching the specified criteria
48
+ dispatcher.handle "rjr::num_dispatches" do |filter|
49
+ select_stats(*filter).size
50
+ end
51
+
52
+ # Retrieve the internal status of this node
53
+ dispatcher.handle "rjr::status" do
54
+ {
55
+ # event machine
56
+ :event_machine => { :running => EventMachine.reactor_running?,
57
+ :thread_status => RJR::EMAdapter.instance.rector_thread ?
58
+ RJR::EMAdapter.instance.reactor_thread.status :
59
+ nil,
60
+ :connections => EventMachine.connection_count },
61
+
62
+ # thread pool
63
+ :thread_pool => { :running => RJR::ThreadPool.instance.running? }
64
+ }
65
+ end
66
+
67
+ #:log =>
68
+ # lambda {},
69
+ end
70
+