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.
- data/README.md +49 -36
- data/Rakefile +2 -0
- data/bin/rjr-client +11 -9
- data/bin/rjr-server +12 -10
- data/examples/amqp.rb +29 -0
- data/examples/client.rb +32 -0
- data/examples/complete.rb +36 -0
- data/examples/local.rb +29 -0
- data/examples/server.rb +26 -0
- data/examples/tcp.rb +29 -0
- data/examples/web.rb +22 -0
- data/examples/ws.rb +29 -0
- data/lib/rjr/common.rb +7 -12
- data/lib/rjr/dispatcher.rb +171 -239
- data/lib/rjr/em_adapter.rb +33 -66
- data/lib/rjr/message.rb +43 -12
- data/lib/rjr/node.rb +197 -103
- data/lib/rjr/nodes/amqp.rb +216 -0
- data/lib/rjr/nodes/easy.rb +159 -0
- data/lib/rjr/nodes/local.rb +118 -0
- data/lib/rjr/{missing_node.rb → nodes/missing.rb} +4 -2
- data/lib/rjr/nodes/multi.rb +79 -0
- data/lib/rjr/nodes/tcp.rb +211 -0
- data/lib/rjr/nodes/web.rb +197 -0
- data/lib/rjr/nodes/ws.rb +187 -0
- data/lib/rjr/stats.rb +70 -0
- data/lib/rjr/thread_pool.rb +178 -123
- data/site/index.html +45 -0
- data/site/jquery-latest.js +9404 -0
- data/site/jrw.js +297 -0
- data/site/json.js +199 -0
- data/specs/dispatcher_spec.rb +244 -198
- data/specs/em_adapter_spec.rb +52 -80
- data/specs/message_spec.rb +223 -197
- data/specs/node_spec.rb +67 -163
- data/specs/nodes/amqp_spec.rb +82 -0
- data/specs/nodes/easy_spec.rb +13 -0
- data/specs/nodes/local_spec.rb +72 -0
- data/specs/nodes/multi_spec.rb +65 -0
- data/specs/nodes/tcp_spec.rb +75 -0
- data/specs/nodes/web_spec.rb +77 -0
- data/specs/nodes/ws_spec.rb +78 -0
- data/specs/stats_spec.rb +59 -0
- data/specs/thread_pool_spec.rb +44 -35
- metadata +40 -30
- data/lib/rjr/amqp_node.rb +0 -330
- data/lib/rjr/inspect.rb +0 -65
- data/lib/rjr/local_node.rb +0 -150
- data/lib/rjr/multi_node.rb +0 -65
- data/lib/rjr/tcp_node.rb +0 -323
- data/lib/rjr/thread_pool2.rb +0 -272
- data/lib/rjr/util.rb +0 -104
- data/lib/rjr/web_node.rb +0 -266
- data/lib/rjr/ws_node.rb +0 -289
- data/lib/rjr.rb +0 -16
- data/specs/amqp_node_spec.rb +0 -31
- data/specs/inspect_spec.rb +0 -60
- data/specs/local_node_spec.rb +0 -43
- data/specs/multi_node_spec.rb +0 -45
- data/specs/tcp_node_spec.rb +0 -33
- data/specs/util_spec.rb +0 -46
- data/specs/web_node_spec.rb +0 -32
- data/specs/ws_node_spec.rb +0 -32
- /data/lib/rjr/{tcp_node2.rb → nodes/tcp2.rb} +0 -0
- /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
|
data/lib/rjr/nodes/ws.rb
ADDED
@@ -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
|
+
|