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 CHANGED
@@ -1,54 +1,164 @@
1
1
  # DripDrop
2
2
 
3
- DripDrop is a library for structured message-passing async apps using EventMachine, ZeroMQ, and other protocols.
3
+ ## INSTALLATION NOTES:
4
4
 
5
- Here's an example of the kind of thing DripDrop makes easy, from [example/combined.rb](http://github.com/andrewvc/dripdrop/blob/master/example/combined.rb)
6
-
7
- require 'dripdrop'
8
- Thread.abort_on_exception = true #Always a good idea in multithreaded apps.
9
-
10
- # Encapsulates our EM and ZMQ reactors
11
- DripDrop::Node.new do
12
- # Define all our sockets
13
- route :stats_pub, :zmq_publish, 'tcp://127.0.0.1:2200', :bind
14
- route :stats_sub1, :zmq_subscribe, stats_pub.address, :connect
15
- route :stats_sub2, :zmq_subscribe, stats_pub.address, :connect
16
- route :http_collector, :http_server, 'http://127.0.0.1:8080'
17
- route :http_agent, :http_client, http_collector.address
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
- stats_sub1.on_recv do |message|
20
- puts "Receiver 1: #{message.body}"
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
- stats_sub2.on_recv do |message|
23
- puts "Receiver 2: #{message.body}"
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
- i = 0
27
- http_collector.on_recv do |message,response|
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
- EM::PeriodicTimer.new(1) do
34
- msg = DripDrop::Message.new('http/status', :body => "Success #{i}")
35
- http_agent.send_message(msg) do |resp_msg|
36
- puts "RESP: #{resp_msg.body['seq']}"
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.start! #Start the reactor and block until complete
111
+ end
112
+
113
+ MyApp.new.start!
40
114
 
41
- 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 MessagePack. There is a raw mode 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.
115
+ # Custom Messages
42
116
 
43
- #RDoc
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
- 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.
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
- #How It Works
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
- DripDrop encapsulates both zmqmachine, and eventmachine. It provides some sane default messaging choices, using [MessagePack](http://msgpack.org/)(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.
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.1.2')
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.9.10
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.9.10"
8
+ s.version = "0.10.0.beta1"
9
9
 
10
- s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
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-15}
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/websockets.rb",
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.1}
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.1.2"])
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.1.2"])
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.1.2"])
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
 
@@ -8,40 +8,30 @@ class ComplexExample < DripDrop::Node
8
8
  end
9
9
 
10
10
  def action
11
- if [:all, :websockets].include?(@mode)
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
- Coordinator.new(:broadcast_out => broadcast_out, :reqs_in => reqs_in, :reqs_htout => reqs_htout).run
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
- @reqs_in.on_recv do |message, response|
32
+ reqs_in.on_recv do |message, response|
43
33
  puts "Proxying #{message.inspect} to htout"
44
- @reqs_htout.send_message(message) do |http_response|
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
- @bc_out.send_message :name => 'tick', :body => Time.now.to_s
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(opts={})
60
- @ws = opts[:ws]
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
- @bc_in.on_recv do |message|
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
- @reqs_out.send_message(message) do |resp_message|
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(e)
16
+ print_exception(exception)
17
17
  end
18
18
  end
19
19
 
20
20
  def print_exception(exception)
21
- $stderr.write exception.message
22
- $stderr.write exception.backtrace.join("\t\n")
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 DripDrop::Message.new(message[:name], :head => message[:head],
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
- encoded_message = dd_messagify(message).encoded
76
- @ws.send(encoded_message)
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