rjr 0.12.2 → 0.15.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|