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