dripdrop 0.10.0-java

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