dripdrop 0.6.0 → 0.7.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/VERSION +1 -1
- data/dripdrop.gemspec +13 -20
- data/example/complex/README +22 -0
- data/example/complex/client.rb +20 -0
- data/example/complex/server.rb +115 -0
- data/example/complex/service.rb +8 -0
- data/example/complex/websocket.rb +442 -0
- data/lib/dripdrop/handlers/http.rb +7 -3
- data/lib/dripdrop/handlers/websockets.rb +2 -2
- data/lib/dripdrop/handlers/zeromq.rb +46 -38
- data/lib/dripdrop/node.rb +63 -43
- data/lib/dripdrop/node/nodelet.rb +17 -16
- data/spec/node/http_spec.rb +38 -26
- data/spec/node/nodelet_spec.rb +25 -15
- data/spec/node/routing_spec.rb +5 -5
- data/spec/node/websocket_spec.rb +1 -1
- data/spec/node_spec.rb +55 -11
- data/spec/spec_helper.rb +1 -1
- metadata +15 -22
- data/example/stats_app/core.rb +0 -113
- data/example/stats_app/public/.sass-cache/b48b4299d80c05f528daf63fe51d85e5e3c10d98/stats.scssc +0 -0
- data/example/stats_app/public/backbone.js +0 -16
- data/example/stats_app/public/build_templates.rb +0 -5
- data/example/stats_app/public/json2.js +0 -482
- data/example/stats_app/public/protovis-r3.2.js +0 -277
- data/example/stats_app/public/stats.css +0 -5
- data/example/stats_app/public/stats.haml +0 -61
- data/example/stats_app/public/stats.html +0 -26
- data/example/stats_app/public/stats.js +0 -113
- data/example/stats_app/public/stats.scss +0 -10
- data/example/stats_app/public/underscore.js +0 -17
@@ -71,13 +71,15 @@ class DripDrop
|
|
71
71
|
begin
|
72
72
|
Thin::Logging.silent = true
|
73
73
|
|
74
|
+
uri_path = @uri.path.empty? ? '/' : @uri.path
|
75
|
+
|
74
76
|
Thin::Server.start(@uri.host, @uri.port) do
|
75
|
-
map
|
77
|
+
map uri_path do
|
76
78
|
run HTTPApp.new(msg_format,&block)
|
77
79
|
end
|
78
80
|
end
|
79
81
|
rescue Exception => e
|
80
|
-
|
82
|
+
$stderr.write "Error in Thin server: #{e.message}\n#{e.backtrace.join("\n")}"
|
81
83
|
end
|
82
84
|
end
|
83
85
|
end
|
@@ -94,9 +96,11 @@ class DripDrop
|
|
94
96
|
def send_message(message,&block)
|
95
97
|
dd_message = dd_messagify(message)
|
96
98
|
if dd_message.class == DripDrop::Message
|
99
|
+
uri_path = @uri.path.empty? ? '/' : @uri.path
|
100
|
+
|
97
101
|
req = EM::Protocols::HttpClient.request(
|
98
102
|
:host => @uri.host, :port => @uri.port,
|
99
|
-
:request =>
|
103
|
+
:request => uri_path, :verb => 'POST',
|
100
104
|
:contenttype => 'application/json',
|
101
105
|
:content => dd_message.encode_json
|
102
106
|
)
|
@@ -16,14 +16,14 @@ class DripDrop
|
|
16
16
|
|
17
17
|
ws.onopen { @onopen_handler.call(dd_conn) if @onopen_handler }
|
18
18
|
ws.onclose { @onclose_handler.call(dd_conn) if @onclose_handler }
|
19
|
-
ws.onerror { @onerror_handler.call(dd_conn) if @onerror_handler }
|
19
|
+
ws.onerror {|reason| @onerror_handler.call(reason, dd_conn) if @onerror_handler }
|
20
20
|
|
21
21
|
ws.onmessage do |message|
|
22
22
|
if @onmessage_handler
|
23
23
|
begin
|
24
24
|
message = DripDrop::Message.decode_json(message) unless @raw
|
25
25
|
rescue StandardError => e
|
26
|
-
|
26
|
+
$stderr.write "Could not parse message: #{e.message}" if @debug
|
27
27
|
end
|
28
28
|
|
29
29
|
@onmessage_handler.call(message,dd_conn)
|
@@ -27,7 +27,7 @@ class DripDrop
|
|
27
27
|
elsif @socket_ctype == :connect
|
28
28
|
socket.connect(@zaddress)
|
29
29
|
else
|
30
|
-
raise "Unsupported socket ctype '#{@socket_ctype}'. Expected :bind or :connect"
|
30
|
+
EM.next_tick { raise "Unsupported socket ctype '#{@socket_ctype}'. Expected :bind or :connect" }
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
@@ -59,7 +59,7 @@ class DripDrop
|
|
59
59
|
if part.class == String
|
60
60
|
socket.send_message_string(part, multipart_flag)
|
61
61
|
else
|
62
|
-
|
62
|
+
$stderr.write "Can only send Strings, not #{part.class}: #{part}" if @debug
|
63
63
|
end
|
64
64
|
end
|
65
65
|
end
|
@@ -98,16 +98,18 @@ class DripDrop
|
|
98
98
|
end
|
99
99
|
|
100
100
|
def on_readable(socket, messages)
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
101
|
+
EM.next_tick {
|
102
|
+
case @msg_format
|
103
|
+
when :raw
|
104
|
+
@recv_cbak.call(messages)
|
105
|
+
when :dripdrop
|
106
|
+
raise "Expected message in one part" if messages.length > 1
|
107
|
+
body = messages.shift.copy_out_string
|
108
|
+
@recv_cbak.call(decode_message(body))
|
109
|
+
else
|
110
|
+
raise "Unknown message format '#{@msg_format}'"
|
111
|
+
end
|
112
|
+
}
|
111
113
|
end
|
112
114
|
end
|
113
115
|
|
@@ -127,19 +129,21 @@ class DripDrop
|
|
127
129
|
end
|
128
130
|
|
129
131
|
def on_readable(socket, messages)
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
132
|
+
EM.next_tick {
|
133
|
+
if @msg_format == :dripdrop
|
134
|
+
unless messages.length == 2
|
135
|
+
puts "Expected pub/sub message to come in two parts, not #{messages.length}: #{messages.inspect}"
|
136
|
+
return false
|
137
|
+
end
|
138
|
+
topic = messages.shift.copy_out_string
|
139
|
+
if @topic_filter.nil? || topic.match(@topic_filter)
|
140
|
+
body = messages.shift.copy_out_string
|
141
|
+
@recv_cbak.call(decode_message(body))
|
142
|
+
end
|
143
|
+
else
|
144
|
+
super(socket,messages)
|
139
145
|
end
|
140
|
-
|
141
|
-
super(socket,messages)
|
142
|
-
end
|
146
|
+
}
|
143
147
|
end
|
144
148
|
end
|
145
149
|
|
@@ -176,16 +180,18 @@ class DripDrop
|
|
176
180
|
end
|
177
181
|
|
178
182
|
def on_readable(socket,messages)
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
183
|
+
EM.next_tick {
|
184
|
+
if @msg_format == :dripdrop
|
185
|
+
identities = messages[0..-2].map {|m| m.copy_out_string}
|
186
|
+
body = messages.last.copy_out_string
|
187
|
+
message = decode_message(body)
|
188
|
+
seq = message.head['_dripdrop/x_seq_counter']
|
189
|
+
response = ZMQXRepHandler::Response.new(self, identities,seq)
|
190
|
+
@recv_cbak.call(message,response)
|
191
|
+
else
|
192
|
+
super(socket,messages)
|
193
|
+
end
|
194
|
+
}
|
189
195
|
end
|
190
196
|
|
191
197
|
def send_message(message,identities,seq)
|
@@ -224,10 +230,12 @@ class DripDrop
|
|
224
230
|
@promises = {}
|
225
231
|
|
226
232
|
self.on_recv do |message|
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
233
|
+
EM.next_tick {
|
234
|
+
seq = message.head['_dripdrop/x_seq_counter']
|
235
|
+
raise "Missing Seq Counter" unless seq
|
236
|
+
promise = @promises.delete(seq)
|
237
|
+
promise.call(message) if promise
|
238
|
+
}
|
231
239
|
end
|
232
240
|
end
|
233
241
|
|
data/lib/dripdrop/node.rb
CHANGED
@@ -13,7 +13,7 @@ require 'dripdrop/handlers/http'
|
|
13
13
|
|
14
14
|
class DripDrop
|
15
15
|
class Node
|
16
|
-
attr_reader :zm_reactor, :routing
|
16
|
+
attr_reader :zm_reactor, :routing, :nodelets
|
17
17
|
attr_accessor :debug
|
18
18
|
|
19
19
|
def initialize(opts={},&block)
|
@@ -24,16 +24,24 @@ class DripDrop
|
|
24
24
|
@debug = opts[:debug]
|
25
25
|
@recipients_for = {}
|
26
26
|
@handler_default_opts = {:debug => @debug}
|
27
|
+
@nodelets = {} # Cache of registered nodelets
|
27
28
|
end
|
28
29
|
|
29
30
|
# Starts the reactors and runs the block passed to initialize.
|
30
31
|
# This is non-blocking.
|
31
32
|
def start
|
32
33
|
@thread = Thread.new do
|
34
|
+
EM.error_handler {|e| self.error_handler e}
|
33
35
|
EM.run do
|
34
36
|
ZM::Reactor.new(:my_reactor).run do |zm_reactor|
|
35
37
|
@zm_reactor = zm_reactor
|
36
|
-
|
38
|
+
if @block
|
39
|
+
self.instance_eval(&@block)
|
40
|
+
elsif self.respond_to?(:action)
|
41
|
+
self.action
|
42
|
+
else
|
43
|
+
raise "Could not start, no block or action specified"
|
44
|
+
end
|
37
45
|
end
|
38
46
|
end
|
39
47
|
end
|
@@ -75,8 +83,14 @@ class DripDrop
|
|
75
83
|
# See the docs for +routes_for+ for more info in grouping routes for
|
76
84
|
# nodelets and maintaining sanity in larger apps
|
77
85
|
def route(name,handler_type,*handler_args)
|
86
|
+
route_full(nil, name, handler_type, *handler_args)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Probably not useful for most, apps. This is used internally to
|
90
|
+
# create a route for a given nodelet.
|
91
|
+
def route_full(nodelet, name, handler_type, *handler_args)
|
78
92
|
# If we're in a route_for block, prepend appropriately
|
79
|
-
full_name =
|
93
|
+
full_name = (nodelet && nodelet.name) ? "#{nodelet.name}_#{name}".to_sym : name
|
80
94
|
|
81
95
|
handler = self.send(handler_type, *handler_args)
|
82
96
|
@routing[full_name] = handler
|
@@ -89,46 +103,50 @@ class DripDrop
|
|
89
103
|
handler
|
90
104
|
end
|
91
105
|
|
92
|
-
#
|
93
|
-
# later on.
|
94
|
-
#
|
95
|
-
# All routes defined with the +route_for+ block will be prepended with the
|
96
|
-
# +nodelet_name+ and an underscore. So, the following routes:
|
97
|
-
#
|
98
|
-
# routes_for :forwarder do
|
99
|
-
# route :input, :zmq_subscribe, 'tcp://127.0.0.1:2200', :bind
|
100
|
-
# route :output, :zmq_publish, f.in.address, :connect
|
101
|
-
# end
|
102
|
-
#
|
103
|
-
# Will yield the routes: +forwarder_input+ and +forwarder_output+ globally.
|
104
|
-
# Within the block scope of the +forwarder+ nodelet however, the routes are additionally
|
105
|
-
# available with their own short names. See the +nodelet+ method for details.
|
106
|
+
# DEPRECATED, will be deleted in 0.8
|
106
107
|
def routes_for(nodelet_name,&block)
|
107
|
-
|
108
|
-
block
|
109
|
-
|
108
|
+
$stderr.write "routes_for is now deprecated, use nodelet instead"
|
109
|
+
nlet = nodelet(nodelet_name,&block)
|
110
|
+
block.call(nlet)
|
110
111
|
end
|
111
112
|
|
112
113
|
# Nodelets are a way of segmenting a DripDrop::Node. This can be used
|
113
114
|
# for both organization and deployment. One might want the production
|
114
115
|
# deployment of an app to be broken across multiple servers or processes
|
115
|
-
# for instance
|
116
|
-
# managing routes becomes a little easier.
|
116
|
+
# for instance:
|
117
117
|
#
|
118
|
-
#
|
119
|
-
#
|
120
|
-
#
|
118
|
+
# nodelet :heartbeat do |nlet|
|
119
|
+
# nlet.route :ticker, :zmq_publish, 'tcp://127.0.0.1', :bind
|
120
|
+
# EM::PeriodicalTimer.new(1) do
|
121
|
+
# nlet.ticker.send_message(:name => 'tick')
|
122
|
+
# end
|
121
123
|
# end
|
122
124
|
#
|
123
|
-
#
|
124
|
-
#
|
125
|
-
#
|
125
|
+
# Nodelets can also be subclassed, for instance:
|
126
|
+
#
|
127
|
+
# class SpecialNodelet < DripDrop::Node::Nodelet
|
128
|
+
# def action
|
129
|
+
# nlet.route :ticker, :zmq_publish, 'tcp://127.0.0.1', :bind
|
130
|
+
# EM::PeriodicalTimer.new(1) do
|
131
|
+
# nlet.ticker.send_message(:name => 'tick')
|
132
|
+
# end
|
133
|
+
# end
|
126
134
|
# end
|
127
|
-
|
128
|
-
|
129
|
-
|
135
|
+
#
|
136
|
+
# nodelet :heartbeat, SpecialNodelet
|
137
|
+
#
|
138
|
+
# If you specify a block, Nodelet#action will be ignored and the block
|
139
|
+
# will be run
|
140
|
+
def nodelet(name,klass=Nodelet,&block)
|
141
|
+
nlet = @nodelets[name] ||= klass.new(self,name,routing)
|
142
|
+
if block
|
143
|
+
block.call(nlet)
|
144
|
+
else
|
145
|
+
nlet.action
|
146
|
+
end
|
147
|
+
nlet
|
130
148
|
end
|
131
|
-
|
149
|
+
|
132
150
|
# Creates a ZMQ::SUB type socket. Can only receive messages via +on_recv+.
|
133
151
|
# zmq_subscribe sockets have a +topic_filter+ option, which restricts which
|
134
152
|
# messages they can receive. It takes a regexp as an option.
|
@@ -176,12 +194,12 @@ class DripDrop
|
|
176
194
|
# +on_open+, +on_recv+, +on_close+ and +on_error+.
|
177
195
|
#
|
178
196
|
# For example +on_recv+ could be used to echo incoming messages thusly:
|
179
|
-
# websocket(addr).on_open {|
|
197
|
+
# websocket(addr).on_open {|conn|
|
180
198
|
# ws.send_message(:name => 'ws_open_ack')
|
181
|
-
# }.on_recv {|msg,
|
182
|
-
#
|
183
|
-
# }.on_close {|
|
184
|
-
# }.on_error {|
|
199
|
+
# }.on_recv {|msg,conn|
|
200
|
+
# conn.send(msg)
|
201
|
+
# }.on_close {|conn|
|
202
|
+
# }.on_error {|reason,conn|
|
185
203
|
# }
|
186
204
|
#
|
187
205
|
# The +ws+ object that's passed into the handlers is not
|
@@ -189,8 +207,7 @@ class DripDrop
|
|
189
207
|
def websocket(address,opts={})
|
190
208
|
uri = URI.parse(address)
|
191
209
|
h_opts = handler_opts_given(opts)
|
192
|
-
|
193
|
-
handler
|
210
|
+
DripDrop::WebSocketHandler.new(uri,h_opts)
|
194
211
|
end
|
195
212
|
|
196
213
|
# Starts a new Thin HTTP server listening on address.
|
@@ -199,8 +216,7 @@ class DripDrop
|
|
199
216
|
def http_server(address,opts={},&block)
|
200
217
|
uri = URI.parse(address)
|
201
218
|
h_opts = handler_opts_given(opts)
|
202
|
-
|
203
|
-
handler
|
219
|
+
DripDrop::HTTPServerHandler.new(uri, h_opts,&block)
|
204
220
|
end
|
205
221
|
|
206
222
|
# An EM HTTP client.
|
@@ -212,8 +228,7 @@ class DripDrop
|
|
212
228
|
def http_client(address,opts={})
|
213
229
|
uri = URI.parse(address)
|
214
230
|
h_opts = handler_opts_given(opts)
|
215
|
-
|
216
|
-
handler
|
231
|
+
DripDrop::HTTPClientHandler.new(uri, h_opts)
|
217
232
|
end
|
218
233
|
|
219
234
|
# An inprocess pub/sub queue that works similarly to EM::Channel,
|
@@ -252,6 +267,11 @@ class DripDrop
|
|
252
267
|
@recipients_for[dest].delete(identifier)
|
253
268
|
end
|
254
269
|
|
270
|
+
# Catch all error handler
|
271
|
+
def error_handler(e)
|
272
|
+
$stderr.write "#{e.class}: #{e.message}\n\t#{e.backtrace.join("\n\t")}"
|
273
|
+
end
|
274
|
+
|
255
275
|
private
|
256
276
|
|
257
277
|
def zmq_handler(klass, zm_sock_type, address, socket_ctype, opts={})
|
@@ -3,27 +3,28 @@ class DripDrop::Node
|
|
3
3
|
class Nodelet
|
4
4
|
attr_accessor :name, :routing
|
5
5
|
|
6
|
-
def initialize(name, routes)
|
7
|
-
@
|
8
|
-
@
|
9
|
-
|
10
|
-
# Copy the original routing table
|
11
|
-
route route_name, handler
|
12
|
-
|
13
|
-
# Define short versions of the local routes for
|
14
|
-
# this nodelet's routing table
|
15
|
-
if (route_name.to_s =~ /^#{name}_(.+)$/)
|
16
|
-
short_name = $1
|
17
|
-
route short_name, handler
|
18
|
-
end
|
19
|
-
end
|
6
|
+
def initialize(ctx, name, routes)
|
7
|
+
@ctx = ctx
|
8
|
+
@name = name
|
9
|
+
@internal_routing = {}
|
20
10
|
end
|
21
11
|
|
22
|
-
def route(name,
|
23
|
-
@
|
12
|
+
def route(name,handler_type,*handler_args)
|
13
|
+
handler = @ctx.route_full(self, name, handler_type, *handler_args)
|
14
|
+
@internal_routing[name] = handler
|
15
|
+
|
24
16
|
(class << self; self; end).class_eval do
|
25
17
|
define_method(name) { handler }
|
26
18
|
end
|
27
19
|
end
|
20
|
+
|
21
|
+
# Check for the method as a route in @ctx, if found
|
22
|
+
# memoize it by defining it as a singleton
|
23
|
+
def method_missing(meth,*args)
|
24
|
+
(class << self; self; end).class_eval do
|
25
|
+
define_method(meth) { @ctx.send(meth,*args) }
|
26
|
+
end
|
27
|
+
self.send(meth,*args)
|
28
|
+
end
|
28
29
|
end
|
29
30
|
end
|
data/spec/node/http_spec.rb
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe "http" do
|
4
|
-
|
4
|
+
|
5
|
+
def http_send_messages(to_send,addr=rand_addr('http'),&block)
|
5
6
|
responses = []
|
6
7
|
client = nil
|
7
8
|
server = nil
|
8
9
|
|
9
|
-
@node = run_reactor(
|
10
|
-
addr = rand_addr
|
11
|
-
|
10
|
+
@node = run_reactor(2) do
|
12
11
|
zmq_subscribe(rand_addr, :bind) do |message|
|
13
12
|
end
|
14
13
|
|
@@ -31,31 +30,44 @@ describe "http" do
|
|
31
30
|
|
32
31
|
{:responses => responses, :handlers => {:server => [server] }}
|
33
32
|
end
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
@client_responses
|
41
|
-
|
33
|
+
|
34
|
+
shared_examples_for "all http nodes" do
|
35
|
+
describe "basic sending and receiving" do
|
36
|
+
before(:all) do
|
37
|
+
@sent = []
|
38
|
+
10.times {|i| @sent << DripDrop::Message.new("test-#{i}")}
|
39
|
+
@client_responses = []
|
40
|
+
@http_info = http_send_messages(@sent,@http_test_addr) do |sent_message,resp_message|
|
41
|
+
@client_responses << {:sent_message => sent_message,
|
42
|
+
:resp_message => resp_message}
|
43
|
+
end
|
44
|
+
@responses = @http_info[:responses]
|
42
45
|
end
|
43
|
-
@responses = @http_info[:responses]
|
44
|
-
end
|
45
46
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
47
|
+
it "should receive all sent messages" do
|
48
|
+
resp_names = @responses.map(&:name).inject(Set.new) {|memo,rn| memo << rn}
|
49
|
+
@sent.map(&:name).each {|sn| resp_names.should include(sn)}
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should return to the client as many responses as sent messages" do
|
53
|
+
@client_responses.length.should == @sent.length
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should return to the client an identical message to that which was sent" do
|
57
|
+
@client_responses.each do |resp|
|
58
|
+
resp[:sent_message].name.should == resp[:resp_message].name
|
59
|
+
end
|
58
60
|
end
|
59
61
|
end
|
60
62
|
end
|
63
|
+
|
64
|
+
describe "http apps using the URL root (/)" do
|
65
|
+
before(:all) { @http_test_addr = rand_addr('http') }
|
66
|
+
it_should_behave_like "all http nodes"
|
67
|
+
end
|
68
|
+
|
69
|
+
describe "http apps using a subdirectory of the URL (/subdir)" do
|
70
|
+
before(:all) { @http_test_addr = rand_addr('http') + '/subdir' }
|
71
|
+
it_should_behave_like "all http nodes"
|
72
|
+
end
|
61
73
|
end
|