dripdrop 0.10.0-java

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 (47) hide show
  1. data/.document +5 -0
  2. data/.gitignore +31 -0
  3. data/Gemfile +5 -0
  4. data/LICENSE +20 -0
  5. data/README.md +164 -0
  6. data/Rakefile +16 -0
  7. data/dripdrop.gemspec +37 -0
  8. data/example/agent_test.rb +14 -0
  9. data/example/combined.rb +33 -0
  10. data/example/complex/README +22 -0
  11. data/example/complex/client.rb +20 -0
  12. data/example/complex/server.rb +102 -0
  13. data/example/complex/service.rb +8 -0
  14. data/example/complex/websocket.rb +442 -0
  15. data/example/http.rb +23 -0
  16. data/example/pubsub.rb +29 -0
  17. data/example/pushpull.rb +21 -0
  18. data/example/subclass.rb +54 -0
  19. data/example/xreq_xrep.rb +24 -0
  20. data/js/dripdrop.html +186 -0
  21. data/js/dripdrop.js +107 -0
  22. data/js/qunit.css +155 -0
  23. data/js/qunit.js +1261 -0
  24. data/lib/dripdrop.rb +2 -0
  25. data/lib/dripdrop/agent.rb +40 -0
  26. data/lib/dripdrop/handlers/base.rb +42 -0
  27. data/lib/dripdrop/handlers/http_client.rb +38 -0
  28. data/lib/dripdrop/handlers/http_server.rb +59 -0
  29. data/lib/dripdrop/handlers/mongrel2.rb +163 -0
  30. data/lib/dripdrop/handlers/websocket_server.rb +86 -0
  31. data/lib/dripdrop/handlers/zeromq.rb +300 -0
  32. data/lib/dripdrop/message.rb +190 -0
  33. data/lib/dripdrop/node.rb +351 -0
  34. data/lib/dripdrop/node/nodelet.rb +35 -0
  35. data/lib/dripdrop/version.rb +3 -0
  36. data/spec/gimite-websocket.rb +442 -0
  37. data/spec/message_spec.rb +94 -0
  38. data/spec/node/http_spec.rb +77 -0
  39. data/spec/node/nodelet_spec.rb +67 -0
  40. data/spec/node/routing_spec.rb +67 -0
  41. data/spec/node/websocket_spec.rb +98 -0
  42. data/spec/node/zmq_m2_spec.rb +77 -0
  43. data/spec/node/zmq_pushpull_spec.rb +54 -0
  44. data/spec/node/zmq_xrepxreq_spec.rb +108 -0
  45. data/spec/node_spec.rb +85 -0
  46. data/spec/spec_helper.rb +20 -0
  47. metadata +167 -0
