dripdrop 0.9.10 → 0.10.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +143 -33
- data/Rakefile +7 -3
- data/VERSION +1 -1
- data/dripdrop.gemspec +14 -14
- data/example/complex/server.rb +18 -31
- data/lib/dripdrop/handlers/base.rb +9 -6
- data/lib/dripdrop/handlers/mongrel2.rb +163 -0
- data/lib/dripdrop/handlers/{websockets.rb → websocket_server.rb} +6 -2
- data/lib/dripdrop/handlers/zeromq.rb +101 -71
- data/lib/dripdrop/message.rb +2 -2
- data/lib/dripdrop/node.rb +63 -24
- data/spec/node/websocket_spec.rb +1 -1
- data/spec/node/zmq_m2_spec.rb +77 -0
- data/spec/node/zmq_pushpull_spec.rb +3 -1
- data/spec/node_spec.rb +1 -2
- metadata +103 -111
data/README.md
CHANGED
@@ -1,54 +1,164 @@
|
|
1
1
|
# DripDrop
|
2
2
|
|
3
|
-
|
3
|
+
## INSTALLATION NOTES:
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
5
|
+
1. Install with 'gem install dripdrop --pre' as you probably want the beta gem.
|
6
|
+
2. Build eventmachine from [git master](https://github.com/eventmachine/eventmachine). It fixes a ZeroMQ bug that will cause you much pain if you don't have it.
|
7
|
+
3. Build zeromq2 from [git master](https://github.com/zeromq/zeromq2)
|
8
|
+
4. You probably want the yajl-ruby (or json-java if you're on jRuby) gem installed for optimal JSON generation. You don't have to use JSON with DripDrop, but it is the default.
|
9
|
+
5. If you have any problems, open an issue, a lot of stuff is in flux!
|
10
|
+
|
11
|
+
## About
|
12
|
+
|
13
|
+
DripDrop is a library aiming to help you write better message passing apps. It's a rich toolchest, currently based on EventMachine, which provides async IO.
|
14
|
+
|
15
|
+
DripDrop helps in these ways:
|
16
|
+
|
17
|
+
1. Normalized Communication Interfaces. All protocols, regardless of their using HTTP, ZeroMQ, or WebSockets, have a unified API, with minor differences only to accomodate the reality of the underlying protocol.
|
18
|
+
2. A set of tools to break your app into composable parts--we call them nodelets in dripdrop--ideally communicating with each other with the aforementioned interfaces..
|
19
|
+
3. A simple, yet powerful messaging class, that lets you control the structure, formatting, and serialization of messages sent between nodelets.
|
20
|
+
4. Tools to break your nodelets off when it comes time to deploy, letting you scale your app by delegating roles and scaling out resources
|
21
|
+
|
22
|
+
## Normalized Interfaces
|
23
|
+
|
24
|
+
Let's start by looking at the normalized communication interface in a simple app.
|
25
|
+
|
26
|
+
class MyApp < DripDrop::Node
|
27
|
+
def action #Special method that gets executed on Node#start
|
28
|
+
# Define some sockets, here we create an HTTP server, and
|
29
|
+
# a client to it. :my_hts and :my_htc are custom names
|
30
|
+
# that will be available after definition
|
31
|
+
route :my_server, :http_server, 'http://127.0.0.1:2201'
|
32
|
+
route :my_client, :http_client, 'http://127.0.0.1:2201'
|
33
|
+
|
34
|
+
# Our http server is a simple time server
|
35
|
+
my_server.on_recv do |message,response|
|
36
|
+
response.send_message(:name => 'time', :body => {'time' => Time.now.to_s})
|
37
|
+
end
|
18
38
|
|
19
|
-
|
20
|
-
|
39
|
+
# Here, we setup a timer, and periodically poll the http server
|
40
|
+
EM::PeriodicTimer.new(1) do
|
41
|
+
# Messages must have a :name. They can optionally have a :body.
|
42
|
+
# Additionally, they can set custom :head properties.
|
43
|
+
my_client.send_message(:name => 'time_request') do |response_message|
|
44
|
+
puts "The time is: #{response_message.body['time']}"
|
45
|
+
end
|
46
|
+
end
|
21
47
|
end
|
22
|
-
|
23
|
-
|
48
|
+
end
|
49
|
+
|
50
|
+
#Start the app and block
|
51
|
+
MyApp.new.start!
|
52
|
+
|
53
|
+
What we've done here is use HTTP as a simple messaging protocol. Yes, we've thrown out a good chunk of what HTTP does, but consider this, that exact same code would work if we replaced the top two lines with:
|
54
|
+
|
55
|
+
route :my_server, :zmq_xrep, 'http://127.0.0.1:2201', :bind
|
56
|
+
route :my_client, :zmq_xreq, 'http://127.0.0.1:2201', :connect
|
57
|
+
|
58
|
+
That replaces the HTTP server and client with ultra-high performance zeromq sockets. Now, protocols have varying strengths and weaknesses, and ZeroMQ is not HTTP necessarily, for instance, given a :zmq_pub socket, you can only send_messages, but there is no response message, because :zmq_pub is the publishing end of a request/reply pattern. The messaging API attempts to reduce all methods on sockets to the following set:
|
59
|
+
|
60
|
+
* on_recv (sometimes takes a block with |message,response| if it can send a response)
|
61
|
+
* send_message
|
62
|
+
* on_open (Websockets only)
|
63
|
+
* on_close (Websockets only)
|
64
|
+
|
65
|
+
|
66
|
+
## Composable Parts
|
67
|
+
|
68
|
+
The tools mentioned above are useful, but if you try and build a larger app you'll quickly find them lacking. The callbacks get tricky, and mixing your logic up in a single #action method becomes messy. That's why we have nodelets in DripDrop. Here's a trivial example.
|
69
|
+
|
70
|
+
class MyApp < DripDrop::Node
|
71
|
+
# This will instantiate a new StatsCollector object, and define the
|
72
|
+
# stats_raw and stats_filtered methods inside it.
|
73
|
+
nodelet :stats_collector, StatsCollector do |nodelet|
|
74
|
+
nodelet.route :stats_raw, :zmq_pull, 'tcp://127.0.0.1:2301', :bind
|
75
|
+
nodelet.route :stats_filtered, :zmq_push, 'tcp://127.0.0.1:2302', :bind
|
24
76
|
end
|
25
77
|
|
26
|
-
|
27
|
-
|
28
|
-
i += 1
|
29
|
-
stats_pub.send_message(message)
|
30
|
-
response.send_message(:name => 'ack', :body => {:seq => i})
|
78
|
+
nodelet :stats_processor, StatsProcessor do |nodelet|
|
79
|
+
nodelet.route :stats_ingress, :zmq_pull, 'tcp://127.0.0.1:2302', :bind
|
31
80
|
end
|
32
81
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
82
|
+
# The nodelets method gives you access to all defined nodelets
|
83
|
+
# We created a #run method on each nodelet we call here.
|
84
|
+
nodelets.each {|n| n.run}
|
85
|
+
end
|
86
|
+
|
87
|
+
# You must subclass Nodelet
|
88
|
+
# The method #run here is merely a convention
|
89
|
+
class StatsCollector < DripDrop::Node::Nodelet
|
90
|
+
def run
|
91
|
+
stats_raw.on_recv do |raw_stat_msg|
|
92
|
+
# Custom filtering code could go here...
|
93
|
+
stats_filtered.send_message(raw_stat_msg)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
class StatsProcessor < DripDrop::Node::Nodelet
|
99
|
+
# Initialize shouldn't be subclassed on a Nodelet, this gets called
|
100
|
+
# After the nodelet is instantiated
|
101
|
+
def configure
|
102
|
+
@name_counts = Hash.new(0)
|
103
|
+
end
|
104
|
+
|
105
|
+
def run
|
106
|
+
stats_ingress.on_recv do |message|
|
107
|
+
@name_counts[message.name] += 1
|
108
|
+
puts @name_counts.inspect
|
37
109
|
end
|
38
110
|
end
|
39
|
-
end
|
111
|
+
end
|
112
|
+
|
113
|
+
MyApp.new.start!
|
40
114
|
|
41
|
-
|
115
|
+
# Custom Messages
|
42
116
|
|
43
|
-
|
117
|
+
DripDrop::Message is the parent class of all messages in dripdrop, it's a flexible and freeform way to send data. In more complex apps you'll want to both define custom behaviour on messages, and restrict the data they carry. This is possible by subclassing DripDrop::Message. Before we look at that though, lets see what makes a DripDrop::Message.
|
44
118
|
|
45
|
-
|
119
|
+
The simplest DripDrop::Message you could create would look something like this if dumped into JSON:
|
120
|
+
|
121
|
+
{name: 'msgname', head: {}, body: null}
|
122
|
+
|
123
|
+
In other words, a dripdrop message *must* provide a name, it must also be able to store arbitrary, nested, keys and values in its head, and it may use the body for any data it wishes.
|
46
124
|
|
47
|
-
|
125
|
+
If you'd like to create your own Message format, simply Subclass DripDrop::Message. If you want to restrict your handlers to using a specific message type, it's easily done by passing in the :message_class option. For instance
|
126
|
+
|
127
|
+
class MyMessageClass < DripDrop::Message
|
128
|
+
# Custom code
|
129
|
+
end
|
130
|
+
class MyApp < DripDrop::Node
|
131
|
+
def action
|
132
|
+
route :myhandler, :zmq_publish, 'tcp://127.0.0.1:2200', :bind, :message_class => MyMessageClass
|
133
|
+
route :myhandler, :zmq_subscribe, 'tcp://127.0.0.1:2200', :connect, :message_class => MyMessageClass
|
134
|
+
end
|
135
|
+
end
|
48
136
|
|
49
|
-
|
137
|
+
# Breaking out your nodelets
|
138
|
+
|
139
|
+
One of the core ideas behind dripdrop, is that if your application is composed of a bunch of separate parts, that in production deployment, will run on separate physical servers, it should still be possible for you to develop and test with ease. If you structure your app into separate nodelets, and *only* communicate between them via message passing, you can accomplish this easily.
|
140
|
+
|
141
|
+
While you will have to write your own executable wrappers suitable for your own deployment, one convenenience feature built in is the notion of a +run_list+. By setting the #run_list you can restrict which nodelets actually get initialized. For example:
|
142
|
+
|
143
|
+
class MyApp < DripDrop::Node
|
144
|
+
nodelet :service_one, ServiceOneClass do
|
145
|
+
#nodelet setup
|
146
|
+
end
|
147
|
+
nodelet :service_two, ServiceTwoClass do
|
148
|
+
#nodelet setup
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Only starts :service_two, the setup for :service_one
|
153
|
+
# is skipped as well
|
154
|
+
MyApp.new(:run_list => [:service_two]).start!
|
155
|
+
|
156
|
+
#RDocs
|
157
|
+
|
158
|
+
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.
|
50
159
|
|
51
160
|
#Contributors
|
52
161
|
|
53
162
|
* Andrew Cholakian: [andrewvc](http://github.com/andrewvc)
|
54
163
|
* John W Higgins: [wishdev](http://github.com/wishdev)
|
164
|
+
* Nick Recobra: [oruen](https://github.com/oruen)
|
data/Rakefile
CHANGED
@@ -10,10 +10,9 @@ begin
|
|
10
10
|
gem.email = "andrew@andrewvc.com"
|
11
11
|
gem.homepage = "http://github.com/andrewvc/dripdrop"
|
12
12
|
gem.authors = ["Andrew Cholakian"]
|
13
|
-
gem.add_dependency('ffi-rzmq', '>= 0.7.1')
|
14
|
-
gem.add_dependency('eventmachine', '>= 0.12.10')
|
15
13
|
gem.add_dependency('em-websocket')
|
16
|
-
gem.add_dependency('em-zeromq', '>= 0.
|
14
|
+
gem.add_dependency('em-zeromq', '>= 0.2.0.beta1')
|
15
|
+
gem.add_development_dependency('rspec', '>= 2.4.0')
|
17
16
|
end
|
18
17
|
Jeweler::GemcutterTasks.new
|
19
18
|
rescue LoadError
|
@@ -33,3 +32,8 @@ Rake::RDocTask.new do |rdoc|
|
|
33
32
|
rdoc.rdoc_files.include('README*')
|
34
33
|
rdoc.rdoc_files.include('lib/**/*.rb')
|
35
34
|
end
|
35
|
+
|
36
|
+
require 'rspec/core/rake_task'
|
37
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
38
|
+
end
|
39
|
+
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.10.0.beta1
|
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.10.0.beta1"
|
9
9
|
|
10
|
-
s.required_rubygems_version = Gem::Requirement.new("
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new("> 1.3.1") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Andrew Cholakian"]
|
12
|
-
s.date = %q{2011-02-
|
12
|
+
s.date = %q{2011-02-28}
|
13
13
|
s.description = %q{Evented framework for ZeroMQ and EventMachine Apps. }
|
14
14
|
s.email = %q{andrew@andrewvc.com}
|
15
15
|
s.extra_rdoc_files = [
|
@@ -45,7 +45,8 @@ Gem::Specification.new do |s|
|
|
45
45
|
"lib/dripdrop/handlers/base.rb",
|
46
46
|
"lib/dripdrop/handlers/http_client.rb",
|
47
47
|
"lib/dripdrop/handlers/http_server.rb",
|
48
|
-
"lib/dripdrop/handlers/
|
48
|
+
"lib/dripdrop/handlers/mongrel2.rb",
|
49
|
+
"lib/dripdrop/handlers/websocket_server.rb",
|
49
50
|
"lib/dripdrop/handlers/zeromq.rb",
|
50
51
|
"lib/dripdrop/message.rb",
|
51
52
|
"lib/dripdrop/node.rb",
|
@@ -56,6 +57,7 @@ Gem::Specification.new do |s|
|
|
56
57
|
"spec/node/nodelet_spec.rb",
|
57
58
|
"spec/node/routing_spec.rb",
|
58
59
|
"spec/node/websocket_spec.rb",
|
60
|
+
"spec/node/zmq_m2_spec.rb",
|
59
61
|
"spec/node/zmq_pushpull_spec.rb",
|
60
62
|
"spec/node/zmq_xrepxreq_spec.rb",
|
61
63
|
"spec/node_spec.rb",
|
@@ -63,7 +65,7 @@ Gem::Specification.new do |s|
|
|
63
65
|
]
|
64
66
|
s.homepage = %q{http://github.com/andrewvc/dripdrop}
|
65
67
|
s.require_paths = ["lib"]
|
66
|
-
s.rubygems_version = %q{1.5.
|
68
|
+
s.rubygems_version = %q{1.5.0}
|
67
69
|
s.summary = %q{Evented framework for ZeroMQ and EventMachine Apps.}
|
68
70
|
s.test_files = [
|
69
71
|
"spec/gimite-websocket.rb",
|
@@ -72,6 +74,7 @@ Gem::Specification.new do |s|
|
|
72
74
|
"spec/node/nodelet_spec.rb",
|
73
75
|
"spec/node/routing_spec.rb",
|
74
76
|
"spec/node/websocket_spec.rb",
|
77
|
+
"spec/node/zmq_m2_spec.rb",
|
75
78
|
"spec/node/zmq_pushpull_spec.rb",
|
76
79
|
"spec/node/zmq_xrepxreq_spec.rb",
|
77
80
|
"spec/node_spec.rb",
|
@@ -82,21 +85,18 @@ Gem::Specification.new do |s|
|
|
82
85
|
s.specification_version = 3
|
83
86
|
|
84
87
|
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
85
|
-
s.add_runtime_dependency(%q<ffi-rzmq>, [">= 0.7.1"])
|
86
|
-
s.add_runtime_dependency(%q<eventmachine>, [">= 0.12.10"])
|
87
88
|
s.add_runtime_dependency(%q<em-websocket>, [">= 0"])
|
88
|
-
s.add_runtime_dependency(%q<em-zeromq>, [">= 0.
|
89
|
+
s.add_runtime_dependency(%q<em-zeromq>, [">= 0.2.0.beta1"])
|
90
|
+
s.add_development_dependency(%q<rspec>, [">= 2.4.0"])
|
89
91
|
else
|
90
|
-
s.add_dependency(%q<ffi-rzmq>, [">= 0.7.1"])
|
91
|
-
s.add_dependency(%q<eventmachine>, [">= 0.12.10"])
|
92
92
|
s.add_dependency(%q<em-websocket>, [">= 0"])
|
93
|
-
s.add_dependency(%q<em-zeromq>, [">= 0.
|
93
|
+
s.add_dependency(%q<em-zeromq>, [">= 0.2.0.beta1"])
|
94
|
+
s.add_dependency(%q<rspec>, [">= 2.4.0"])
|
94
95
|
end
|
95
96
|
else
|
96
|
-
s.add_dependency(%q<ffi-rzmq>, [">= 0.7.1"])
|
97
|
-
s.add_dependency(%q<eventmachine>, [">= 0.12.10"])
|
98
97
|
s.add_dependency(%q<em-websocket>, [">= 0"])
|
99
|
-
s.add_dependency(%q<em-zeromq>, [">= 0.
|
98
|
+
s.add_dependency(%q<em-zeromq>, [">= 0.2.0.beta1"])
|
99
|
+
s.add_dependency(%q<rspec>, [">= 2.4.0"])
|
100
100
|
end
|
101
101
|
end
|
102
102
|
|
data/example/complex/server.rb
CHANGED
@@ -8,40 +8,30 @@ class ComplexExample < DripDrop::Node
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def action
|
11
|
-
|
12
|
-
route :ws_listener, :websocket, 'ws://127.0.0.1:8080'
|
13
|
-
route :broadcast_in, :zmq_subscribe, 'tcp://127.0.0.1:2200', :connect
|
14
|
-
route :reqs_out, :zmq_xreq, 'tcp://127.0.0.1:2201', :connect
|
15
|
-
|
16
|
-
WSListener.new(:ws => ws_listener, :broadcast_in => broadcast_in, :reqs_out => reqs_out).run
|
11
|
+
nodelet :ws_listener, WSListener do |n|
|
12
|
+
n.route :ws_listener, :websocket, 'ws://127.0.0.1:8080'
|
13
|
+
n.route :broadcast_in, :zmq_subscribe, 'tcp://127.0.0.1:2200', :connect
|
14
|
+
n.route :reqs_out, :zmq_xreq, 'tcp://127.0.0.1:2201', :connect
|
17
15
|
end
|
18
|
-
|
19
|
-
if [:all, :coordinator].include?(@mode)
|
20
|
-
route :broadcast_out, :zmq_publish, 'tcp://127.0.0.1:2200', :bind
|
21
|
-
route :reqs_in, :zmq_xrep, 'tcp://127.0.0.1:2201', :bind
|
22
|
-
route :reqs_htout, :http_client, 'tcp://127.0.0.1:3000/endpoint'
|
23
16
|
|
24
|
-
|
17
|
+
nodelet :coordinator, Coordinator do |n|
|
18
|
+
n.route :broadcast_out, :zmq_publish, 'tcp://127.0.0.1:2200', :bind
|
19
|
+
n.route :reqs_in, :zmq_xrep, 'tcp://127.0.0.1:2201', :bind
|
20
|
+
n.route :reqs_htout, :http_client, 'tcp://127.0.0.1:3000/endpoint'
|
25
21
|
end
|
26
22
|
end
|
27
23
|
end
|
28
24
|
|
29
|
-
class Coordinator
|
30
|
-
def initialize(opts={})
|
31
|
-
@bc_out = opts[:broadcast_out]
|
32
|
-
@reqs_in = opts[:reqs_in]
|
33
|
-
@reqs_htout = opts[:reqs_htout]
|
34
|
-
end
|
35
|
-
|
25
|
+
class Coordinator < DripDrop::Node::Nodelet
|
36
26
|
def run
|
37
27
|
proxy_reqs
|
38
28
|
heartbeat
|
39
29
|
end
|
40
30
|
|
41
31
|
def proxy_reqs
|
42
|
-
|
32
|
+
reqs_in.on_recv do |message, response|
|
43
33
|
puts "Proxying #{message.inspect} to htout"
|
44
|
-
|
34
|
+
reqs_htout.send_message(message) do |http_response|
|
45
35
|
puts "Received http response #{http_response.inspect} sending back"
|
46
36
|
response.send_message(http_response)
|
47
37
|
end
|
@@ -50,18 +40,17 @@ class Coordinator
|
|
50
40
|
|
51
41
|
def heartbeat
|
52
42
|
EM::PeriodicTimer.new(1) do
|
53
|
-
|
43
|
+
broadcast_out.send_message :name => 'tick', :body => Time.now.to_s
|
54
44
|
end
|
55
45
|
end
|
56
46
|
end
|
57
47
|
|
58
|
-
class WSListener
|
59
|
-
def initialize(
|
60
|
-
|
61
|
-
@bc_in = opts[:broadcast_in]
|
62
|
-
@reqs_out = opts[:reqs_out]
|
48
|
+
class WSListener < DripDrop::Node::Nodelet
|
49
|
+
def initialize(*args)
|
50
|
+
super
|
63
51
|
@client_channel = EM::Channel.new
|
64
52
|
end
|
53
|
+
|
65
54
|
def run
|
66
55
|
proxy_websockets
|
67
56
|
broadcast_to_websockets
|
@@ -69,15 +58,13 @@ class WSListener
|
|
69
58
|
|
70
59
|
def broadcast_to_websockets
|
71
60
|
# Receives messages from Broadcast Out
|
72
|
-
|
61
|
+
broadcast_in.on_recv do |message|
|
73
62
|
puts "Broadcast In recv: #{message.inspect}"
|
74
63
|
@client_channel.push(message)
|
75
64
|
end
|
76
65
|
end
|
77
66
|
|
78
67
|
def proxy_websockets
|
79
|
-
ws = @ws
|
80
|
-
|
81
68
|
sigs_sids = {} #Map connection signatures to subscriber IDs
|
82
69
|
|
83
70
|
ws.on_open do |conn|
|
@@ -102,7 +89,7 @@ class WSListener
|
|
102
89
|
|
103
90
|
ws.on_recv do |message,conn|
|
104
91
|
puts "WS Recv #{message.name}"
|
105
|
-
|
92
|
+
reqs_out.send_message(message) do |resp_message|
|
106
93
|
puts "Recvd resp_message #{resp_message.inspect}, sending back to client"
|
107
94
|
conn.send_message(resp_message)
|
108
95
|
end
|
@@ -13,21 +13,24 @@ class DripDrop
|
|
13
13
|
print_exception(e)
|
14
14
|
end
|
15
15
|
else
|
16
|
-
print_exception(
|
16
|
+
print_exception(exception)
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
20
|
def print_exception(exception)
|
21
|
-
|
22
|
-
|
21
|
+
if exception.is_a?(Exception)
|
22
|
+
$stderr.write exception.message
|
23
|
+
$stderr.write exception.backtrace.join("\t\n")
|
24
|
+
else
|
25
|
+
$stderr.write "Expected an exception, got: #{exception.inspect}"
|
26
|
+
end
|
23
27
|
end
|
24
28
|
|
25
29
|
private
|
26
30
|
# Normalize Hash objs and DripDrop::Message objs into DripDrop::Message objs
|
27
|
-
def dd_messagify(message)
|
31
|
+
def dd_messagify(message,klass=DripDrop::Message)
|
28
32
|
if message.is_a?(Hash)
|
29
|
-
return
|
30
|
-
:body => message[:body])
|
33
|
+
return klass.from_hash(message)
|
31
34
|
elsif message.is_a?(DripDrop::Message)
|
32
35
|
return message
|
33
36
|
else
|
@@ -0,0 +1,163 @@
|
|
1
|
+
=begin
|
2
|
+
Large portion of at least the concepts (and plenty of the code) used here come from m2r
|
3
|
+
|
4
|
+
https://github.com/perplexes/m2r
|
5
|
+
|
6
|
+
Under the following license
|
7
|
+
|
8
|
+
Copyright (c) 2009 Pradeep Elankumaran
|
9
|
+
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
11
|
+
a copy of this software and associated documentation files (the
|
12
|
+
"Software"), to deal in the Software without restriction, including
|
13
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
14
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
15
|
+
permit persons to whom the Software is furnished to do so, subject to
|
16
|
+
the following conditions:
|
17
|
+
|
18
|
+
The above copyright notice and this permission notice shall be
|
19
|
+
included in all copies or substantial portions of the Software.
|
20
|
+
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
22
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
23
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
24
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
25
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
26
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
27
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
28
|
+
=end
|
29
|
+
|
30
|
+
class DripDrop
|
31
|
+
class Mongrel2Handler < ZMQBaseHandler
|
32
|
+
include ZMQWritableHandler
|
33
|
+
include ZMQReadableHandler
|
34
|
+
attr_accessor :uuid
|
35
|
+
|
36
|
+
def initialize(*args)
|
37
|
+
super(*args)
|
38
|
+
@connections = []
|
39
|
+
self.uuid = @opts[:uuid]
|
40
|
+
end
|
41
|
+
|
42
|
+
def add_connection(connection)
|
43
|
+
@connections << connection
|
44
|
+
end
|
45
|
+
|
46
|
+
def read_connection
|
47
|
+
@connections[0]
|
48
|
+
end
|
49
|
+
|
50
|
+
def write_connection
|
51
|
+
@connections[1]
|
52
|
+
end
|
53
|
+
|
54
|
+
def address
|
55
|
+
raise "Not applicable for a Mongrel2Handler"
|
56
|
+
end
|
57
|
+
|
58
|
+
def on_readable(socket, messages)
|
59
|
+
req = Mongrel2Request.parse_request(messages[0])
|
60
|
+
@recv_cbak.call(req)
|
61
|
+
end
|
62
|
+
|
63
|
+
def send_resp(uuid, conn_id, msg)
|
64
|
+
header = "%s %d:%s," % [uuid, conn_id.size, conn_id]
|
65
|
+
string = header + ' ' + msg
|
66
|
+
send_message(string)
|
67
|
+
end
|
68
|
+
|
69
|
+
def reply(req, msg)
|
70
|
+
self.send_resp(req.sender, req.conn_id, msg)
|
71
|
+
end
|
72
|
+
|
73
|
+
def reply_http(req, body, code=200, headers={})
|
74
|
+
self.reply(req, http_response(body, code, headers))
|
75
|
+
end
|
76
|
+
|
77
|
+
def http_response(body, code, headers)
|
78
|
+
headers['Content-Length'] = body.size
|
79
|
+
headers_s = headers.map { |k, v| "%s: %s" % [k, v] }.join("\r\n")
|
80
|
+
|
81
|
+
"HTTP/1.1 #{code} #{StatusMessage[code.to_i]}\r\n#{headers_s}\r\n\r\n#{body}"
|
82
|
+
end
|
83
|
+
|
84
|
+
# From WEBrick
|
85
|
+
StatusMessage = {
|
86
|
+
100 => 'Continue',
|
87
|
+
101 => 'Switching Protocols',
|
88
|
+
200 => 'OK',
|
89
|
+
201 => 'Created',
|
90
|
+
202 => 'Accepted',
|
91
|
+
203 => 'Non-Authoritative Information',
|
92
|
+
204 => 'No Content',
|
93
|
+
205 => 'Reset Content',
|
94
|
+
206 => 'Partial Content',
|
95
|
+
300 => 'Multiple Choices',
|
96
|
+
301 => 'Moved Permanently',
|
97
|
+
302 => 'Found',
|
98
|
+
303 => 'See Other',
|
99
|
+
304 => 'Not Modified',
|
100
|
+
305 => 'Use Proxy',
|
101
|
+
307 => 'Temporary Redirect',
|
102
|
+
400 => 'Bad Request',
|
103
|
+
401 => 'Unauthorized',
|
104
|
+
402 => 'Payment Required',
|
105
|
+
403 => 'Forbidden',
|
106
|
+
404 => 'Not Found',
|
107
|
+
405 => 'Method Not Allowed',
|
108
|
+
406 => 'Not Acceptable',
|
109
|
+
407 => 'Proxy Authentication Required',
|
110
|
+
408 => 'Request Timeout',
|
111
|
+
409 => 'Conflict',
|
112
|
+
410 => 'Gone',
|
113
|
+
411 => 'Length Required',
|
114
|
+
412 => 'Precondition Failed',
|
115
|
+
413 => 'Request Entity Too Large',
|
116
|
+
414 => 'Request-URI Too Large',
|
117
|
+
415 => 'Unsupported Media Type',
|
118
|
+
416 => 'Request Range Not Satisfiable',
|
119
|
+
417 => 'Expectation Failed',
|
120
|
+
500 => 'Internal Server Error',
|
121
|
+
501 => 'Not Implemented',
|
122
|
+
502 => 'Bad Gateway',
|
123
|
+
503 => 'Service Unavailable',
|
124
|
+
504 => 'Gateway Timeout',
|
125
|
+
505 => 'HTTP Version Not Supported'
|
126
|
+
}
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
class Mongrel2Request
|
131
|
+
attr_reader :sender, :conn_id, :path, :headers, :body
|
132
|
+
|
133
|
+
def initialize(sender, conn_id, path, headers, body)
|
134
|
+
@sender = sender
|
135
|
+
@conn_id = conn_id
|
136
|
+
@path = path
|
137
|
+
@headers = headers
|
138
|
+
@body = body
|
139
|
+
|
140
|
+
if headers['METHOD'] == 'JSON'
|
141
|
+
@data = JSON.parse(@body)
|
142
|
+
else
|
143
|
+
@data = {}
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def self.parse_netstring(ns)
|
148
|
+
len, rest = ns.split(':', 2)
|
149
|
+
len = len.to_i
|
150
|
+
raise "Netstring did not end in ','" unless rest[len].chr == ','
|
151
|
+
[rest[0...len], rest[(len+1)..-1]]
|
152
|
+
end
|
153
|
+
|
154
|
+
def self.parse_request(msg)
|
155
|
+
sender, conn_id, path, rest = msg.copy_out_string.split(' ', 4)
|
156
|
+
headers, head_rest = parse_netstring(rest)
|
157
|
+
body, _ = parse_netstring(head_rest)
|
158
|
+
|
159
|
+
headers = JSON.parse(headers)
|
160
|
+
|
161
|
+
self.new(sender, conn_id, path, headers, body)
|
162
|
+
end
|
163
|
+
end
|
@@ -72,8 +72,12 @@ class DripDrop
|
|
72
72
|
end
|
73
73
|
|
74
74
|
def send_message(message)
|
75
|
-
|
76
|
-
|
75
|
+
begin
|
76
|
+
encoded_message = dd_messagify(message).encoded
|
77
|
+
@ws.send(encoded_message)
|
78
|
+
rescue StandardError => e
|
79
|
+
handle_error(e)
|
80
|
+
end
|
77
81
|
end
|
78
82
|
end
|
79
83
|
end
|