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 ADDED
@@ -0,0 +1,3 @@
1
+
2
+ require 'bundler/setup'
3
+ require File.expand_path('lib/henchman')
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ *~
2
+ henchman-*.gem
3
+ Gemfile.lock
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm 1.9.3@henchman --create
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+
2
+ source 'http://rubygems.org/'
3
+
4
+ gemspec
5
+
6
+ group :dev do
7
+ gem 'em-console', :git => 'git://github.com/ProjectDaisy/em-console.git'
8
+ end
9
+
10
+ group :test do
11
+ gem 'rspec'
12
+ end
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
@@ -0,0 +1,11 @@
1
+ require 'bundler/setup'
2
+
3
+ require 'rspec/core/rake_task'
4
+
5
+ task :default => [:spec]
6
+
7
+ RSpec::Core::RakeTask.new(:spec) do |t|
8
+ t.rspec_opts = '--color'
9
+ t.pattern = 'spec/**/*_spec.rb'
10
+ end
11
+
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
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $: << File.join(File.dirname(__FILE__), "..", "lib")
4
+
5
+ require 'henchman'
6
+
7
+ EM.synchrony do
8
+ Henchman::Worker.new("test") do
9
+ puts message.inspect
10
+ puts headers
11
+ end.consume!
12
+ end
data/script/enqueue ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $: << File.join(File.dirname(__FILE__), "..", "lib")
4
+
5
+ require 'henchman'
6
+
7
+ EM.synchrony do
8
+ 10.times do
9
+ Henchman.enqueue("test", {:time => Time.now.to_s})
10
+ EM::Synchrony.sleep 0.5
11
+ end
12
+ EM.stop
13
+ end
data/script/publish ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $: << File.join(File.dirname(__FILE__), "..", "lib")
4
+
5
+ require 'henchman'
6
+
7
+ EM.synchrony do
8
+ 10.times do
9
+ Henchman.publish("testpub", {:time => Time.now.to_s})
10
+ EM::Synchrony.sleep 0.5
11
+ end
12
+ EM.stop
13
+ end
data/script/subscribe ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $: << File.join(File.dirname(__FILE__), "..", "lib")
4
+
5
+ require 'henchman'
6
+
7
+ EM.synchrony do
8
+ Henchman::Worker.new("testpub") do
9
+ puts message.inspect
10
+ puts headers
11
+ end.subscribe!
12
+ end
@@ -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