@@ -0,0 +1,190 @@
1
+ require 'rubygems'
2
+
3
+ if RUBY_PLATFORM == 'java'
4
+ require 'json'
5
+ else
6
+ require 'yajl'
7
+ require 'yajl/json_gem'
8
+ end
9
+
10
+ class DripDrop
11
+ class WrongMessageClassError < StandardError; end
12
+
13
+ # DripDrop::Message messages are exchanged between all tiers in the architecture
14
+ # A Message is composed of a name, head, and body, and should be restricted to types that
15
+ # can be readily encoded to JSON.
16
+ # name: Any string
17
+ # head: A hash containing anything (should be used for metadata)
18
+ # body: anything you'd like, it can be null even
19
+ #
20
+ # Hashes, arrays, strings, integers, symbols, and floats are probably what you should stick to.
21
+ # Internally, they're just stored using MsgPack, which is pretty much a super-fast, binary JSON
22
+ #
23
+ # The basic message format is built to mimic HTTP (s/url_path/name/). Why? Because I'm a dumb web developer :)
24
+ # The name is kind of like the URL, its what kind of message this is, but it's a loose definition,
25
+ # use it as you see fit.
26
+ # head should be used for metadata, body for the actual data.
27
+ # These definitions are intentionally loose, because protocols tend to be used loosely.
28
+ class Message
29
+
30
+ attr_accessor :name, :head, :body
31
+
32
+ # Creates a new message.
33
+ # example:
34
+ # DripDrop::Message.new('mymessage', 'head' => {:timestamp => Time.now},
35
+ # :body => {:mykey => :myval, :other_key => ['complex']})
36
+ def initialize(name,extra={})
37
+ raise ArgumentError, "Message names may not be empty or null!" if name.nil? || name.empty?
38
+
39
+ @head = extra[:head] || extra['head'] || {}
40
+ raise ArgumentError, "Invalid head #{@head}. Head must be a hash!" unless @head.is_a?(Hash)
41
+ @head['message_class'] = self.class.to_s
42
+
43
+ @name = name
44
+ @body = extra[:body] || extra['body']
45
+ end
46
+
47
+ # The encoded message, ready to be sent across the wire via ZMQ
48
+ def encoded
49
+ self.to_hash.to_json
50
+ end
51
+
52
+ # (Deprecated) Encodes the hash represntation of the message to JSON
53
+ def json_encoded
54
+ encoded
55
+ end
56
+ # (Deprecated, use json_encoded)
57
+ def encode_json; json_encoded; end
58
+
59
+ # Convert the Message to a hash like:
60
+ # {'name' => @name, 'head' => @head, 'body' => @body}
61
+ def to_hash
62
+ {'name' => @name, 'head' => @head, 'body' => @body}
63
+ end
64
+
65
+ # Build a new Message from a hash that looks like
66
+ # {:name => name, :body => body, 'head' => head}
67
+ def self.from_hash(hash)
68
+ self.new(hash[:name] || hash['name'],
69
+ :head => hash[:head] || hash['head'],
70
+ :body => hash[:body] || hash['body'])
71
+ end
72
+
73
+ def self.create_message(*args)
74
+ case args[0]
75
+ when Hash then self.from_hash(args[0])
76
+ else self.new(args)
77
+ end
78
+ end
79
+
80
+ def self.recreate_message(hash)
81
+ raise ArgumentError, "Message missing head: #{hash.inspect}" unless hash['head']
82
+
83
+ klass = (hash['head'] && hash['head']['message_class']) ? constantize(hash['head']['message_class']) : nil
84
+ if klass && (!(klass == self) && !self.subclasses.include?(klass))
85
+ raise DripDrop::WrongMessageClassError, "Wrong message class '#{klass}', expected '#{self}' for message #{hash.inspect}"
86
+ end
87
+
88
+ klass ? klass.from_hash(hash) : self.from_hash(hash)
89
+ end
90
+
91
+ # Parses an already encoded string
92
+ def self.decode(msg)
93
+ return nil if msg.nil? || msg.empty?
94
+
95
+ decoded = JSON.parse(msg)
96
+ self.recreate_message(decoded)
97
+ end
98
+
99
+ # (Deprecated). Use decode instead
100
+ def self.parse(msg); self.decode(msg) end
101
+
102
+ # (Deprecated) Decodes a string containing a JSON representation of a message
103
+ def self.decode_json(str)
104
+ self.decode(str)
105
+ end
106
+
107
+ def self.constantize(str)
108
+ begin
109
+ str.split('::').inject(Object) {|memo,name|
110
+ memo = memo.const_get(name); memo
111
+ }
112
+ rescue NameError => e
113
+ nil
114
+ end
115
+ end
116
+
117
+ #Used for reconstructing messages
118
+ def self.subclasses(direct = false)
119
+ classes = []
120
+ if direct
121
+ ObjectSpace.each_object(Class) do |c|
122
+ next unless c.superclass == self
123
+ classes << c
124
+ end
125
+ else
126
+ ObjectSpace.each_object(Class) do |c|
127
+ next unless c.ancestors.include?(self) and (c != self)
128
+ classes << c
129
+ end
130
+ end
131
+ classes
132
+ end
133
+ end
134
+
135
+ #Use of this "metaclass" allows for the automatic recognition of the message's
136
+ #base class
137
+ class AutoMessageClass < Message
138
+ def initialize(*args)
139
+ raise "Cannot create an instance of this class - please use create_message class method"
140
+ end
141
+
142
+ class << self
143
+ attr_accessor :message_subclasses
144
+
145
+ DripDrop::AutoMessageClass.message_subclasses = {'DripDrop::Message' => DripDrop::Message}
146
+
147
+ def verify_args(*args)
148
+ head =
149
+ case args[0]
150
+ when Hash
151
+ az = args[0]
152
+ az[:head] || az['head']
153
+ else
154
+ args[1]
155
+ end
156
+ raise ArgumentError, "Invalid head #{head.inspect}. Head must be a hash! (args: #{args.inspect})" unless head.is_a?(Hash)
157
+
158
+ msg_class = head['message_class']
159
+ unless DripDrop::AutoMessageClass.message_subclasses.has_key?(msg_class)
160
+ raise ArgumentError, "Unknown AutoMessage message class #{msg_class}"
161
+ end
162
+
163
+ DripDrop::AutoMessageClass.message_subclasses[msg_class]
164
+ end
165
+
166
+ def create_message(*args)
167
+ klass = verify_args(*args)
168
+ klass.create_message(*args)
169
+ end
170
+
171
+ def recreate_message(*args)
172
+ klass = verify_args(*args)
173
+ klass.recreate_message(*args)
174
+ end
175
+
176
+ def register_subclass(klass)
177
+ DripDrop::AutoMessageClass.message_subclasses[klass.to_s] = klass
178
+ end
179
+ end
180
+ end
181
+
182
+
183
+ #Including this module into your subclass will automatically register the class
184
+ #with AutoMessageClass
185
+ module SubclassedMessage
186
+ def self.included(base)
187
+ DripDrop::AutoMessageClass.register_subclass base
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,351 @@
1
+ require 'rubygems'
2
+ require 'ffi-rzmq'
3
+ require 'eventmachine'
4
+ require 'uri'
5
+ require 'resolv'
6
+ require 'ipaddr'
7
+
8
+ require 'dripdrop/message'
9
+ require 'dripdrop/node/nodelet'
10
+ require 'dripdrop/handlers/base'
11
+ require 'dripdrop/handlers/zeromq'
12
+ require 'dripdrop/handlers/websocket_server'
13
+ require 'dripdrop/handlers/mongrel2'
14
+ require 'dripdrop/handlers/http_client'
15
+
16
+ begin
17
+ require 'dripdrop/handlers/http_server'
18
+ rescue LoadError => e
19
+ $stderr.write "Warning, could not load http server, your probably don't have eventmachine_httpserver installed\n"
20
+ end
21
+
22
+ class DripDrop
23
+ class Node
24
+ ZCTX = ZMQ::Context.new 1
25
+
26
+ attr_reader :zm_reactor, :routing, :nodelets, :run_list
27
+ attr_accessor :debug
28
+
29
+ def initialize(opts={},&block)
30
+ @block = block
31
+ @thread = nil # Thread containing the reactors
32
+ @routing = {} # Routing table
33
+ @run_list = opts['run_list'] || opts[:run_list] || nil #List of nodelets to run
34
+ @run_list = @run_list.map(&:to_sym) if @run_list
35
+ @debug = opts[:debug]
36
+ @recipients_for = {}
37
+ @handler_default_opts = {:debug => @debug}
38
+ @nodelets = {} # Cache of registered nodelets
39
+ @zctx = ZCTX
40
+ end
41
+
42
+ # Starts the reactors and runs the block passed to initialize.
43
+ # This is non-blocking.
44
+ def start
45
+ @thread = Thread.new do
46
+ EM.error_handler {|e| self.class.error_handler e}
47
+ EM.run { action }
48
+ end
49
+ end
50
+
51
+ # Blocking version of start, equivalent to +start+ then +join+
52
+ def start!
53
+ self.start
54
+ self.join
55
+ end
56
+
57
+ # Stops the reactors. If you were blocked on #join, that will unblock.
58
+ def stop
59
+ EM.stop
60
+ end
61
+
62
+ # When subclassing +DripDrop::Node+ you probably want to define this method
63
+ # Otherwise it will attempt to run the @block passed into +DripDrop::Node.new+
64
+ def action
65
+ if @block
66
+ self.instance_eval(&@block)
67
+ else
68
+ raise "Could not start, no block or action specified"
69
+ end
70
+ end
71
+
72
+ # If the reactor has started, this blocks until the thread
73
+ # running the reactor joins. This should block forever
74
+ # unless +stop+ is called.
75
+ def join
76
+ if @thread
77
+ @thread.join
78
+ else
79
+ raise "Can't join on a node that isn't yet started"
80
+ end
81
+ end
82
+
83
+ # Defines a new route. Routes are the recommended way to instantiate
84
+ # handlers. For example:
85
+ #
86
+ # route :stats_pub, :zmq_publish, 'tcp://127.0.0.1:2200', :bind
87
+ # route :stats_sub, :zmq_subscribe, stats_pub.address, :connect
88
+ #
89
+ # Will make the following methods available within the reactor block:
90
+ # stats_pub # A regular zmq_publish handler
91
+ # :stats_sub # A regular zmq_subscribe handler
92
+ #
93
+ # See the docs for +routes_for+ for more info in grouping routes for
94
+ # nodelets and maintaining sanity in larger apps
95
+ def route(name,handler_type,*handler_args)
96
+ route_full(nil, name, handler_type, *handler_args)
97
+ end
98
+
99
+ # Probably not useful for most, apps. This is used internally to
100
+ # create a route for a given nodelet.
101
+ def route_full(nodelet, name, handler_type, *handler_args)
102
+ # If we're in a route_for block, prepend appropriately
103
+ full_name = (nodelet && nodelet.name) ? "#{nodelet.name}_#{name}".to_sym : name
104
+
105
+ handler = self.send(handler_type, *handler_args)
106
+ @routing[full_name] = handler
107
+
108
+ # Define the route name as a singleton method
109
+ (class << self; self; end).class_eval do
110
+ define_method(full_name) { handler }
111
+ end
112
+
113
+ handler
114
+ end
115
+
116
+ # DEPRECATED, will be deleted in 0.8
117
+ def routes_for(nodelet_name,&block)
118
+ $stderr.write "routes_for is now deprecated, use nodelet instead"
119
+ nlet = nodelet(nodelet_name,&block)
120
+ block.call(nlet)
121
+ end
122
+
123
+ # Nodelets are a way of segmenting a DripDrop::Node. This can be used
124
+ # for both organization and deployment. One might want the production
125
+ # deployment of an app to be broken across multiple servers or processes
126
+ # for instance:
127
+ #
128
+ # nodelet :heartbeat do |nlet|
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
134
+ #
135
+ # Nodelets can also be subclassed, for instance:
136
+ #
137
+ # class SpecialNodelet < DripDrop::Node::Nodelet
138
+ # def action
139
+ # nlet.route :ticker, :zmq_publish, 'tcp://127.0.0.1', :bind
140
+ # EM::PeriodicalTimer.new(1) do
141
+ # nlet.ticker.send_message(:name => 'tick')
142
+ # end
143
+ # end
144
+ # end
145
+ #
146
+ # nodelet :heartbeat, SpecialNodelet
147
+ #
148
+ # If you specify a block, Nodelet#action will be ignored and the block
149
+ # will be run
150
+ #
151
+ # Nodelets are made available as instance methods on the current DripDrop::Nodelet
152
+ # Object, so the following works as well:
153
+ #
154
+ # nodelet :mynodelet
155
+ #
156
+ # mynodelet.route :route_name, :zmq_xreq, 'tcp://127.0.0.1:2000', ;bind
157
+ def nodelet(name,klass=Nodelet,*configure_args,&block)
158
+ # If there's a run list, only run nodes in that list
159
+ return nil if @run_list && !@run_list.include?(name.to_sym)
160
+
161
+ nlet = @nodelets[name] ||= klass.new(self,name,*configure_args)
162
+
163
+ # Define a method returning the nodelet in the current node
164
+ unless respond_to?(name)
165
+ (class << self; self; end).class_eval do
166
+ define_method(name) { nlet }
167
+ end
168
+ end
169
+
170
+ if block
171
+ block.call(nlet)
172
+ else
173
+ nlet.action
174
+ end
175
+ nlet
176
+ end
177
+
178
+ def zmq_m2(addresses, opts={}, &block)
179
+ zmq_handler(DripDrop::Mongrel2Handler, [ZMQ::PULL, ZMQ::PUB], addresses, [:connect, :connect], opts)
180
+ end
181
+
182
+ # Creates a ZMQ::SUB type socket. Can only receive messages via +on_recv+.
183
+ # zmq_subscribe sockets have a +topic_filter+ option, which restricts which
184
+ # messages they can receive. It takes a regexp as an option.
185
+ def zmq_subscribe(address,socket_ctype,opts={},&block)
186
+ zmq_handler(DripDrop::ZMQSubHandler,ZMQ::SUB,address,socket_ctype,opts)
187
+ end
188
+
189
+ # Creates a ZMQ::PUB type socket, can only send messages via +send_message+
190
+ def zmq_publish(address,socket_ctype,opts={})
191
+ zmq_handler(DripDrop::ZMQPubHandler,ZMQ::PUB,address,socket_ctype,opts)
192
+ end
193
+
194
+ # Creates a ZMQ::PULL type socket. Can only receive messages via +on_recv+
195
+ def zmq_pull(address,socket_ctype,opts={},&block)
196
+ zmq_handler(DripDrop::ZMQPullHandler,ZMQ::PULL,address,socket_ctype,opts)
197
+ end
198
+
199
+ # Creates a ZMQ::PUSH type socket, can only send messages via +send_message+
200
+ def zmq_push(address,socket_ctype,opts={})
201
+ zmq_handler(DripDrop::ZMQPushHandler,ZMQ::PUSH,address,socket_ctype,opts)
202
+ end
203
+
204
+ # Creates a ZMQ::XREP type socket, both sends and receivesc XREP sockets are extremely
205
+ # powerful, so their functionality is currently limited. XREP sockets in DripDrop can reply
206
+ # to the original source of the message.
207
+ #
208
+ # Receiving with XREP sockets in DripDrop is different than other types of sockets, on_recv
209
+ # passes 2 arguments to its callback, +message+, and +response+. A minimal example is shown below:
210
+ #
211
+ #
212
+ # zmq_xrep(z_addr, :bind).on_recv do |message,response|
213
+ # response.send_message(message)
214
+ # end
215
+ #
216
+ def zmq_xrep(address,socket_ctype,opts={})
217
+ zmq_handler(DripDrop::ZMQXRepHandler,ZMQ::XREP,address,socket_ctype,opts)
218
+ end
219
+
220
+ # See the documentation for +zmq_xrep+ for more info
221
+ def zmq_xreq(address,socket_ctype,opts={})
222
+ zmq_handler(DripDrop::ZMQXReqHandler,ZMQ::XREQ,address,socket_ctype,opts)
223
+ end
224
+
225
+ # Binds an EM websocket server connection to +address+. takes blocks for
226
+ # +on_open+, +on_recv+, +on_close+ and +on_error+.
227
+ #
228
+ # For example +on_recv+ could be used to echo incoming messages thusly:
229
+ # websocket_server(addr).on_open {|conn|
230
+ # ws.send_message(:name => 'ws_open_ack')
231
+ # }.on_recv {|msg,conn|
232
+ # conn.send(msg)
233
+ # }.on_close {|conn|
234
+ # }.on_error {|reason,conn|
235
+ # }
236
+ #
237
+ # The +ws+ object that's passed into the handlers is not
238
+ # the +DripDrop::WebSocketHandler+ object, but an em-websocket object.
239
+ def websocket_server(address,opts={})
240
+ uri = URI.parse(address)
241
+ h_opts = handler_opts_given(opts)
242
+ DripDrop::WebSocketHandler.new(uri,h_opts)
243
+ end
244
+
245
+ # DEPRECATED: Use websocket_server
246
+ def websocket(*args)
247
+ $stderr.write "DripDrop#websocket handler is deprecated, use DripDrop#websocket_server"
248
+ websocket_server(*args)
249
+ end
250
+
251
+ # Starts a new Thin HTTP server listening on address.
252
+ # Can have an +on_recv+ handler that gets passed +msg+ and +response+ args.
253
+ # http_server(addr) {|msg,response| response.send_message(msg)}
254
+ def http_server(address,opts={},&block)
255
+ uri = URI.parse(address)
256
+ h_opts = handler_opts_given(opts)
257
+ DripDrop::HTTPServerHandler.new(uri, h_opts,&block)
258
+ end
259
+
260
+ # An EM HTTP client.
261
+ # Example:
262
+ # client = http_client(addr)
263
+ # client.send_message(:name => 'name', :body => 'hi') do |resp_msg|
264
+ # puts resp_msg.inspect
265
+ # end
266
+ def http_client(address,opts={})
267
+ uri = URI.parse(address)
268
+ h_opts = handler_opts_given(opts)
269
+ DripDrop::HTTPClientHandler.new(uri, h_opts)
270
+ end
271
+
272
+ # An inprocess pub/sub queue that works similarly to EM::Channel,
273
+ # but has manually specified identifiers for subscribers letting you
274
+ # more easily delete subscribers without crazy id tracking.
275
+ #
276
+ # This is useful for situations where you want to broadcast messages across your app,
277
+ # but need a way to properly delete listeners.
278
+ #
279
+ # +dest+ is the name of the pub/sub channel.
280
+ # +data+ is any type of ruby var you'd like to send.
281
+ def send_internal(dest,data)
282
+ return false unless @recipients_for[dest]
283
+ blocks = @recipients_for[dest].values
284
+ return false unless blocks
285
+ blocks.each do |block|
286
+ block.call(data)
287
+ end
288
+ end
289
+
290
+ # Defines a subscriber to the channel +dest+, to receive messages from +send_internal+.
291
+ # +identifier+ is a unique identifier for this receiver.
292
+ # The identifier can be used by +remove_recv_internal+
293
+ def recv_internal(dest,identifier,&block)
294
+ if @recipients_for[dest]
295
+ @recipients_for[dest][identifier] = block
296
+ else
297
+ @recipients_for[dest] = {identifier => block}
298
+ end
299
+ end
300
+
301
+ # Deletes a subscriber to the channel +dest+ previously identified by a
302
+ # reciever created with +recv_internal+
303
+ def remove_recv_internal(dest,identifier)
304
+ return false unless @recipients_for[dest]
305
+ @recipients_for[dest].delete(identifier)
306
+ end
307
+
308
+ # Catch all error handler
309
+ # Global to all DripDrop Nodes
310
+ def self.error_handler(e)
311
+ $stderr.write "#{e.class}: #{e.message}\n\t#{e.backtrace.join("\n\t")}"
312
+ end
313
+
314
+ private
315
+
316
+ def zmq_handler(klass, sock_type, address, socket_ctype, opts={})
317
+ h_opts = handler_opts_given(opts)
318
+
319
+ sock_type = [sock_type].flatten
320
+ address = [address].flatten
321
+ socket_ctype = [socket_ctype].flatten
322
+
323
+ handler = klass.new(h_opts)
324
+
325
+ sock_type.length.times do |index|
326
+ addr_uri = URI.parse(address[index])
327
+
328
+ host_str = addr_uri.host
329
+ #if addr_uri.scheme == 'tcp'
330
+ # host = Resolv.getaddresses(addr_uri.host).first
331
+ # host_addr = Resolv.getaddresses('localhost').map {|a| IPAddr.new(a)}.find {|a| a.ipv4?}
332
+ # host_str = host_addr.ipv6? ? "[#{host_addr.to_s}]" : host_addr.to_s
333
+ #else
334
+ # host_str = addr_uri.host
335
+ #end
336
+
337
+ z_addr = "#{addr_uri.scheme}://#{host_str}:#{addr_uri.port.to_i}"
338
+
339
+ connection = EM::ZeroMQ::Context.new(@zctx).create sock_type[index], socket_ctype[index], z_addr, handler
340
+ handler.add_connection connection
341
+ end
342
+
343
+ handler.post_setup
344
+ handler
345
+ end
346
+
347
+ def handler_opts_given(opts)
348
+ @handler_default_opts.merge(opts)
349
+ end
350
+ end
351
+ end