henchman 0.0.1

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