henchman 0.0.1
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/.em-console.rc +3 -0
- data/.gitignore +3 -0
- data/.rvmrc +1 -0
- data/Gemfile +12 -0
- data/README.md +90 -0
- data/Rakefile +11 -0
- data/henchman.gemspec +22 -0
- data/lib/henchman.rb +292 -0
- data/lib/henchman/worker.rb +187 -0
- data/script/consume +12 -0
- data/script/enqueue +13 -0
- data/script/publish +13 -0
- data/script/subscribe +12 -0
- data/spec/henchman_spec.rb +193 -0
- metadata +93 -0
data/.em-console.rc
ADDED
data/.gitignore
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm 1.9.3@henchman --create
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
# henchman
|
2
|
+
|
3
|
+
A thin wrapper around [amqp](https://github.com/ruby-amqp/amqp).
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
### Ruby
|
8
|
+
|
9
|
+
We use Ruby 1.9.3.
|
10
|
+
|
11
|
+
To install and run on your local machine use [RVM](https://rvm.beginrescueend.com/).
|
12
|
+
For Mac machines make sure you compile with gcc-4.2 (because the compiler from Xcode doesn't compile Ruby 1.9.3 properly).
|
13
|
+
Download and install gcc from https://github.com/kennethreitz/osx-gcc-installer
|
14
|
+
|
15
|
+
$ gem install rvm
|
16
|
+
$ rvm install 1.9.3
|
17
|
+
|
18
|
+
And for Macs
|
19
|
+
|
20
|
+
$ rvm install 1.9.3 --with-gcc=gcc-4.2
|
21
|
+
|
22
|
+
### Rubygems
|
23
|
+
|
24
|
+
Use [Bundler](http://gembundler.com/) to install the gems needed by Herdis
|
25
|
+
|
26
|
+
$ bundle install
|
27
|
+
|
28
|
+
### RabbitMQ
|
29
|
+
|
30
|
+
henchman naturally needs [RabbitMQ](http://www.rabbitmq.com/) to run. Install it and run it with default options.
|
31
|
+
|
32
|
+
## Using
|
33
|
+
|
34
|
+
### Queues
|
35
|
+
|
36
|
+
To enqueue jobs that will only be consumed by a single consumer, you
|
37
|
+
|
38
|
+
EM.synchrony do
|
39
|
+
Henchman.enqueue("test", {:time => Time.now.to_s})
|
40
|
+
end
|
41
|
+
|
42
|
+
To consume jobs enqueued this way
|
43
|
+
|
44
|
+
EM.synchrony do
|
45
|
+
Henchman::Worker.new("test") do
|
46
|
+
puts message.inspect
|
47
|
+
puts headers
|
48
|
+
end.consume!
|
49
|
+
end
|
50
|
+
|
51
|
+
The `script/enqueue` and `script/consume` scripts provide a test case as simple as possible.
|
52
|
+
|
53
|
+
If you want a global error handler for the all consumers in your Ruby environment
|
54
|
+
|
55
|
+
EM.synchrony do
|
56
|
+
Henchman.error do
|
57
|
+
global_error_handler(exception)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
### Broadcasts
|
62
|
+
|
63
|
+
To publish jobs that will be consumed by every consumer listening to your exchange, you
|
64
|
+
|
65
|
+
EM.synchrony do
|
66
|
+
Henchman.publish("testpub", {:time => Time.now.to_s})
|
67
|
+
end
|
68
|
+
|
69
|
+
To consume jobs published this way
|
70
|
+
|
71
|
+
EM.synchrony do
|
72
|
+
Henchman::Worker.new("test") do
|
73
|
+
puts message.inspect
|
74
|
+
puts headers
|
75
|
+
end.subscribe!
|
76
|
+
end
|
77
|
+
|
78
|
+
The `script/publish` and `script/receive` scripts provide a test case as simple as possible.
|
79
|
+
|
80
|
+
Error handling is done the exact same way as with the single consumer case.
|
81
|
+
|
82
|
+
## Test suite
|
83
|
+
|
84
|
+
$ rake
|
85
|
+
|
86
|
+
## Console
|
87
|
+
|
88
|
+
To run an eventmachine-friendly console to test your servers from IRB
|
89
|
+
|
90
|
+
$ bundle exec em-console
|
data/Rakefile
ADDED
data/henchman.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "henchman"
|
6
|
+
s.version = "0.0.1"
|
7
|
+
s.authors = ["Martin Bruse"]
|
8
|
+
s.email = ["martin@oort.se"]
|
9
|
+
s.homepage = "https://github.com/zond/henchman"
|
10
|
+
s.summary = %q{A maximally simple amqp wrapper}
|
11
|
+
s.description = %q{A maximally simple amqp wrapper}
|
12
|
+
|
13
|
+
s.files = `git ls-files`.split("\n")
|
14
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
15
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
16
|
+
s.require_paths = ["lib"]
|
17
|
+
|
18
|
+
s.add_dependency 'amqp'
|
19
|
+
s.add_dependency 'em-synchrony'
|
20
|
+
s.add_dependency 'multi_json'
|
21
|
+
|
22
|
+
end
|
data/lib/henchman.rb
ADDED
@@ -0,0 +1,292 @@
|
|
1
|
+
|
2
|
+
require 'em-synchrony'
|
3
|
+
require 'amqp'
|
4
|
+
require 'multi_json'
|
5
|
+
|
6
|
+
require 'henchman/worker'
|
7
|
+
|
8
|
+
#
|
9
|
+
# Thin wrapper around AMQP
|
10
|
+
#
|
11
|
+
module Henchman
|
12
|
+
|
13
|
+
extend self
|
14
|
+
|
15
|
+
@@connection = nil
|
16
|
+
@@channel = nil
|
17
|
+
@@error_handler = Proc.new do
|
18
|
+
STDERR.puts("consume(#{queue_name.inspect}, #{headers.inspect}, #{message.inspect}): #{exception.message}")
|
19
|
+
STDERR.puts(exception.backtrace.join("\n"))
|
20
|
+
end
|
21
|
+
@@logger = Proc.new do |msg|
|
22
|
+
puts msg
|
23
|
+
end
|
24
|
+
|
25
|
+
#
|
26
|
+
# Define a log handler.
|
27
|
+
#
|
28
|
+
# @param [Proc] block the block that handles log messages.
|
29
|
+
#
|
30
|
+
def logger(&block)
|
31
|
+
@@logger = block
|
32
|
+
end
|
33
|
+
|
34
|
+
#
|
35
|
+
# Log a message.
|
36
|
+
#
|
37
|
+
# @param [String] msg the message to log.
|
38
|
+
#
|
39
|
+
def log(msg)
|
40
|
+
end
|
41
|
+
|
42
|
+
#
|
43
|
+
# @return [Proc] the error handler
|
44
|
+
#
|
45
|
+
def self.error_handler
|
46
|
+
@@error_handler
|
47
|
+
end
|
48
|
+
|
49
|
+
#
|
50
|
+
# Define an error handler.
|
51
|
+
#
|
52
|
+
# @param [Proc] block the block that handles errors.
|
53
|
+
#
|
54
|
+
def error(&block)
|
55
|
+
@@error_handler = block
|
56
|
+
end
|
57
|
+
|
58
|
+
#
|
59
|
+
# Will return a URL to the AMQP broker to use. Will get this from the <code>ENV</code> variable <code>AMQP_URL</code> if present.
|
60
|
+
#
|
61
|
+
# @return [String] a URL to an AMQP broker.
|
62
|
+
#
|
63
|
+
def amqp_url
|
64
|
+
ENV["AMQP_URL"] || "amqp://localhost/"
|
65
|
+
end
|
66
|
+
|
67
|
+
#
|
68
|
+
# Will return the default options when connecting to the AMQP broker.
|
69
|
+
#
|
70
|
+
# Uses the URL from {#amqp_url} to construct these options.
|
71
|
+
#
|
72
|
+
# @return [Hash] a {::Hash} of options to AMQP.connect.
|
73
|
+
#
|
74
|
+
def amqp_options
|
75
|
+
uri = URI.parse(amqp_url)
|
76
|
+
{
|
77
|
+
:vhost => uri.path,
|
78
|
+
:host => uri.host,
|
79
|
+
:user => uri.user || "guest",
|
80
|
+
:port => uri.port || 5672,
|
81
|
+
:pass => uri.password || "guest"
|
82
|
+
}
|
83
|
+
rescue Object => e
|
84
|
+
raise "invalid AMQP_URL: #{uri.inspect} (#{e})"
|
85
|
+
end
|
86
|
+
|
87
|
+
#
|
88
|
+
# Will return the default options to use when creating queues.
|
89
|
+
#
|
90
|
+
# If you change the returned {::Hash} the changes will persist in this instance, so use this to configure stuff.
|
91
|
+
#
|
92
|
+
# @return [Hash] a {::Hash} of options to use when creating queues.
|
93
|
+
#
|
94
|
+
def queue_options
|
95
|
+
@queue_options ||= {
|
96
|
+
:durable => true,
|
97
|
+
:auto_delete => true
|
98
|
+
}
|
99
|
+
end
|
100
|
+
|
101
|
+
#
|
102
|
+
# Will return the default options to use when creating exchanges.
|
103
|
+
#
|
104
|
+
# If you change the returned {::Hash} the changes will persist in this instance, so use this to configure stuff.
|
105
|
+
#
|
106
|
+
# @return [Hash] a {::Hash} of options to use when creating exchanges.
|
107
|
+
#
|
108
|
+
def exchange_options
|
109
|
+
@exchange_options ||= {
|
110
|
+
:auto_delete => true
|
111
|
+
}
|
112
|
+
end
|
113
|
+
|
114
|
+
#
|
115
|
+
# Will return the default options to use when creating channels.
|
116
|
+
#
|
117
|
+
# If you change the returned {::Hash} the changes will persist in this instance, so use this to configure stuff.
|
118
|
+
#
|
119
|
+
# @return [Hash] a {::Hash} of options to use when creating channels.
|
120
|
+
#
|
121
|
+
def channel_options
|
122
|
+
@channel_options ||= {
|
123
|
+
:prefetch => 1,
|
124
|
+
:auto_recovery => true
|
125
|
+
}
|
126
|
+
end
|
127
|
+
|
128
|
+
#
|
129
|
+
# Will stop and deactivate {::Henchman}.
|
130
|
+
#
|
131
|
+
def stop!
|
132
|
+
with_channel do |channel|
|
133
|
+
channel.close
|
134
|
+
end
|
135
|
+
@@channel = nil
|
136
|
+
with_connection do |connection|
|
137
|
+
connection.close
|
138
|
+
end
|
139
|
+
@@connection = nil
|
140
|
+
AMQP.stop
|
141
|
+
end
|
142
|
+
|
143
|
+
#
|
144
|
+
# Will yield an open and ready connection.
|
145
|
+
#
|
146
|
+
# @param [Proc] block a {::Proc} to yield an open and ready connection to.
|
147
|
+
#
|
148
|
+
def with_connection(&block)
|
149
|
+
@@connection = AMQP.connect(amqp_options) if @@connection.nil? || @@connection.status == :closed
|
150
|
+
@@connection.on_tcp_connection_loss do
|
151
|
+
log("#{self} reconnecting")
|
152
|
+
@@connection.reconnect
|
153
|
+
end
|
154
|
+
@@connection.on_recovery do
|
155
|
+
log("#{self} reconnected!")
|
156
|
+
end
|
157
|
+
@@connection.on_error do |connection, connection_close|
|
158
|
+
raise "#{connection}: #{connection_close.reply_text}"
|
159
|
+
end
|
160
|
+
@@connection.on_open do
|
161
|
+
yield @@connection
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
#
|
166
|
+
# Will yield an open and ready channel.
|
167
|
+
#
|
168
|
+
# @param [Proc] block a {::Proc} to yield an open and ready channel to.
|
169
|
+
#
|
170
|
+
def with_channel(&block)
|
171
|
+
with_connection do |connection|
|
172
|
+
@@channel = AMQP::Channel.new(connection, channel_options) if @@channel.nil? || @@channel.status == :closed
|
173
|
+
@@channel.on_error do |channel, channel_close|
|
174
|
+
log("#{self} reinitializing #{channel} due to #{channel_close}")
|
175
|
+
channel.reuse
|
176
|
+
end
|
177
|
+
@@channel.once_open do
|
178
|
+
yield @@channel
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
#
|
184
|
+
# Will yield an open and ready direct exchange.
|
185
|
+
#
|
186
|
+
# @param [Proc] block a {::Proc} to yield an open and ready direct exchange to.
|
187
|
+
#
|
188
|
+
def with_direct_exchange(&block)
|
189
|
+
with_channel do |channel|
|
190
|
+
channel.direct(AMQ::Protocol::EMPTY_STRING, exchange_options, &block)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
#
|
195
|
+
# Will yield an open and ready fanout exchange.
|
196
|
+
#
|
197
|
+
# @param [String] exchange_name the name of the exchange to create or find.
|
198
|
+
# @param [Proc] block a {::Proc} to yield an open and ready fanout exchange to.
|
199
|
+
#
|
200
|
+
def with_fanout_exchange(exchange_name, &block)
|
201
|
+
with_channel do |channel|
|
202
|
+
channel.fanout(exchange_name, exchange_options, &block)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
#
|
207
|
+
# Will yield an open and ready queue bound to an open and ready fanout exchange.
|
208
|
+
#
|
209
|
+
# @param [String] exchange_name the name of the exchange to create or find
|
210
|
+
# @param [Proc] block the {::Proc} to yield an open and ready queue bound to the found exchange to.
|
211
|
+
#
|
212
|
+
def with_fanout_queue(exchange_name, &block)
|
213
|
+
with_channel do |channel|
|
214
|
+
with_fanout_exchange(exchange_name) do |exchange|
|
215
|
+
channel.queue do |queue|
|
216
|
+
queue.bind(exchange) do
|
217
|
+
yield queue
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
#
|
225
|
+
# Will yield an open and ready queue.
|
226
|
+
#
|
227
|
+
# @param [Proc] block a {::Proc} to yield an open and ready queue to.
|
228
|
+
#
|
229
|
+
def with_queue(queue_name, &block)
|
230
|
+
with_channel do |channel|
|
231
|
+
channel.queue(queue_name, queue_options) do |queue|
|
232
|
+
yield queue
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
#
|
238
|
+
# Enqueue a message synchronously.
|
239
|
+
#
|
240
|
+
# @param [String] queue_name the name of the queue to enqueue on.
|
241
|
+
# @param [Object] message the message to enqueue.
|
242
|
+
#
|
243
|
+
def enqueue(queue_name, message)
|
244
|
+
EM::Synchrony.sync(aenqueue(queue_name, message))
|
245
|
+
end
|
246
|
+
|
247
|
+
#
|
248
|
+
# Enqueue a message asynchronously.
|
249
|
+
#
|
250
|
+
# @param (see #publish)
|
251
|
+
#
|
252
|
+
# @return [EM::Deferrable] a deferrable that will succeed when the publishing is done.
|
253
|
+
#
|
254
|
+
def aenqueue(queue_name, message)
|
255
|
+
deferrable = EM::DefaultDeferrable.new
|
256
|
+
with_direct_exchange do |exchange|
|
257
|
+
exchange.publish(MultiJson.encode(message), :routing_key => queue_name) do
|
258
|
+
deferrable.set_deferred_status :succeeded
|
259
|
+
end
|
260
|
+
end
|
261
|
+
deferrable
|
262
|
+
end
|
263
|
+
|
264
|
+
#
|
265
|
+
# Publish a a message to multiple consumers synchronously.
|
266
|
+
#
|
267
|
+
# @param [String] exchange_name the name of the exchange to publish on.
|
268
|
+
# @param [Object] message the object to publish
|
269
|
+
#
|
270
|
+
def publish(exchange_name, message)
|
271
|
+
EM::Synchrony.sync(apublish(exchange_name, message))
|
272
|
+
end
|
273
|
+
|
274
|
+
#
|
275
|
+
# Publish a message to multiple consumers asynchronously.
|
276
|
+
#
|
277
|
+
# @param (see #publish)
|
278
|
+
#
|
279
|
+
# @return [EM::Deferrable] a deferrable that will succeed when the publishing is done.
|
280
|
+
#
|
281
|
+
def apublish(exchange_name, message)
|
282
|
+
deferrable = EM::DefaultDeferrable.new
|
283
|
+
with_fanout_exchange(exchange_name) do |exchange|
|
284
|
+
exchange.publish(MultiJson.encode(message)) do
|
285
|
+
deferrable.set_deferred_status :succeeded
|
286
|
+
end
|
287
|
+
end
|
288
|
+
deferrable
|
289
|
+
end
|
290
|
+
|
291
|
+
end
|
292
|
+
|
@@ -0,0 +1,187 @@
|
|
1
|
+
|
2
|
+
module Henchman
|
3
|
+
|
4
|
+
#
|
5
|
+
# A class that handles incoming messages.
|
6
|
+
#
|
7
|
+
class Worker
|
8
|
+
|
9
|
+
#
|
10
|
+
# The handling of an incoming message.
|
11
|
+
#
|
12
|
+
class Task
|
13
|
+
|
14
|
+
#
|
15
|
+
# [AMQP::Header] The metadata of the message.
|
16
|
+
#
|
17
|
+
attr_accessor :headers
|
18
|
+
|
19
|
+
#
|
20
|
+
# [Object] The message itself
|
21
|
+
attr_accessor :message
|
22
|
+
|
23
|
+
#
|
24
|
+
# [Henchman::Worker] the {::Henchman::Worker} this {::Henchman::Worker::Task} belongs to.
|
25
|
+
#
|
26
|
+
attr_accessor :worker
|
27
|
+
|
28
|
+
#
|
29
|
+
# [Exception] any {::Exception} this {::Henchman::Worker::Task} has fallen victim to.
|
30
|
+
#
|
31
|
+
attr_accessor :exception
|
32
|
+
|
33
|
+
#
|
34
|
+
# [Object] the result of executing this {::Henchman::Worker::Task}.
|
35
|
+
#
|
36
|
+
attr_accessor :result
|
37
|
+
|
38
|
+
#
|
39
|
+
# Create a {::Henchman::Worker::Task} for a given {::Henchman::Worker}.
|
40
|
+
#
|
41
|
+
# @param [Henchman::Worker] worker the {::Henchman::Worker} creating this {::Henchman::Worker::Task}.
|
42
|
+
# @param [AMQP::Header] header the {::AMQP::Header} being handled.
|
43
|
+
# @param [Object] message the {::Object} being handled.
|
44
|
+
#
|
45
|
+
def initialize(worker, headers, message)
|
46
|
+
@worker = worker
|
47
|
+
@headers = headers
|
48
|
+
@message = message
|
49
|
+
end
|
50
|
+
|
51
|
+
#
|
52
|
+
# Call this {::Henchman::Worker::Task}.
|
53
|
+
#
|
54
|
+
def call
|
55
|
+
begin
|
56
|
+
@result = instance_eval(&(worker.block))
|
57
|
+
rescue Exception => e
|
58
|
+
@exception = e
|
59
|
+
@result = instance_eval(&(Henchman.error_handler))
|
60
|
+
ensure
|
61
|
+
headers.ack if headers.respond_to?(:ack)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
#
|
66
|
+
# Enqueue something on another queue.
|
67
|
+
#
|
68
|
+
# @param [String] queue_name the name of the queue on which to publish.
|
69
|
+
# @param [Object] message the message to publish-
|
70
|
+
#
|
71
|
+
def enqueue(queue_name, message)
|
72
|
+
Fiber.new do
|
73
|
+
Henchman.enqueue(queue_name, message)
|
74
|
+
end.resume
|
75
|
+
end
|
76
|
+
|
77
|
+
#
|
78
|
+
# Unsubscribe the {::Henchman::Worker} of this {::Henchman::Worker::Task} from the queue it subscribes to.
|
79
|
+
#
|
80
|
+
def unsubscribe!
|
81
|
+
worker.unsubscribe!
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
#
|
86
|
+
# [String] the name of the queue this {::Henchman::Worker} listens to.
|
87
|
+
#
|
88
|
+
attr_accessor :queue_name
|
89
|
+
|
90
|
+
#
|
91
|
+
# [AMQP::Consumer] the consumer feeding this {::Henchman::Worker} with messages.
|
92
|
+
#
|
93
|
+
attr_accessor :consumer
|
94
|
+
|
95
|
+
#
|
96
|
+
# [Proc] the {::Proc} handling the messages for this {::Henchman::Worker}.
|
97
|
+
#
|
98
|
+
attr_accessor :block
|
99
|
+
|
100
|
+
#
|
101
|
+
# @param [String] queue_name the name of the queue this worker listens to.
|
102
|
+
# @param [Symbol] exchange_type the type of exchange this worker will connect its queue to.
|
103
|
+
# @param [Proc] block the {::Proc} that will handle the messages for this {::Henchman::Worker}.
|
104
|
+
#
|
105
|
+
def initialize(queue_name, &block)
|
106
|
+
@block = block
|
107
|
+
@queue_name = queue_name
|
108
|
+
end
|
109
|
+
|
110
|
+
#
|
111
|
+
# Subscribe this {::Henchman::Worker} to a queue.
|
112
|
+
#
|
113
|
+
# @param [AMQP::Queue] queue the {::AMQP::Queue} to subscribe the {::Henchman::Worker} to.
|
114
|
+
# @param [EM::Deferrable] deferrable an {::EM::Deferrable} that will succeed with the subscription is done.
|
115
|
+
#
|
116
|
+
def subscribe_to(queue, deferrable)
|
117
|
+
Henchman.with_channel do |channel|
|
118
|
+
@consumer = AMQP::Consumer.new(channel,
|
119
|
+
queue,
|
120
|
+
queue.generate_consumer_tag(queue.name), # consumer_tag
|
121
|
+
false, # exclusive
|
122
|
+
false) # no_ack
|
123
|
+
consumer.on_delivery do |headers, data|
|
124
|
+
if queue.channel.status == :opened
|
125
|
+
begin
|
126
|
+
call(MultiJson.decode(data), headers)
|
127
|
+
rescue Exception => e
|
128
|
+
STDERR.puts e
|
129
|
+
STDERR.puts e.backtrace.join("\n")
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
consumer.consume do
|
134
|
+
deferrable.set_deferred_status :succeeded
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
#
|
140
|
+
# Make this {::Henchman::Worker} subscribe to a fanout exchange.
|
141
|
+
#
|
142
|
+
def subscribe!
|
143
|
+
deferrable = EM::DefaultDeferrable.new
|
144
|
+
Henchman.with_fanout_queue(queue_name) do |queue|
|
145
|
+
subscribe_to(queue, deferrable)
|
146
|
+
end
|
147
|
+
EM::Synchrony.sync deferrable
|
148
|
+
end
|
149
|
+
|
150
|
+
#
|
151
|
+
# Make this {::Henchman::Worker} subscribe to a direct exchange.
|
152
|
+
#
|
153
|
+
def consume!
|
154
|
+
deferrable = EM::DefaultDeferrable.new
|
155
|
+
Henchman.with_queue(queue_name) do |queue|
|
156
|
+
subscribe_to(queue, deferrable)
|
157
|
+
end
|
158
|
+
EM::Synchrony.sync deferrable
|
159
|
+
end
|
160
|
+
|
161
|
+
#
|
162
|
+
# Call this worker with some data.
|
163
|
+
#
|
164
|
+
# @param [AMQP::Header] headers the headers to handle.
|
165
|
+
# @param [Object] message the message to handle.
|
166
|
+
#
|
167
|
+
# @return [Henchman::Worker::Task] a {::Henchman::Worker::Task} for this {::Henchman::Worker}.
|
168
|
+
#
|
169
|
+
def call(message, headers = nil)
|
170
|
+
Task.new(self, headers, message).call
|
171
|
+
end
|
172
|
+
|
173
|
+
#
|
174
|
+
# Unsubscribe this {::Henchman::Worker} from its queue.
|
175
|
+
#
|
176
|
+
def unsubscribe!
|
177
|
+
deferrable = EM::DefaultDeferrable.new
|
178
|
+
consumer.cancel do
|
179
|
+
deferrable.set_deferred_status :succeeded
|
180
|
+
end
|
181
|
+
Fiber.new do
|
182
|
+
EM::Synchrony.sync deferrable
|
183
|
+
end.resume
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
end
|
data/script/consume
ADDED
data/script/enqueue
ADDED
data/script/publish
ADDED
data/script/subscribe
ADDED
@@ -0,0 +1,193 @@
|
|
1
|
+
|
2
|
+
dir = File.dirname(File.expand_path(__FILE__))
|
3
|
+
$LOAD_PATH.unshift dir + '/../lib'
|
4
|
+
|
5
|
+
require 'henchman'
|
6
|
+
|
7
|
+
require 'rspec'
|
8
|
+
|
9
|
+
describe Henchman do
|
10
|
+
|
11
|
+
context 'without amqp running' do
|
12
|
+
|
13
|
+
it 'allows testing of workers' do
|
14
|
+
val = rand(1 << 32)
|
15
|
+
found = nil
|
16
|
+
worker = Henchman::Worker.new("test.queue") do
|
17
|
+
if message["val"] == val
|
18
|
+
found = val
|
19
|
+
end
|
20
|
+
end
|
21
|
+
worker.call("val" => val)
|
22
|
+
found.should == val
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
context 'with amqp running' do
|
28
|
+
|
29
|
+
around :each do |example|
|
30
|
+
EM.synchrony do
|
31
|
+
example.run
|
32
|
+
Henchman.stop!
|
33
|
+
EM.stop
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'should consume jobs' do
|
38
|
+
val = rand(1 << 32)
|
39
|
+
found = nil
|
40
|
+
deferrable = EM::DefaultDeferrable.new
|
41
|
+
Henchman::Worker.new("test.queue") do
|
42
|
+
if message["val"] == val
|
43
|
+
found = val
|
44
|
+
deferrable.set_deferred_status :succeeded
|
45
|
+
end
|
46
|
+
nil
|
47
|
+
end.consume!
|
48
|
+
Henchman.enqueue("test.queue", :val => val)
|
49
|
+
EM::Synchrony.sync deferrable
|
50
|
+
found.should == val
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'should forward consumed jobs if they ask for it' do
|
54
|
+
val = rand(1 << 32)
|
55
|
+
found = nil
|
56
|
+
deferrable = EM::DefaultDeferrable.new
|
57
|
+
Henchman::Worker.new("test.queue2") do
|
58
|
+
if message["val"] == val
|
59
|
+
found = val
|
60
|
+
deferrable.set_deferred_status :succeeded
|
61
|
+
end
|
62
|
+
nil
|
63
|
+
end.consume!
|
64
|
+
Henchman::Worker.new("test.queue") do
|
65
|
+
if message["val"] == val
|
66
|
+
enqueue("test.queue2", "val" => val)
|
67
|
+
else
|
68
|
+
nil
|
69
|
+
end
|
70
|
+
end.consume!
|
71
|
+
Henchman.enqueue("test.queue", :val => val, :bajs => "hepp")
|
72
|
+
EM::Synchrony.sync deferrable
|
73
|
+
found.should == val
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'should be able to unsubscribe' do
|
77
|
+
val = rand(1 << 32)
|
78
|
+
found = 0
|
79
|
+
deferrable = EM::DefaultDeferrable.new
|
80
|
+
Henchman::Worker.new("test.queue") do
|
81
|
+
if message["val"] == val
|
82
|
+
found += 1
|
83
|
+
unsubscribe!
|
84
|
+
deferrable.set_deferred_status :succeeded
|
85
|
+
end
|
86
|
+
nil
|
87
|
+
end.consume!
|
88
|
+
Henchman.enqueue("test.queue", :val => val)
|
89
|
+
Henchman.enqueue("test.queue", :val => val)
|
90
|
+
EM::Synchrony.sync deferrable
|
91
|
+
EM::Synchrony.sleep 0.2
|
92
|
+
found.should == 1
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'handles errors with a global error handler' do
|
96
|
+
val = rand(1 << 32)
|
97
|
+
error = nil
|
98
|
+
deferrable = EM::DefaultDeferrable.new
|
99
|
+
Henchman::Worker.new("test.queue") do
|
100
|
+
if message["val"] == val
|
101
|
+
raise "error!"
|
102
|
+
end
|
103
|
+
nil
|
104
|
+
end.consume!
|
105
|
+
Henchman.error do
|
106
|
+
if exception.message == "error!"
|
107
|
+
error = exception
|
108
|
+
deferrable.set_deferred_status :succeeded
|
109
|
+
end
|
110
|
+
end
|
111
|
+
Henchman.enqueue("test.queue", :val => val)
|
112
|
+
EM::Synchrony.sync deferrable
|
113
|
+
error.message.should == "error!"
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'should let many consumers consume off the same queue' do
|
117
|
+
consumers = Set.new
|
118
|
+
found = 0
|
119
|
+
val = rand(1 << 32)
|
120
|
+
deferrable = EM::DefaultDeferrable.new
|
121
|
+
Henchman::Worker.new("test.queue") do
|
122
|
+
if message["val"] == val
|
123
|
+
consumers << "1"
|
124
|
+
found += 1
|
125
|
+
deferrable.set_deferred_status :succeeded if found == 10
|
126
|
+
end
|
127
|
+
nil
|
128
|
+
end.consume!
|
129
|
+
Henchman::Worker.new("test.queue") do
|
130
|
+
if message["val"] == val
|
131
|
+
consumers << "2"
|
132
|
+
found += 1
|
133
|
+
deferrable.set_deferred_status :succeeded if found == 10
|
134
|
+
end
|
135
|
+
nil
|
136
|
+
end.consume!
|
137
|
+
Henchman::Worker.new("test.queue") do
|
138
|
+
if message["val"] == val
|
139
|
+
consumers << "3"
|
140
|
+
found += 1
|
141
|
+
deferrable.set_deferred_status :succeeded if found == 10
|
142
|
+
end
|
143
|
+
nil
|
144
|
+
end.consume!
|
145
|
+
10.times do
|
146
|
+
Henchman.enqueue("test.queue", :val => val)
|
147
|
+
end
|
148
|
+
EM::Synchrony.sync deferrable
|
149
|
+
consumers.should == Set.new(["1", "2", "3"])
|
150
|
+
found.should == 10
|
151
|
+
end
|
152
|
+
|
153
|
+
it 'should let many consumers consume off the same fanout' do
|
154
|
+
consumers = Set.new
|
155
|
+
found = 0
|
156
|
+
val = rand(1 << 32)
|
157
|
+
deferrable = EM::DefaultDeferrable.new
|
158
|
+
Henchman::Worker.new("test.exchange") do
|
159
|
+
if message["val"] == val
|
160
|
+
consumers << "1"
|
161
|
+
found += 1
|
162
|
+
deferrable.set_deferred_status :succeeded if found == 30
|
163
|
+
end
|
164
|
+
nil
|
165
|
+
end.subscribe!
|
166
|
+
Henchman::Worker.new("test.exchange") do
|
167
|
+
if message["val"] == val
|
168
|
+
consumers << "2"
|
169
|
+
found += 1
|
170
|
+
deferrable.set_deferred_status :succeeded if found == 30
|
171
|
+
end
|
172
|
+
nil
|
173
|
+
end.subscribe!
|
174
|
+
Henchman::Worker.new("test.exchange") do
|
175
|
+
if message["val"] == val
|
176
|
+
consumers << "3"
|
177
|
+
found += 1
|
178
|
+
deferrable.set_deferred_status :succeeded if found == 30
|
179
|
+
end
|
180
|
+
nil
|
181
|
+
end.subscribe!
|
182
|
+
10.times do |n|
|
183
|
+
Henchman.publish("test.exchange", :val => val, :n => n)
|
184
|
+
end
|
185
|
+
EM::Synchrony.sync deferrable
|
186
|
+
consumers.should == Set.new(["1", "2", "3"])
|
187
|
+
found.should == 30
|
188
|
+
end
|
189
|
+
|
190
|
+
end
|
191
|
+
|
192
|
+
end
|
193
|
+
|
metadata
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: henchman
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Martin Bruse
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-03-14 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: amqp
|
16
|
+
requirement: &70332933283000 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70332933283000
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: em-synchrony
|
27
|
+
requirement: &70332933282540 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70332933282540
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: multi_json
|
38
|
+
requirement: &70332933281980 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70332933281980
|
47
|
+
description: A maximally simple amqp wrapper
|
48
|
+
email:
|
49
|
+
- martin@oort.se
|
50
|
+
executables: []
|
51
|
+
extensions: []
|
52
|
+
extra_rdoc_files: []
|
53
|
+
files:
|
54
|
+
- .em-console.rc
|
55
|
+
- .gitignore
|
56
|
+
- .rvmrc
|
57
|
+
- Gemfile
|
58
|
+
- README.md
|
59
|
+
- Rakefile
|
60
|
+
- henchman.gemspec
|
61
|
+
- lib/henchman.rb
|
62
|
+
- lib/henchman/worker.rb
|
63
|
+
- script/consume
|
64
|
+
- script/enqueue
|
65
|
+
- script/publish
|
66
|
+
- script/subscribe
|
67
|
+
- spec/henchman_spec.rb
|
68
|
+
homepage: https://github.com/zond/henchman
|
69
|
+
licenses: []
|
70
|
+
post_install_message:
|
71
|
+
rdoc_options: []
|
72
|
+
require_paths:
|
73
|
+
- lib
|
74
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
75
|
+
none: false
|
76
|
+
requirements:
|
77
|
+
- - ! '>='
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '0'
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
requirements: []
|
87
|
+
rubyforge_project:
|
88
|
+
rubygems_version: 1.8.15
|
89
|
+
signing_key:
|
90
|
+
specification_version: 3
|
91
|
+
summary: A maximally simple amqp wrapper
|
92
|
+
test_files:
|
93
|
+
- spec/henchman_spec.rb
|