dripdrop 0.2.0 → 0.3.0
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/README.md +10 -1
- data/VERSION +1 -1
- data/dripdrop.gemspec +13 -8
- data/example/agent_test.rb +4 -3
- data/example/http.rb +1 -1
- data/example/subclass.rb +54 -0
- data/example/xreq_xrep.rb +2 -2
- data/lib/dripdrop/handlers/http.rb +3 -3
- data/lib/dripdrop/handlers/zeromq.rb +20 -5
- data/lib/dripdrop/message.rb +110 -33
- data/lib/dripdrop/node.rb +55 -40
- data/lib/dripdrop.rb +1 -0
- data/spec/message_spec.rb +99 -0
- data/spec/node/http_spec.rb +67 -0
- data/spec/node/{zmq_pushpull.rb → zmq_pushpull_spec.rb} +4 -5
- data/spec/node/{zmq_xrepxreq.rb → zmq_xrepxreq_spec.rb} +4 -4
- data/spec/node_spec.rb +12 -6
- metadata +13 -8
data/README.md
CHANGED
@@ -45,10 +45,19 @@ Here's an example of the kind of thing DripDrop makes easy, from [examples/pubsu
|
|
45
45
|
|
46
46
|
Note that these aren't regular ZMQ sockets, and that the HTTP server isn't a regular server. They only speak and respond using DripDrop::Message formatted messages. For HTTP/WebSockets it's JSON that looks like {name: 'name', head: {}, body: anything}, for ZeroMQ it means BERT. There is a raw made that you can use for other message formats, but using DripDrop::Messages makes things easier, and for some socket types (like XREQ/XREP) the predefined format is very useful in matching requests to replies.
|
47
47
|
|
48
|
-
Want to see a longer example encapsulating both zmqmachine and eventmachine functionality? Check out [this file](http://github.com/andrewvc/dripdrop-webstats/blob/master/lib/dripdrop-webstats.rb)
|
48
|
+
Want to see a longer example encapsulating both zmqmachine and eventmachine functionality? Check out [this file](http://github.com/andrewvc/dripdrop-webstats/blob/master/lib/dripdrop-webstats.rb).
|
49
|
+
|
50
|
+
#RDoc
|
51
|
+
|
52
|
+
RDocs can be found [here](http://www.rdoc.info/github/andrewvc/dripdrop/master/frames). Most of the interesting stuff is in the [Node](http://www.rdoc.info/github/andrewvc/dripdrop/master/DripDrop/Node) and [Message](http://www.rdoc.info/github/andrewvc/dripdrop/master/DripDrop/Message) classes.
|
49
53
|
|
50
54
|
#How It Works
|
51
55
|
|
52
56
|
DripDrop encapsulates both zmqmachine, and eventmachine. It provides some sane default messaging choices, using [BERT](http://github.com/blog/531-introducing-bert-and-bert-rpc) (A binary, JSON, like serialization format) and JSON for serialization. While zmqmachine and eventmachine APIs, some convoluted ones, the goal here is to smooth over the bumps, and make them play together nicely.
|
53
57
|
|
58
|
+
#Contributors
|
59
|
+
|
60
|
+
Andrew Cholakian: [andrewvc](http://github.com/andrewvc)
|
61
|
+
John W Higgins: [wishdev](http://github.com/wishdev)
|
62
|
+
|
54
63
|
Copyright (c) 2010 Andrew Cholakian. See LICENSE for details.
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.3.0
|
data/dripdrop.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{dripdrop}
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.3.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Andrew Cholakian"]
|
12
|
-
s.date = %q{2010-10-
|
12
|
+
s.date = %q{2010-10-21}
|
13
13
|
s.description = %q{0MQ App stats}
|
14
14
|
s.email = %q{andrew@andrewvc.com}
|
15
15
|
s.extra_rdoc_files = [
|
@@ -29,6 +29,7 @@ Gem::Specification.new do |s|
|
|
29
29
|
"example/http.rb",
|
30
30
|
"example/pubsub.rb",
|
31
31
|
"example/pushpull.rb",
|
32
|
+
"example/subclass.rb",
|
32
33
|
"example/xreq_xrep.rb",
|
33
34
|
"js/dripdrop.html",
|
34
35
|
"js/dripdrop.js",
|
@@ -42,8 +43,10 @@ Gem::Specification.new do |s|
|
|
42
43
|
"lib/dripdrop/handlers/zeromq.rb",
|
43
44
|
"lib/dripdrop/message.rb",
|
44
45
|
"lib/dripdrop/node.rb",
|
45
|
-
"spec/
|
46
|
-
"spec/node/
|
46
|
+
"spec/message_spec.rb",
|
47
|
+
"spec/node/http_spec.rb",
|
48
|
+
"spec/node/zmq_pushpull_spec.rb",
|
49
|
+
"spec/node/zmq_xrepxreq_spec.rb",
|
47
50
|
"spec/node_spec.rb",
|
48
51
|
"spec/spec_helper.rb"
|
49
52
|
]
|
@@ -53,10 +56,12 @@ Gem::Specification.new do |s|
|
|
53
56
|
s.rubygems_version = %q{1.3.7}
|
54
57
|
s.summary = %q{0MQ App Stats}
|
55
58
|
s.test_files = [
|
56
|
-
"spec/
|
57
|
-
"spec/node/
|
58
|
-
"spec/
|
59
|
-
"spec/
|
59
|
+
"spec/spec_helper.rb",
|
60
|
+
"spec/node/http_spec.rb",
|
61
|
+
"spec/node/zmq_xrepxreq_spec.rb",
|
62
|
+
"spec/node/zmq_pushpull_spec.rb",
|
63
|
+
"spec/message_spec.rb",
|
64
|
+
"spec/node_spec.rb"
|
60
65
|
]
|
61
66
|
|
62
67
|
if s.respond_to? :specification_version then
|
data/example/agent_test.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
|
+
##
|
2
|
+
##TODO: This badly needs to be rewritten
|
3
|
+
##
|
4
|
+
|
1
5
|
require 'rubygems'
|
2
6
|
require 'dripdrop/agent'
|
3
7
|
|
4
8
|
agent = DripDrop::Agent.new(ZMQ::PUB,'tcp://127.0.0.1:2900',:connect)
|
5
9
|
|
6
10
|
loop do
|
7
|
-
#Test is the message name, this is the first part of the 0MQ message, used for filtering
|
8
|
-
#at the 0MQ sub socket level, :head is always a hash, :body is freeform
|
9
|
-
#EVERYTHING must be serializable to BERT
|
10
11
|
agent.send_message('test', :body => 'hello', :head => {:key => 'value'})
|
11
12
|
puts "SEND"
|
12
13
|
sleep 1
|
data/example/http.rb
CHANGED
data/example/subclass.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'dripdrop'
|
2
|
+
Thread.abort_on_exception = true
|
3
|
+
|
4
|
+
#We will create a subclass of the Message class
|
5
|
+
#which will add a timestamp to the header every
|
6
|
+
#time it is passed around
|
7
|
+
|
8
|
+
#First our subclass
|
9
|
+
|
10
|
+
class TimestampedMessage < DripDrop::Message
|
11
|
+
def self.create_message(*args)
|
12
|
+
obj = super
|
13
|
+
obj.head[:timestamps] = []
|
14
|
+
obj.head[:timestamps] << Time.now
|
15
|
+
obj
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.recreate_message(*args)
|
19
|
+
obj = super
|
20
|
+
obj.head[:timestamps] << Time.now.to_s
|
21
|
+
obj
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
#Define our handlers
|
26
|
+
#We'll create a batch of 5 push/pull queues them to
|
27
|
+
#show the timestamp array getting larger
|
28
|
+
#as we go along
|
29
|
+
|
30
|
+
DripDrop.default_message_class = TimestampedMessage
|
31
|
+
|
32
|
+
node = DripDrop::Node.new do
|
33
|
+
push1 = zmq_push("tcp://127.0.0.1:2201", :bind)
|
34
|
+
push2 = zmq_push("tcp://127.0.0.1:2202", :bind)
|
35
|
+
|
36
|
+
pull1 = zmq_pull("tcp://127.0.0.1:2201", :connect)
|
37
|
+
pull2 = zmq_pull("tcp://127.0.0.1:2202", :connect)
|
38
|
+
|
39
|
+
pull1.on_recv do |msg|
|
40
|
+
puts "Pull 1 #{msg.head}"
|
41
|
+
sleep 1
|
42
|
+
push2.send_message(msg)
|
43
|
+
end
|
44
|
+
|
45
|
+
pull2.on_recv do |msg|
|
46
|
+
puts "Pull 2 #{msg.head}"
|
47
|
+
end
|
48
|
+
|
49
|
+
push1.send_message(TimestampedMessage.create_message(:name => 'test', :body => "Hello there"))
|
50
|
+
end
|
51
|
+
|
52
|
+
node.start
|
53
|
+
sleep 5
|
54
|
+
node.stop
|
data/example/xreq_xrep.rb
CHANGED
@@ -5,9 +5,9 @@ DripDrop::Node.new do
|
|
5
5
|
z_addr = 'tcp://127.0.0.1:2200'
|
6
6
|
|
7
7
|
rep = zmq_xrep(z_addr, :bind)
|
8
|
-
rep.on_recv do |identities,seq
|
8
|
+
rep.on_recv do |message,identities,seq|
|
9
9
|
puts "REP #{message.body}"
|
10
|
-
rep.send_message(identities,seq
|
10
|
+
rep.send_message(message,identities,seq)
|
11
11
|
end
|
12
12
|
|
13
13
|
req = zmq_xreq(z_addr, :connect)
|
@@ -46,7 +46,7 @@ class DripDrop
|
|
46
46
|
when :dripdrop_json
|
47
47
|
msg = DripDrop::Message.decode_json(env['rack.input'].read)
|
48
48
|
msg.head[:http_env] = env
|
49
|
-
@recv_cbak.call(body
|
49
|
+
@recv_cbak.call(msg,body)
|
50
50
|
else
|
51
51
|
raise "Unsupported message type #{@msg_format}"
|
52
52
|
end
|
@@ -69,8 +69,8 @@ class DripDrop
|
|
69
69
|
#Rack middleware was not meant to be used this way...
|
70
70
|
#Thin's error handling only rescues stuff w/o a backtrace
|
71
71
|
begin
|
72
|
-
Thin::Logging.debug =
|
73
|
-
Thin::Logging.trace =
|
72
|
+
Thin::Logging.debug = false
|
73
|
+
Thin::Logging.trace = false
|
74
74
|
Thin::Server.start(@address.host, @address.port) do
|
75
75
|
map '/' do
|
76
76
|
run HTTPApp.new(msg_format,&block)
|
@@ -1,6 +1,13 @@
|
|
1
1
|
require 'ffi-rzmq'
|
2
2
|
|
3
3
|
class DripDrop
|
4
|
+
#Setup the default message class handler first
|
5
|
+
class << self
|
6
|
+
attr_accessor :default_message_class
|
7
|
+
|
8
|
+
DripDrop.default_message_class = DripDrop::Message
|
9
|
+
end
|
10
|
+
|
4
11
|
class ZMQBaseHandler
|
5
12
|
attr_reader :address, :socket_ctype, :socket
|
6
13
|
|
@@ -9,6 +16,7 @@ class DripDrop
|
|
9
16
|
@zm_reactor = zm_reactor
|
10
17
|
@socket_ctype = socket_ctype # :bind or :connect
|
11
18
|
@debug = opts[:debug] # TODO: Start actually using this
|
19
|
+
@opts = opts
|
12
20
|
end
|
13
21
|
|
14
22
|
def on_attach(socket)
|
@@ -91,8 +99,15 @@ class DripDrop
|
|
91
99
|
end
|
92
100
|
|
93
101
|
module ZMQReadableHandler
|
102
|
+
attr_accessor :message_class
|
103
|
+
|
94
104
|
def initialize(*args)
|
95
105
|
super(*args)
|
106
|
+
@message_class = @opts[:msg_class] || DripDrop.default_message_class
|
107
|
+
end
|
108
|
+
|
109
|
+
def decode_message(msg)
|
110
|
+
@message_class.decode(msg)
|
96
111
|
end
|
97
112
|
|
98
113
|
def on_readable(socket, messages)
|
@@ -102,7 +117,7 @@ class DripDrop
|
|
102
117
|
when :dripdrop
|
103
118
|
raise "Expected message in one part" if messages.length > 1
|
104
119
|
body = messages.shift.copy_out_string
|
105
|
-
@recv_cbak.call(
|
120
|
+
@recv_cbak.call(decode_message(body))
|
106
121
|
else
|
107
122
|
raise "Unknown message format '#{@msg_format}'"
|
108
123
|
end
|
@@ -129,7 +144,7 @@ class DripDrop
|
|
129
144
|
topic = messages.shift.copy_out_string
|
130
145
|
if @topic_filter.nil? || topic.match(@topic_filter)
|
131
146
|
body = messages.shift.copy_out_string
|
132
|
-
msg = @recv_cbak.call(
|
147
|
+
msg = @recv_cbak.call(decode_message(body))
|
133
148
|
end
|
134
149
|
else
|
135
150
|
super(socket,messages)
|
@@ -173,15 +188,15 @@ class DripDrop
|
|
173
188
|
if @msg_format == :dripdrop
|
174
189
|
identities = messages[0..-2].map {|m| m.copy_out_string}
|
175
190
|
body = messages.last.copy_out_string
|
176
|
-
message =
|
191
|
+
message = decode_message(body)
|
177
192
|
seq = message.head['_dripdrop/x_seq_counter']
|
178
|
-
@recv_cbak.call(identities,seq
|
193
|
+
@recv_cbak.call(message,identities,seq)
|
179
194
|
else
|
180
195
|
super(socket,messages)
|
181
196
|
end
|
182
197
|
end
|
183
198
|
|
184
|
-
def send_message(identities,seq
|
199
|
+
def send_message(message,identities,seq)
|
185
200
|
if message.is_a?(DripDrop::Message)
|
186
201
|
message.head['_dripdrop/x_seq_counter'] = seq
|
187
202
|
super(identities + [message.encoded])
|
data/lib/dripdrop/message.rb
CHANGED
@@ -3,61 +3,93 @@ require 'bert'
|
|
3
3
|
require 'json'
|
4
4
|
|
5
5
|
class DripDrop
|
6
|
-
#DripDrop::Message messages are exchanged between all tiers in the architecture
|
7
|
-
#A Message is composed of a name, head, and body, and should be restricted to types that
|
8
|
-
#can be readily encoded to JSON
|
9
|
-
#
|
6
|
+
# DripDrop::Message messages are exchanged between all tiers in the architecture
|
7
|
+
# A Message is composed of a name, head, and body, and should be restricted to types that
|
8
|
+
# can be readily encoded to JSON.
|
9
|
+
# name: Any string
|
10
|
+
# head: A hash containing anything (should be used for metadata)
|
11
|
+
# body: anything you'd like, it can be null even
|
10
12
|
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
13
|
+
# Hashes, arrays, strings, integers, symbols, and floats are probably what you should stick to.
|
14
|
+
# Internally, they're just stored as BERT, which is great because if you don't use JSON
|
15
|
+
# things like symbols and binary data are transmitted more efficiently and transparently.
|
16
|
+
#
|
17
|
+
# The basic message format is built to mimic HTTP (s/url_path/name/). Why? Because I'm a dumb web developer :)
|
18
|
+
# The name is kind of like the URL, its what kind of message this is, but it's a loose definition,
|
19
|
+
# use it as you see fit.
|
20
|
+
# head should be used for metadata, body for the actual data.
|
21
|
+
# These definitions are intentionally loose, because protocols tend to be used loosely.
|
15
22
|
class Message
|
16
23
|
attr_accessor :name, :head, :body
|
17
|
-
|
18
|
-
#
|
19
|
-
#example:
|
20
|
-
#
|
21
|
-
#
|
24
|
+
|
25
|
+
# Creates a new message.
|
26
|
+
# example:
|
27
|
+
# DripDrop::Message.new('mymessage', :head => {:timestamp => Time.now},
|
28
|
+
# :body => {:mykey => :myval, :other_key => ['complex']})
|
22
29
|
def initialize(name,extra={})
|
23
|
-
raise "
|
24
|
-
|
30
|
+
raise ArgumentError, "Message names may not be empty or null!" if name.nil? || name.empty?
|
31
|
+
|
25
32
|
@head = extra[:head] || {}
|
26
|
-
raise "
|
27
|
-
|
33
|
+
raise ArgumentError, "Invalid head #{@head}. Head must be a hash!" unless @head.is_a?(Hash)
|
34
|
+
@head[:msg_class] = self.class.to_s
|
35
|
+
|
28
36
|
@name = name
|
29
37
|
@body = extra[:body]
|
30
38
|
end
|
31
|
-
|
32
|
-
#The encoded message, ready to be sent across the wire via ZMQ
|
39
|
+
|
40
|
+
# The encoded message, ready to be sent across the wire via ZMQ
|
33
41
|
def encoded
|
34
42
|
BERT.encode(self.to_hash)
|
35
43
|
end
|
36
|
-
|
37
|
-
|
44
|
+
|
45
|
+
# Encodes the hash represntation of the message to JSON
|
46
|
+
def json_encoded
|
38
47
|
self.to_hash.to_json
|
39
48
|
end
|
49
|
+
# (Deprecated, use json_encoded)
|
50
|
+
def encode_json; json_encoded; end
|
40
51
|
|
41
|
-
#Convert the Message to a hash like:
|
42
|
-
#{:name => @name, :head => @head, :body => @body}
|
52
|
+
# Convert the Message to a hash like:
|
53
|
+
# {:name => @name, :head => @head, :body => @body}
|
43
54
|
def to_hash
|
44
55
|
{:name => @name, :head => @head, :body => @body}
|
45
56
|
end
|
46
57
|
|
47
|
-
#
|
48
|
-
|
49
|
-
def self.
|
58
|
+
# Build a new Message from a hash that looks like
|
59
|
+
# {:name => name, :body => body, :head => head}
|
60
|
+
def self.from_hash(hash)
|
61
|
+
self.new(hash[:name],:head => hash[:head], :body => hash[:body])
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.create_message(*args)
|
65
|
+
case args[0]
|
66
|
+
when Hash then self.from_hash(args[0])
|
67
|
+
else self.new(args)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.recreate_message(hash)
|
72
|
+
raise ArgumentError, "Wrong message class #{hash[:head][:msg_class]} for #{self.to_s}" unless hash[:head][:msg_class] == self.to_s
|
73
|
+
self.from_hash(hash)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Parses an already encoded string
|
77
|
+
def self.decode(msg)
|
50
78
|
return nil if msg.nil? || msg.empty?
|
51
79
|
#This makes parsing ZMQ messages less painful, even if its ugly here
|
52
80
|
#We check the class name as a string in case we don't have ZMQ loaded
|
53
81
|
if msg.class.to_s == 'ZMQ::Message'
|
54
|
-
msg = msg.copy_out_string
|
82
|
+
msg = msg.copy_out_string
|
55
83
|
return nil if msg.empty?
|
56
84
|
end
|
57
85
|
decoded = BERT.decode(msg)
|
58
|
-
self.
|
86
|
+
self.recreate_message(decoded)
|
59
87
|
end
|
60
88
|
|
89
|
+
# (Deprecated). Use decode instead
|
90
|
+
def self.parse(msg); self.decode(msg) end
|
91
|
+
|
92
|
+
# Decodes a string containing a JSON representation of a message
|
61
93
|
def self.decode_json(str)
|
62
94
|
begin
|
63
95
|
json_hash = JSON.parse(str)
|
@@ -67,12 +99,57 @@ class DripDrop
|
|
67
99
|
end
|
68
100
|
self.new(json_hash['name'], :head => json_hash['head'], :body => json_hash['body'])
|
69
101
|
end
|
102
|
+
end
|
103
|
+
|
104
|
+
#Use of this "metaclass" allows for the automatic recognition of the message's
|
105
|
+
#base class
|
106
|
+
class AutoMessageClass < Message
|
107
|
+
def initialize(*args)
|
108
|
+
raise "Cannot create an instance of this class - please use create_message class method"
|
109
|
+
end
|
110
|
+
|
111
|
+
class << self
|
112
|
+
attr_accessor :message_subclasses
|
113
|
+
|
114
|
+
DripDrop::AutoMessageClass.message_subclasses = {'DripDrop::Message' => DripDrop::Message}
|
115
|
+
|
116
|
+
def verify_args(*args)
|
117
|
+
head =
|
118
|
+
case args[0]
|
119
|
+
when Hash then args[0][:head]
|
120
|
+
else args[1]
|
121
|
+
end
|
122
|
+
raise ArgumentError, "Invalid head #{head}. Head must be a hash!" unless head.is_a?(Hash)
|
123
|
+
|
124
|
+
msg_class = head[:msg_class]
|
125
|
+
unless DripDrop::AutoMessageClass.message_subclasses.has_key?(msg_class)
|
126
|
+
raise ArgumentError, "Unknown AutoMessage message class #{msg_class}"
|
127
|
+
end
|
128
|
+
|
129
|
+
DripDrop::AutoMessageClass.message_subclasses[msg_class]
|
130
|
+
end
|
131
|
+
|
132
|
+
def create_message(*args)
|
133
|
+
klass = verify_args(*args)
|
134
|
+
klass.create_message(*args)
|
135
|
+
end
|
136
|
+
|
137
|
+
def recreate_message(*args)
|
138
|
+
klass = verify_args(*args)
|
139
|
+
klass.recreate_message(*args)
|
140
|
+
end
|
141
|
+
|
142
|
+
def register_subclass(klass)
|
143
|
+
DripDrop::AutoMessageClass.message_subclasses[klass.to_s] = klass
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
70
147
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
def
|
75
|
-
|
148
|
+
#Including this module into your subclass will automatically register the class
|
149
|
+
#with AutoMessageClass
|
150
|
+
module SubclassedMessage
|
151
|
+
def self.included(base)
|
152
|
+
DripDrop::AutoMessageClass.register_subclass base
|
76
153
|
end
|
77
154
|
end
|
78
155
|
end
|
data/lib/dripdrop/node.rb
CHANGED
@@ -24,6 +24,8 @@ class DripDrop
|
|
24
24
|
@thread = nil
|
25
25
|
end
|
26
26
|
|
27
|
+
# Starts the reactors and runs the block passed to initialize.
|
28
|
+
# This is non-blocking.
|
27
29
|
def start
|
28
30
|
@thread = Thread.new do
|
29
31
|
EM.run do
|
@@ -35,6 +37,9 @@ class DripDrop
|
|
35
37
|
end
|
36
38
|
end
|
37
39
|
|
40
|
+
# If the reactor has started, this blocks until the thread
|
41
|
+
# running the reactor joins. This should block forever
|
42
|
+
# unless +stop+ is called.
|
38
43
|
def join
|
39
44
|
if @thread
|
40
45
|
@thread.join
|
@@ -43,55 +48,38 @@ class DripDrop
|
|
43
48
|
end
|
44
49
|
end
|
45
50
|
|
46
|
-
#Blocking version of start, equivalent to +start+ then +join+
|
51
|
+
# Blocking version of start, equivalent to +start+ then +join+
|
47
52
|
def start!
|
48
53
|
self.start
|
49
54
|
self.join
|
50
55
|
end
|
51
56
|
|
57
|
+
# Stops the reactors. If you were blocked on #join, that will unblock.
|
52
58
|
def stop
|
53
59
|
@zm_reactor.stop
|
54
60
|
EM.stop
|
55
61
|
end
|
56
62
|
|
57
|
-
#TODO: All these need to be majorly DRYed up
|
58
|
-
|
59
63
|
# Creates a ZMQ::SUB type socket. Can only receive messages via +on_recv+
|
60
64
|
def zmq_subscribe(address,socket_ctype,opts={},&block)
|
61
|
-
|
62
|
-
h_opts = handler_opts_given(opts)
|
63
|
-
handler = DripDrop::ZMQSubHandler.new(zm_addr,@zm_reactor,socket_ctype,h_opts)
|
64
|
-
@zm_reactor.sub_socket(handler)
|
65
|
-
handler
|
65
|
+
zmq_handler(DripDrop::ZMQSubHandler,:sub_socket,address,socket_ctype,opts={})
|
66
66
|
end
|
67
67
|
|
68
68
|
# Creates a ZMQ::PUB type socket, can only send messages via +send_message+
|
69
69
|
def zmq_publish(address,socket_ctype,opts={})
|
70
|
-
|
71
|
-
h_opts = handler_opts_given(opts)
|
72
|
-
handler = DripDrop::ZMQPubHandler.new(zm_addr,@zm_reactor,socket_ctype,h_opts)
|
73
|
-
@zm_reactor.pub_socket(handler)
|
74
|
-
handler
|
70
|
+
zmq_handler(DripDrop::ZMQPubHandler,:pub_socket,address,socket_ctype,opts={})
|
75
71
|
end
|
76
72
|
|
77
73
|
# Creates a ZMQ::PULL type socket. Can only receive messages via +on_recv+
|
78
74
|
def zmq_pull(address,socket_ctype,opts={},&block)
|
79
|
-
|
80
|
-
h_opts = handler_opts_given(opts)
|
81
|
-
handler = DripDrop::ZMQPullHandler.new(zm_addr,@zm_reactor,socket_ctype,h_opts)
|
82
|
-
@zm_reactor.pull_socket(handler)
|
83
|
-
handler
|
75
|
+
zmq_handler(DripDrop::ZMQPullHandler,:pull_socket,address,socket_ctype,opts={})
|
84
76
|
end
|
85
77
|
|
86
78
|
# Creates a ZMQ::PUSH type socket, can only send messages via +send_message+
|
87
79
|
def zmq_push(address,socket_ctype,opts={})
|
88
|
-
|
89
|
-
h_opts = handler_opts_given(opts)
|
90
|
-
handler = DripDrop::ZMQPushHandler.new(zm_addr,@zm_reactor,socket_ctype,h_opts)
|
91
|
-
@zm_reactor.push_socket(handler)
|
92
|
-
handler
|
80
|
+
zmq_handler(DripDrop::ZMQPushHandler,:push_socket,address,socket_ctype,opts={})
|
93
81
|
end
|
94
|
-
|
82
|
+
|
95
83
|
# Creates a ZMQ::XREP type socket, both sends and receivesc XREP sockets are extremely
|
96
84
|
# powerful, so their functionality is currently limited. XREP sockets in DripDrop can reply
|
97
85
|
# to the original source of the message.
|
@@ -104,29 +92,32 @@ class DripDrop
|
|
104
92
|
# To reply from an xrep handler, be sure to call send messages with the same +identities+ and +seq+
|
105
93
|
# arguments that +on_recv+ had. So, send_message takes +identities+, +seq+, and +message+.
|
106
94
|
def zmq_xrep(address,socket_ctype,opts={})
|
107
|
-
|
108
|
-
h_opts = handler_opts_given(opts)
|
109
|
-
handler = DripDrop::ZMQXRepHandler.new(zm_addr,@zm_reactor,socket_ctype,h_opts)
|
110
|
-
@zm_reactor.xrep_socket(handler)
|
111
|
-
handler
|
95
|
+
zmq_handler(DripDrop::ZMQXRepHandler,:xrep_socket,address,socket_ctype,opts={})
|
112
96
|
end
|
113
97
|
|
114
98
|
# See the documentation for +zmq_xrep+ for more info
|
115
99
|
def zmq_xreq(address,socket_ctype,opts={})
|
116
|
-
|
117
|
-
h_opts = handler_opts_given(opts)
|
118
|
-
handler = DripDrop::ZMQXReqHandler.new(zm_addr,@zm_reactor,socket_ctype,h_opts)
|
119
|
-
@zm_reactor.xreq_socket(handler)
|
120
|
-
handler
|
100
|
+
zmq_handler(DripDrop::ZMQXReqHandler,:xreq_socket,address,socket_ctype,opts={})
|
121
101
|
end
|
122
|
-
|
123
|
-
|
102
|
+
|
103
|
+
# Binds an EM websocket connection to +address+. takes blocks for
|
104
|
+
# +on_open+, +on_recv+, +on_close+ and +on_error+.
|
105
|
+
#
|
106
|
+
# For example +on_recv+ could be used to echo incoming messages thusly:
|
107
|
+
# websocket(addr).on_recv {|msg,websocket| ws.send(msg)}
|
108
|
+
#
|
109
|
+
# All other events only receive the +websocket+ object, which corresponds
|
110
|
+
# not to the +DripDrop::WebSocketHandler+ object, but to an em-websocket object.
|
111
|
+
def websocket(address,opts={})
|
124
112
|
uri = URI.parse(address)
|
125
113
|
h_opts = handler_opts_given(opts)
|
126
114
|
handler = DripDrop::WebSocketHandler.new(uri,h_opts)
|
127
115
|
handler
|
128
116
|
end
|
129
117
|
|
118
|
+
# Starts a new Thin HTTP server listening on address.
|
119
|
+
# Can have an +on_recv+ handler that gets passed a single +response+ arg.
|
120
|
+
# http_server(addr) {|response,msg| response.send_message(msg)}
|
130
121
|
def http_server(address,opts={},&block)
|
131
122
|
uri = URI.parse(address)
|
132
123
|
h_opts = handler_opts_given(opts)
|
@@ -134,6 +125,12 @@ class DripDrop
|
|
134
125
|
handler
|
135
126
|
end
|
136
127
|
|
128
|
+
# An EM HTTP client.
|
129
|
+
# Example:
|
130
|
+
# client = http_client(addr)
|
131
|
+
# client.send_message(:name => 'name', :body => 'hi') do |resp_msg|
|
132
|
+
# puts resp_msg.inspect
|
133
|
+
# end
|
137
134
|
def http_client(address,opts={})
|
138
135
|
uri = URI.parse(address)
|
139
136
|
h_opts = handler_opts_given(opts)
|
@@ -141,6 +138,15 @@ class DripDrop
|
|
141
138
|
handler
|
142
139
|
end
|
143
140
|
|
141
|
+
# An inprocess pub/sub queue that works similarly to EM::Channel,
|
142
|
+
# but has manually specified identifiers for subscribers letting you
|
143
|
+
# more easily delete subscribers without crazy id tracking.
|
144
|
+
#
|
145
|
+
# This is useful for situations where you want to broadcast messages across your app,
|
146
|
+
# but need a way to properly delete listeners.
|
147
|
+
#
|
148
|
+
# +dest+ is the name of the pub/sub channel.
|
149
|
+
# +data+ is any type of ruby var you'd like to send.
|
144
150
|
def send_internal(dest,data)
|
145
151
|
return false unless @recipients_for[dest]
|
146
152
|
blocks = @recipients_for[dest].values
|
@@ -150,6 +156,9 @@ class DripDrop
|
|
150
156
|
end
|
151
157
|
end
|
152
158
|
|
159
|
+
# Defines a subscriber to the channel +dest+, to receive messages from +send_internal+.
|
160
|
+
# +identifier+ is a unique identifier for this receiver.
|
161
|
+
# The identifier can be used by +remove_recv_internal+
|
153
162
|
def recv_internal(dest,identifier,&block)
|
154
163
|
if @recipients_for[dest]
|
155
164
|
@recipients_for[dest][identifier] = block
|
@@ -158,6 +167,8 @@ class DripDrop
|
|
158
167
|
end
|
159
168
|
end
|
160
169
|
|
170
|
+
# Deletes a subscriber to the channel +dest+ previously identified by a
|
171
|
+
# reciever created with +recv_internal+
|
161
172
|
def remove_recv_internal(dest,identifier)
|
162
173
|
return false unless @recipients_for[dest]
|
163
174
|
@recipients_for[dest].delete(identifier)
|
@@ -165,10 +176,14 @@ class DripDrop
|
|
165
176
|
|
166
177
|
private
|
167
178
|
|
168
|
-
def
|
169
|
-
addr_uri = URI.parse(
|
170
|
-
ZM::Address.new(addr_uri.host,addr_uri.port.to_i,addr_uri.scheme.to_sym)
|
171
|
-
|
179
|
+
def zmq_handler(klass, zm_sock_type, address, socket_ctype, opts={})
|
180
|
+
addr_uri = URI.parse(address)
|
181
|
+
zm_addr = ZM::Address.new(addr_uri.host,addr_uri.port.to_i,addr_uri.scheme.to_sym)
|
182
|
+
h_opts = handler_opts_given(opts)
|
183
|
+
handler = klass.new(zm_addr,@zm_reactor,socket_ctype,h_opts)
|
184
|
+
@zm_reactor.send(zm_sock_type,handler)
|
185
|
+
handler
|
186
|
+
end
|
172
187
|
|
173
188
|
def handler_opts_given(opts)
|
174
189
|
@handler_default_opts.merge(opts)
|
data/lib/dripdrop.rb
CHANGED
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class SpecMessageClass < DripDrop::Message
|
4
|
+
include DripDrop::SubclassedMessage
|
5
|
+
end
|
6
|
+
|
7
|
+
describe DripDrop::Message do
|
8
|
+
describe "basic message" do
|
9
|
+
def create_basic
|
10
|
+
attrs = {
|
11
|
+
:name => 'test',
|
12
|
+
:head => {:foo => :bar},
|
13
|
+
:body => [:foo, :bar, :baz]
|
14
|
+
}
|
15
|
+
message = DripDrop::Message.new(attrs[:name],:head => attrs[:head],
|
16
|
+
:body => attrs[:body])
|
17
|
+
[message, attrs]
|
18
|
+
end
|
19
|
+
it "should create a basic message without raising an exception" do
|
20
|
+
lambda {
|
21
|
+
message, attrs = create_basic
|
22
|
+
}.should_not raise_exception
|
23
|
+
end
|
24
|
+
describe "with minimal attributes" do
|
25
|
+
it "should create a message with only a name" do
|
26
|
+
lambda {
|
27
|
+
DripDrop::Message.new('nameonly')
|
28
|
+
}.should_not raise_exception
|
29
|
+
end
|
30
|
+
it "should set the head to a single key hash containing message class if nil provided" do
|
31
|
+
DripDrop::Message.new('nilhead', :head => nil).head.should == {:msg_class => 'DripDrop::Message'}
|
32
|
+
end
|
33
|
+
it "should raise an exception if a non-hash, non-nil head is provided" do
|
34
|
+
lambda {
|
35
|
+
DripDrop::Message.new('arrhead', :head => [])
|
36
|
+
}.should raise_exception(ArgumentError)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
describe "encoding" do
|
40
|
+
before(:all) do
|
41
|
+
@message, @attrs = create_basic
|
42
|
+
end
|
43
|
+
it "should encode to valid BERT hash without error" do
|
44
|
+
enc = @message.encoded
|
45
|
+
enc.should be_a(String)
|
46
|
+
BERT.decode(enc).should be_a(Hash)
|
47
|
+
end
|
48
|
+
it "should decode encoded messages without errors" do
|
49
|
+
DripDrop::Message.decode(@message.encoded).should be_a(DripDrop::Message)
|
50
|
+
end
|
51
|
+
it "should encode to valid JSON without error" do
|
52
|
+
enc = @message.json_encoded
|
53
|
+
enc.should be_a(String)
|
54
|
+
JSON.parse(enc).should be_a(Hash)
|
55
|
+
end
|
56
|
+
it "should decode JSON encoded messages without errors" do
|
57
|
+
DripDrop::Message.decode_json(@message.json_encoded).should be_a(DripDrop::Message)
|
58
|
+
end
|
59
|
+
it "should convert messages to Hash representations" do
|
60
|
+
@message.to_hash.should be_a(Hash)
|
61
|
+
end
|
62
|
+
it "should be able to turn hash representations back into Message objs" do
|
63
|
+
DripDrop::Message.from_hash(@message.to_hash).should be_a(DripDrop::Message)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
describe "subclassing" do
|
67
|
+
def create_auto_message
|
68
|
+
attrs = {
|
69
|
+
:name => 'test',
|
70
|
+
:head => {:foo => :bar, :msg_class => 'SpecMessageClass'},
|
71
|
+
:body => [:foo, :bar, :baz]
|
72
|
+
}
|
73
|
+
|
74
|
+
message = DripDrop::AutoMessageClass.create_message(attrs)
|
75
|
+
|
76
|
+
[message, attrs]
|
77
|
+
end
|
78
|
+
before(:all) do
|
79
|
+
@message, @attrs = create_auto_message
|
80
|
+
end
|
81
|
+
it "should be added to the subclass message class hash if SubclassedMessage included" do
|
82
|
+
DripDrop::AutoMessageClass.message_subclasses.should include('SpecMessageClass' => SpecMessageClass)
|
83
|
+
end
|
84
|
+
it "should throw an exception if we try to recreate a message of the wrong class" do
|
85
|
+
msg = DripDrop::Message.new('test')
|
86
|
+
lambda{SpecMessageClass.recreate_message(msg.to_hash)}.should raise_exception
|
87
|
+
end
|
88
|
+
|
89
|
+
describe "DripDrop::AutoMessageClass" do
|
90
|
+
it "should create a properly classed message based on head[:msg_class]" do
|
91
|
+
@message.should be_a(SpecMessageClass)
|
92
|
+
end
|
93
|
+
it "should recreate a message based on head[:msg_class]" do
|
94
|
+
DripDrop::AutoMessageClass.recreate_message(@message.to_hash).should be_a(SpecMessageClass)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "http" do
|
4
|
+
def http_send_messages(to_send,&block)
|
5
|
+
responses = []
|
6
|
+
client = nil
|
7
|
+
server = nil
|
8
|
+
|
9
|
+
@ddn = DripDrop::Node.new do
|
10
|
+
addr = rand_addr
|
11
|
+
|
12
|
+
zmq_subscribe(rand_addr, :bind) do |message|
|
13
|
+
end
|
14
|
+
|
15
|
+
client = http_client(addr)
|
16
|
+
|
17
|
+
server = http_server(addr).on_recv do |message,response|
|
18
|
+
$stdout.flush
|
19
|
+
responses << message
|
20
|
+
response.send_message(message)
|
21
|
+
end
|
22
|
+
|
23
|
+
to_send.each do |message|
|
24
|
+
EM::next_tick do
|
25
|
+
http_client(addr).send_message(message) do |resp_message|
|
26
|
+
block.call(message,resp_message)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
@ddn.start
|
33
|
+
sleep 0.1
|
34
|
+
@ddn.stop
|
35
|
+
|
36
|
+
{:responses => responses, :handlers => {:server => [server] }}
|
37
|
+
end
|
38
|
+
describe "basic sending and receiving" do
|
39
|
+
before(:all) do
|
40
|
+
@sent = []
|
41
|
+
10.times {|i| @sent << DripDrop::Message.new("test-#{i}")}
|
42
|
+
@client_responses = []
|
43
|
+
http_info = http_send_messages(@sent) do |sent_message,resp_message|
|
44
|
+
@client_responses << {:sent_message => sent_message,
|
45
|
+
:resp_message => resp_message}
|
46
|
+
end
|
47
|
+
@responses = http_info[:responses]
|
48
|
+
@push_handler = http_info[:handlers][:push]
|
49
|
+
@pull_handlers = http_info[:handlers][:pull]
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should receive all sent messages" do
|
53
|
+
resp_names = @responses.map(&:name).inject(Set.new) {|memo,rn| memo << rn}
|
54
|
+
@sent.map(&:name).each {|sn| resp_names.should include(sn)}
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should return to the client as many responses as sent messages" do
|
58
|
+
@client_responses.length.should == @sent.length
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should return to the client an identical message to that which was sent" do
|
62
|
+
@client_responses.each do |resp|
|
63
|
+
resp[:sent_message].name.should == resp[:resp_message].name
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -29,7 +29,7 @@ describe "zmq push/pull" do
|
|
29
29
|
|
30
30
|
@ddn.start
|
31
31
|
sleep 0.1
|
32
|
-
@ddn.stop
|
32
|
+
@ddn.stop rescue nil
|
33
33
|
|
34
34
|
{:responses => responses, :handlers => { :push => push, :pull => [pull] }}
|
35
35
|
end
|
@@ -43,10 +43,9 @@ describe "zmq push/pull" do
|
|
43
43
|
@pull_handlers = pp_info[:handlers][:pull]
|
44
44
|
end
|
45
45
|
|
46
|
-
it "should receive all sent messages
|
47
|
-
@
|
48
|
-
|
49
|
-
end
|
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)}
|
50
49
|
end
|
51
50
|
|
52
51
|
it "should receive messages on both pull sockets" do
|
@@ -12,7 +12,7 @@ describe "zmq xreq/xrep" do
|
|
12
12
|
rep = zmq_xrep(addr, :bind)
|
13
13
|
req = zmq_xreq(addr, :connect)
|
14
14
|
|
15
|
-
rep.on_recv do |identities,seq
|
15
|
+
rep.on_recv do |message,identities,seq|
|
16
16
|
yield(identities,seq,message) if block
|
17
17
|
responses << {:identities => identities, :seq => seq, :message => message}
|
18
18
|
end
|
@@ -22,7 +22,7 @@ describe "zmq xreq/xrep" do
|
|
22
22
|
|
23
23
|
@ddn.start
|
24
24
|
sleep 0.1
|
25
|
-
@ddn.stop
|
25
|
+
@ddn.stop rescue nil
|
26
26
|
|
27
27
|
{:responses => responses, :handlers => {:req => req, :rep => rep}}
|
28
28
|
end
|
@@ -64,8 +64,8 @@ describe "zmq xreq/xrep" do
|
|
64
64
|
req1 = zmq_xreq(addr, :connect)
|
65
65
|
req2 = zmq_xreq(addr, :connect)
|
66
66
|
|
67
|
-
rep.on_recv do |identities,seq
|
68
|
-
rep.send_message(identities,seq
|
67
|
+
rep.on_recv do |message,identities,seq|
|
68
|
+
rep.send_message(message,identities,seq)
|
69
69
|
end
|
70
70
|
|
71
71
|
r1_msg = DripDrop::Message.new("REQ1 Message")
|
data/spec/node_spec.rb
CHANGED
@@ -2,17 +2,21 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe DripDrop::Node do
|
4
4
|
describe "initialization" do
|
5
|
-
before do
|
6
|
-
@ddn = DripDrop::Node.new {
|
5
|
+
before(:all) do
|
6
|
+
@ddn = DripDrop::Node.new {
|
7
|
+
zmq_subscribe(rand_addr,:bind) #Keeps ZMQMachine Happy
|
8
|
+
}
|
7
9
|
@ddn.start
|
8
|
-
sleep
|
10
|
+
sleep 1
|
9
11
|
end
|
10
12
|
|
11
13
|
it "should start EventMachine" do
|
14
|
+
pending "This is not repeatedly reliable"
|
12
15
|
EM.reactor_running?.should be_true
|
13
16
|
end
|
14
17
|
|
15
18
|
it "should start ZMQMachine" do
|
19
|
+
pending "This is not repeatedly reliable"
|
16
20
|
@ddn.zm_reactor.running?.should be_true
|
17
21
|
end
|
18
22
|
|
@@ -23,10 +27,12 @@ describe DripDrop::Node do
|
|
23
27
|
|
24
28
|
describe "shutdown" do
|
25
29
|
before do
|
26
|
-
@ddn = DripDrop::Node.new {
|
30
|
+
@ddn = DripDrop::Node.new {
|
31
|
+
zmq_subscribe(rand_addr,:bind) #Keeps ZMQMachine Happy
|
32
|
+
}
|
27
33
|
@ddn.start
|
28
|
-
sleep 0.1
|
29
|
-
@ddn.stop
|
34
|
+
sleep 0.1
|
35
|
+
@ddn.stop rescue nil
|
30
36
|
sleep 0.1
|
31
37
|
end
|
32
38
|
|
metadata
CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
|
|
4
4
|
prerelease: false
|
5
5
|
segments:
|
6
6
|
- 0
|
7
|
-
-
|
7
|
+
- 3
|
8
8
|
- 0
|
9
|
-
version: 0.
|
9
|
+
version: 0.3.0
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Andrew Cholakian
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2010-10-
|
17
|
+
date: 2010-10-21 00:00:00 -07:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
@@ -91,6 +91,7 @@ files:
|
|
91
91
|
- example/http.rb
|
92
92
|
- example/pubsub.rb
|
93
93
|
- example/pushpull.rb
|
94
|
+
- example/subclass.rb
|
94
95
|
- example/xreq_xrep.rb
|
95
96
|
- js/dripdrop.html
|
96
97
|
- js/dripdrop.js
|
@@ -104,8 +105,10 @@ files:
|
|
104
105
|
- lib/dripdrop/handlers/zeromq.rb
|
105
106
|
- lib/dripdrop/message.rb
|
106
107
|
- lib/dripdrop/node.rb
|
107
|
-
- spec/
|
108
|
-
- spec/node/
|
108
|
+
- spec/message_spec.rb
|
109
|
+
- spec/node/http_spec.rb
|
110
|
+
- spec/node/zmq_pushpull_spec.rb
|
111
|
+
- spec/node/zmq_xrepxreq_spec.rb
|
109
112
|
- spec/node_spec.rb
|
110
113
|
- spec/spec_helper.rb
|
111
114
|
has_rdoc: true
|
@@ -141,7 +144,9 @@ signing_key:
|
|
141
144
|
specification_version: 3
|
142
145
|
summary: 0MQ App Stats
|
143
146
|
test_files:
|
144
|
-
- spec/node/zmq_pushpull.rb
|
145
|
-
- spec/node/zmq_xrepxreq.rb
|
146
|
-
- spec/node_spec.rb
|
147
147
|
- spec/spec_helper.rb
|
148
|
+
- spec/node/http_spec.rb
|
149
|
+
- spec/node/zmq_xrepxreq_spec.rb
|
150
|
+
- spec/node/zmq_pushpull_spec.rb
|
151
|
+
- spec/message_spec.rb
|
152
|
+
- spec/node_spec.rb
|