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.
@@ -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 '/' do
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
- puts "Error in Thin server: #{e.message}\n#{e.backtrace.join("\n")}"
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 => '/', :verb => 'POST',
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
- puts "Could not parse message: #{e.message}"
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
- raise "Can only send Strings, not #{part.class}: #{part}"
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
- case @msg_format
102
- when :raw
103
- @recv_cbak.call(messages)
104
- when :dripdrop
105
- raise "Expected message in one part" if messages.length > 1
106
- body = messages.shift.copy_out_string
107
- @recv_cbak.call(decode_message(body))
108
- else
109
- raise "Unknown message format '#{@msg_format}'"
110
- end
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
- if @msg_format == :dripdrop
131
- unless messages.length == 2
132
- puts "Expected pub/sub message to come in two parts, not #{messages.length}: #{messages.inspect}"
133
- return false
134
- end
135
- topic = messages.shift.copy_out_string
136
- if @topic_filter.nil? || topic.match(@topic_filter)
137
- body = messages.shift.copy_out_string
138
- msg = @recv_cbak.call(decode_message(body))
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
- else
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
- if @msg_format == :dripdrop
180
- identities = messages[0..-2].map {|m| m.copy_out_string}
181
- body = messages.last.copy_out_string
182
- message = decode_message(body)
183
- seq = message.head['_dripdrop/x_seq_counter']
184
- response = ZMQXRepHandler::Response.new(self, identities,seq)
185
- @recv_cbak.call(message,response)
186
- else
187
- super(socket,messages)
188
- end
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
- seq = message.head['_dripdrop/x_seq_counter']
228
- raise "Missing Seq Counter" unless seq
229
- promise = @promises.delete(seq)
230
- promise.call(message) if promise
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
- self.instance_eval(&@block)
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 = @route_prepend ? "#{@route_prepend}_#{name}".to_sym : 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
- # Defines a group of +route+s, to be used as the interface for a +nodelet+
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
- @route_prepend = nodelet_name #This feels ugly. Blech.
108
- block.call
109
- @route_prepend = nil
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. Additionally, by combining nodelets with +routes_for+
116
- # managing routes becomes a little easier.
116
+ # for instance:
117
117
  #
118
- # Nodelets can be used thusly:
119
- # routes_for :heartbeat do
120
- # route :ticker, :zmq_publish, 'tcp://127.0.0.1', :bind
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
- # nodelet :heartbeat do
124
- # zm_reactor.periodical_timer(500) do
125
- # ticker.send_message(:name => 'tick')
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
- def nodelet(name,&block)
128
- nlet_obj = Nodelet.new(name,routing)
129
- block.call(nlet_obj)
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 {|ws|
197
+ # websocket(addr).on_open {|conn|
180
198
  # ws.send_message(:name => 'ws_open_ack')
181
- # }.on_recv {|msg,ws|
182
- # ws.send(msg)
183
- # }.on_close {|ws|
184
- # }.on_error {|ws|
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
- handler = DripDrop::WebSocketHandler.new(uri,h_opts)
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
- handler = DripDrop::HTTPServerHandler.new(uri, h_opts,&block)
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
- handler = DripDrop::HTTPClientHandler.new(uri, h_opts)
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
- @name = name
8
- @routing = {}
9
- routes.each do |route_name,handler|
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,handler)
23
- @routing[name] = handler
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
@@ -1,14 +1,13 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe "http" do
4
- def http_send_messages(to_send,&block)
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(1) do
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
- describe "basic sending and receiving" do
35
- before(:all) do
36
- @sent = []
37
- 10.times {|i| @sent << DripDrop::Message.new("test-#{i}")}
38
- @client_responses = []
39
- @http_info = http_send_messages(@sent) do |sent_message,resp_message|
40
- @client_responses << {:sent_message => sent_message,
41
- :resp_message => resp_message}
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
- it "should receive all sent messages" do
47
- resp_names = @responses.map(&:name).inject(Set.new) {|memo,rn| memo << rn}
48
- @sent.map(&:name).each {|sn| resp_names.should include(sn)}
49
- end
50
-
51
- it "should return to the client as many responses as sent messages" do
52
- @client_responses.length.should == @sent.length
53
- end
54
-
55
- it "should return to the client an identical message to that which was sent" do
56
- @client_responses.each do |resp|
57
- resp[:sent_message].name.should == resp[:resp_message].name
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