mb-minion 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +30 -0
- data/examples/batch.rb +31 -0
- data/examples/batch_wait.rb +36 -0
- data/examples/batch_wait_2.rb +52 -0
- data/examples/math.rb +42 -0
- data/examples/sandwich.rb +38 -0
- data/examples/sandwich_batch.rb +44 -0
- data/examples/when.rb +34 -0
- data/lib/mb-ext/string.rb +7 -0
- data/lib/mb-minion.rb +231 -0
- data/lib/mb-minion/handler.rb +177 -0
- data/lib/mb-minion/message.rb +58 -0
- data/lib/mb-minion/version.rb +4 -0
- data/spec/minion/handler_spec.rb +147 -0
- data/spec/minion/message_spec.rb +38 -0
- data/spec/minion_spec.rb +238 -0
- data/spec/spec_helper.rb +11 -0
- metadata +201 -0
data/Rakefile
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require "bundler"
|
2
|
+
Bundler.setup
|
3
|
+
|
4
|
+
require "rake"
|
5
|
+
require "rake/rdoctask"
|
6
|
+
require "rspec"
|
7
|
+
require "rspec/core/rake_task"
|
8
|
+
|
9
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
|
10
|
+
require "mb-minion/version"
|
11
|
+
|
12
|
+
task :build do
|
13
|
+
system "gem build mb-minion.gemspec"
|
14
|
+
end
|
15
|
+
|
16
|
+
task :install => :build do
|
17
|
+
system "sudo gem install mb-minion-#{Minion::VERSION}.gem"
|
18
|
+
end
|
19
|
+
|
20
|
+
task :release => :build do
|
21
|
+
system "git tag -a #{Minion::VERSION} -m 'Tagging #{Minion::VERSION}'"
|
22
|
+
system "git push --tags"
|
23
|
+
system "gem push mb-minion-#{Minion::VERSION}.gem"
|
24
|
+
end
|
25
|
+
|
26
|
+
Rspec::Core::RakeTask.new(:spec) do |spec|
|
27
|
+
spec.pattern = "spec/**/*_spec.rb"
|
28
|
+
end
|
29
|
+
|
30
|
+
task :default => :spec
|
data/examples/batch.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
#
|
4
|
+
# This example illustrates the use of batching
|
5
|
+
# messages together. This way work can be
|
6
|
+
# distributed in small chunks, but worked on
|
7
|
+
# in larger groups
|
8
|
+
#
|
9
|
+
|
10
|
+
$:.unshift File.dirname(__FILE__) + '/../lib'
|
11
|
+
require 'rubygems'
|
12
|
+
require 'mb-minion'
|
13
|
+
|
14
|
+
include Minion
|
15
|
+
|
16
|
+
error do |exception,queue,message,headers|
|
17
|
+
puts "got an error processing queue #{queue}"
|
18
|
+
puts exception.message
|
19
|
+
puts exception.backtrace
|
20
|
+
end
|
21
|
+
|
22
|
+
logger do |msg|
|
23
|
+
puts "--> #{msg}"
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
job "do.many", :batch_size => 10 do |msg|
|
28
|
+
puts "got #{msg.batch.size} messages"
|
29
|
+
end
|
30
|
+
|
31
|
+
27.times{ Minion.enqueue 'do.many', {"something" => true} }
|
@@ -0,0 +1,36 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
#
|
4
|
+
# This example illustrates the use batching
|
5
|
+
# when we want to wait until the hit an exact
|
6
|
+
# number of messages before running
|
7
|
+
#
|
8
|
+
|
9
|
+
$:.unshift File.dirname(__FILE__) + '/../lib'
|
10
|
+
require 'rubygems'
|
11
|
+
require 'mb-minion'
|
12
|
+
|
13
|
+
include Minion
|
14
|
+
|
15
|
+
error do |exception,queue,message,headers|
|
16
|
+
puts "got an error processing queue #{queue}"
|
17
|
+
puts exception.message
|
18
|
+
puts exception.backtrace
|
19
|
+
end
|
20
|
+
|
21
|
+
logger do |msg|
|
22
|
+
puts "--> #{msg}"
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
job "do.only_in_tens", :batch_size => 10, :wait => true do |msg|
|
27
|
+
puts "got #{msg.batch.size} messages"
|
28
|
+
end
|
29
|
+
|
30
|
+
# We won't run this, but just so you know, this is the
|
31
|
+
# same result as the "batch.rb" example
|
32
|
+
job "do.in_tens_or_emtpy", :batch_size => 10, :wait => false do |msg|
|
33
|
+
puts "got #{msg.batch.size} messages"
|
34
|
+
end
|
35
|
+
|
36
|
+
27.times{ Minion.enqueue 'do.only_in_tens', {"something" => true} }
|
@@ -0,0 +1,52 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
#
|
4
|
+
# This example illustrates the use batching
|
5
|
+
# when we want to wait some # of seconds for an
|
6
|
+
# exact number of messages before running,
|
7
|
+
# but if we don't get that many, we'll go ahead
|
8
|
+
# anyways.
|
9
|
+
#
|
10
|
+
# This prevents some odd batch counts, but allows
|
11
|
+
# for flexibility of the queue size
|
12
|
+
#
|
13
|
+
|
14
|
+
$:.unshift File.dirname(__FILE__) + '/../lib'
|
15
|
+
require 'rubygems'
|
16
|
+
require 'mb-minion'
|
17
|
+
|
18
|
+
include Minion
|
19
|
+
|
20
|
+
error do |exception,queue,message,headers|
|
21
|
+
puts "got an error processing queue #{queue}"
|
22
|
+
puts exception.message
|
23
|
+
puts exception.backtrace
|
24
|
+
end
|
25
|
+
|
26
|
+
logger do |msg|
|
27
|
+
puts "--> #{msg}"
|
28
|
+
end
|
29
|
+
|
30
|
+
Thread.new do
|
31
|
+
puts "------------------------------------------"
|
32
|
+
puts "First, no waiting with artificial delays"
|
33
|
+
puts "------------------------------------------"
|
34
|
+
3.times{ Minion.enqueue 'do.no_waiting', {"something" => true} }
|
35
|
+
sleep 0.2
|
36
|
+
4.times{ Minion.enqueue 'do.no_waiting', {"something" => true} }
|
37
|
+
sleep 5
|
38
|
+
puts "------------------------------------------"
|
39
|
+
puts "Now if we give it a second, all is well"
|
40
|
+
puts "------------------------------------------"
|
41
|
+
3.times{ Minion.enqueue 'do.in_tens_or_wait_2', {"something" => true} }
|
42
|
+
sleep 0.2
|
43
|
+
4.times{ Minion.enqueue 'do.in_tens_or_wait_2', {"something" => true} }
|
44
|
+
end
|
45
|
+
|
46
|
+
job "do.no_waiting", :batch_size => 10 do |msg|
|
47
|
+
puts "got #{msg.batch.size} messages"
|
48
|
+
end
|
49
|
+
|
50
|
+
job "do.in_tens_or_wait_2", :batch_size => 10, :wait => 2 do |msg|
|
51
|
+
puts "got #{msg.batch.size} messages"
|
52
|
+
end
|
data/examples/math.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
#
|
4
|
+
# This example illustrates the use of chaining
|
5
|
+
# callbacks to create building blocks with the
|
6
|
+
# message content being just an integer
|
7
|
+
#
|
8
|
+
|
9
|
+
$:.unshift File.dirname(__FILE__) + '/../lib'
|
10
|
+
require 'rubygems'
|
11
|
+
require 'minion'
|
12
|
+
|
13
|
+
include Minion
|
14
|
+
|
15
|
+
error do |exception,queue,message,headers|
|
16
|
+
puts "got an error processing queue #{queue}"
|
17
|
+
puts exception.message
|
18
|
+
puts exception.backtrace
|
19
|
+
end
|
20
|
+
|
21
|
+
logger do |msg|
|
22
|
+
puts "--> #{msg}"
|
23
|
+
end
|
24
|
+
|
25
|
+
job "math.incr" do |msg|
|
26
|
+
msg.content.to_i + 1
|
27
|
+
end
|
28
|
+
|
29
|
+
job "math.double" do |msg|
|
30
|
+
msg.content.to_i * 2
|
31
|
+
end
|
32
|
+
|
33
|
+
job "math.square" do |msg|
|
34
|
+
msg.content.to_i * msg.content.to_i
|
35
|
+
end
|
36
|
+
|
37
|
+
job "math.print" do |msg|
|
38
|
+
puts "NUMBER -----> #{msg.content}"
|
39
|
+
end
|
40
|
+
|
41
|
+
enqueue([ "math.incr", "math.double", "math.square", "math.incr", "math.double", "math.print" ], 3)
|
42
|
+
|
@@ -0,0 +1,38 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
#
|
4
|
+
# This example illustrates the use of chaining
|
5
|
+
# callbacks to create building blocks with the
|
6
|
+
# message content being just a hash
|
7
|
+
#
|
8
|
+
|
9
|
+
$:.unshift File.dirname(__FILE__) + '/../lib'
|
10
|
+
require 'rubygems'
|
11
|
+
require 'mb-minion'
|
12
|
+
|
13
|
+
include Minion
|
14
|
+
|
15
|
+
error do |exception,queue,message,headers|
|
16
|
+
puts "got an error processing queue #{queue}"
|
17
|
+
puts exception.message
|
18
|
+
puts exception.backtrace
|
19
|
+
end
|
20
|
+
|
21
|
+
job "add.bread" do |msg|
|
22
|
+
msg.content.merge("bread" => "sourdough")
|
23
|
+
end
|
24
|
+
|
25
|
+
job "add.meat" do |msg|
|
26
|
+
msg.content.merge("meat" => "turkey")
|
27
|
+
end
|
28
|
+
|
29
|
+
job "add.condiments" do |msg|
|
30
|
+
msg.content.merge("condiments" => "mayo")
|
31
|
+
end
|
32
|
+
|
33
|
+
job "eat.sandwich" do |msg|
|
34
|
+
puts "YUM! A #{msg['meat']} on #{msg['bread']} sandwich with #{msg['condiments']}"
|
35
|
+
end
|
36
|
+
|
37
|
+
enqueue(["add.bread", "add.meat", "add.condiments", "eat.sandwich" ])
|
38
|
+
|
@@ -0,0 +1,44 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
#
|
4
|
+
# This example illustrates the use batching
|
5
|
+
# and chaining of callbacks in a single example.
|
6
|
+
# Another example of doing a map-reduce operation.
|
7
|
+
#
|
8
|
+
|
9
|
+
$:.unshift File.dirname(__FILE__) + '/../lib'
|
10
|
+
require 'rubygems'
|
11
|
+
require 'mb-minion'
|
12
|
+
|
13
|
+
include Minion
|
14
|
+
|
15
|
+
error do |exception,queue,message,headers|
|
16
|
+
puts "got an error processing queue #{queue}"
|
17
|
+
puts exception.message
|
18
|
+
puts exception.backtrace
|
19
|
+
end
|
20
|
+
|
21
|
+
MAKINGS = {
|
22
|
+
'bread' => %w[wheat rye sourdough white pumpernickle],
|
23
|
+
'meat' => %w[turkey ham pastrami salami],
|
24
|
+
'condiments' => %w[mayo mustard relish sourkraut]
|
25
|
+
}
|
26
|
+
|
27
|
+
job "add.bread", :batch_size => 5 do |msg|
|
28
|
+
puts "Puts making #{msg.batch.size} sandwiches"
|
29
|
+
msg.batch.map{|s| s.merge("bread" => MAKINGS['bread'].sample)}
|
30
|
+
end
|
31
|
+
|
32
|
+
job "add.meat" do |msg|
|
33
|
+
msg.map{|s| s.merge("meat" => MAKINGS['meat'].sample)}
|
34
|
+
end
|
35
|
+
|
36
|
+
job "add.condiments" do |msg|
|
37
|
+
msg.map{|s| s.merge("condiments" => MAKINGS['condiments'].sample)}
|
38
|
+
end
|
39
|
+
|
40
|
+
job "eat.sandwich" do |msg|
|
41
|
+
msg.each_with_index{|s, i| puts "SANDWICH ##{i}: A #{s['meat']} on #{s['bread']} sandwich with #{s['condiments']}"}
|
42
|
+
end
|
43
|
+
|
44
|
+
15.times{ enqueue(["add.bread", "add.meat", "add.condiments", "eat.sandwich" ]) }
|
data/examples/when.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
#
|
4
|
+
# This example illustrates the use of the :when
|
5
|
+
# parameter to allow/disallow subscription to the
|
6
|
+
# queue
|
7
|
+
#
|
8
|
+
|
9
|
+
$:.unshift File.dirname(__FILE__) + '/../lib'
|
10
|
+
require 'rubygems'
|
11
|
+
require 'mb-minion'
|
12
|
+
|
13
|
+
include Minion
|
14
|
+
|
15
|
+
error do |exception,queue,message,headers|
|
16
|
+
puts "got an error processing queue #{queue}"
|
17
|
+
puts exception.message
|
18
|
+
puts exception.backtrace
|
19
|
+
end
|
20
|
+
|
21
|
+
logger do |msg|
|
22
|
+
puts "--> #{msg}"
|
23
|
+
end
|
24
|
+
|
25
|
+
$listen = true
|
26
|
+
|
27
|
+
job "do.once", :when => lambda { $listen } do |args|
|
28
|
+
puts "Do this one action - then unsubscribe..."
|
29
|
+
$listen = false
|
30
|
+
end
|
31
|
+
|
32
|
+
enqueue("do.once",[])
|
33
|
+
enqueue("do.once",[])
|
34
|
+
|
data/lib/mb-minion.rb
ADDED
@@ -0,0 +1,231 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "amqp"
|
3
|
+
require "bunny"
|
4
|
+
require "json" unless defined? ActiveSupport::JSON
|
5
|
+
require "uri"
|
6
|
+
require "mb-minion/handler"
|
7
|
+
require "mb-minion/version"
|
8
|
+
require "mb-minion/message"
|
9
|
+
require "mb-ext/string"
|
10
|
+
|
11
|
+
module Minion
|
12
|
+
extend self
|
13
|
+
|
14
|
+
# Handle when an error gets raised.
|
15
|
+
#
|
16
|
+
# @example Handle the error.
|
17
|
+
# Minion.error(exception)
|
18
|
+
#
|
19
|
+
# @param [ Exception ] exception The error that was raised.
|
20
|
+
#
|
21
|
+
# @return [ Object ] The output og the error handler block.
|
22
|
+
def alert(exception)
|
23
|
+
raise(exception) unless error_handling
|
24
|
+
error_handling.call(exception)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Gets the hash of configuration options.
|
28
|
+
#
|
29
|
+
# @example Get the configuration hash.
|
30
|
+
# Minion.config
|
31
|
+
#
|
32
|
+
# @return [ Hash ] The configuration options.
|
33
|
+
def config
|
34
|
+
uri = URI.parse(url)
|
35
|
+
{
|
36
|
+
:vhost => uri.path,
|
37
|
+
:host => uri.host,
|
38
|
+
:user => uri.user,
|
39
|
+
:port => (uri.port || 5672),
|
40
|
+
:pass => uri.password
|
41
|
+
}
|
42
|
+
rescue Object => e
|
43
|
+
raise("invalid AMQP_URL: #{uri.inspect} (#{e})")
|
44
|
+
end
|
45
|
+
|
46
|
+
# Add content to the supplied queue or queues. The hash will get converted to
|
47
|
+
# JSON and placed on the queue as the JSON string.
|
48
|
+
#
|
49
|
+
# @example Place data on a single queue.
|
50
|
+
# Minion.enqueue("queue.name", { field: "value" })
|
51
|
+
#
|
52
|
+
# @example Place data on multiple queues.
|
53
|
+
# Minion.enqueue([ "queue.first", "queue.second" ], { field: "value" })
|
54
|
+
#
|
55
|
+
# @param [ String, Array<String> ] name The name or names of the queues.
|
56
|
+
# @param [ Hash ] data The payload to send.
|
57
|
+
#
|
58
|
+
# @raise [ RuntimeError ] If the name is nil or empty.
|
59
|
+
def enqueue(queues, data = {})
|
60
|
+
raise "Cannot enqueue an empty or nil name" if queues.nil? || queues.empty?
|
61
|
+
# Wrap raw data when we receive it
|
62
|
+
data = {'content' => data} unless data.class == Hash && data['content']
|
63
|
+
if queues.respond_to? :shift
|
64
|
+
queue = queues.shift
|
65
|
+
data['callbacks'] = queues
|
66
|
+
else
|
67
|
+
queue = queues
|
68
|
+
end
|
69
|
+
|
70
|
+
# @todo: Durran: Any multi-byte character in the JSON causes a bad_payload
|
71
|
+
# error on the rabbitmq side. It seems a fix in the old amqp gem
|
72
|
+
# regressed in the new fork.
|
73
|
+
encoded = JSON.dump(data).force_encoding("ISO-8859-1")
|
74
|
+
|
75
|
+
Minion.info("Send: #{queue}:#{encoded}")
|
76
|
+
connect do |bunny|
|
77
|
+
q = bunny.queue(queue, :durable => true, :auto_delete => false)
|
78
|
+
e = bunny.exchange('') # Connect to default exchange
|
79
|
+
e.publish(encoded, :key => q.name)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Get the message count for a specific queue
|
84
|
+
#
|
85
|
+
# @example Get the message count for queue 'minion.test'.
|
86
|
+
# Minion.message_count('minion.test')
|
87
|
+
#
|
88
|
+
# @return [ Fixnum ] the number of messages
|
89
|
+
def message_count(queue)
|
90
|
+
connect do |bunny|
|
91
|
+
return bunny.queue(queue, :durable => true, :auto_delete => false).message_count
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Define an optional method of changing the ways errors get handled.
|
96
|
+
#
|
97
|
+
# @example Define a custom error handler.
|
98
|
+
# Minion.error do |e|
|
99
|
+
# puts "I got an error - #{e.message}"
|
100
|
+
# end
|
101
|
+
#
|
102
|
+
# @param [ Proc ] block The block that will handle the error.
|
103
|
+
def error(&block)
|
104
|
+
@@error_handling = block
|
105
|
+
end
|
106
|
+
|
107
|
+
# Runs each of the handlers.
|
108
|
+
#
|
109
|
+
# @example Check all handlers.
|
110
|
+
# Minion.check_handlers
|
111
|
+
def execute_handlers
|
112
|
+
@@handlers.each { |handler| handler.execute }
|
113
|
+
end
|
114
|
+
|
115
|
+
# Log the supplied information message.
|
116
|
+
#
|
117
|
+
# @example Log the message.
|
118
|
+
# Minion.info("something happened")
|
119
|
+
#
|
120
|
+
# @return [ Object ] The output of the logging block.
|
121
|
+
def info(message)
|
122
|
+
logging.call(message)
|
123
|
+
end
|
124
|
+
|
125
|
+
# Sets up a subscriber to a queue to process jobs.
|
126
|
+
#
|
127
|
+
# @example Set up the subscriber.
|
128
|
+
# Minion.job "my.queue.name" do |attributes|
|
129
|
+
# puts "Here's the message data: #{attributes"
|
130
|
+
# end
|
131
|
+
#
|
132
|
+
# @param [ String ] queue The queue to subscribe to.
|
133
|
+
# @param [ Hash ] options Options for the subscriber.
|
134
|
+
#
|
135
|
+
# @option options [ lambda ] :when Conditionally process the job.
|
136
|
+
# @option options [ boolean ] :ack Should we automatically ack the message?
|
137
|
+
def job(queue, options = {}, &block)
|
138
|
+
Minion::Handler.new(queue, block, options).tap do |handler|
|
139
|
+
@@handlers ||= []
|
140
|
+
at_exit { Minion.run } if @@handlers.size == 0
|
141
|
+
@@handlers << handler
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Define an optional method of changing the ways logging is handled.
|
146
|
+
#
|
147
|
+
# @example Define a custom logger.
|
148
|
+
# Minion.logger do |message|
|
149
|
+
# puts "Something did something - #{message}"
|
150
|
+
# end
|
151
|
+
#
|
152
|
+
# @param [ Proc ] block The block that will handle the logging.
|
153
|
+
def logger(&block)
|
154
|
+
@@logging = block
|
155
|
+
end
|
156
|
+
|
157
|
+
# Runs the minion subscribers.
|
158
|
+
#
|
159
|
+
# @example Run the subscribers.
|
160
|
+
# Minion.run
|
161
|
+
def run
|
162
|
+
Minion.info("Starting minion")
|
163
|
+
Signal.trap("INT") { AMQP.stop { EM.stop } }
|
164
|
+
Signal.trap("TERM") { AMQP.stop { EM.stop } }
|
165
|
+
|
166
|
+
EM.run do
|
167
|
+
AMQP.start(config) do
|
168
|
+
AMQP::Channel.new.prefetch(1)
|
169
|
+
execute_handlers
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
# Get the url for the amqp server.
|
175
|
+
#
|
176
|
+
# @example Get the url.
|
177
|
+
# Minion.url
|
178
|
+
#
|
179
|
+
# @return [ String ] The url.
|
180
|
+
def url
|
181
|
+
@@url ||= (ENV["AMQP_URL"] || "amqp://guest:guest@localhost/")
|
182
|
+
end
|
183
|
+
|
184
|
+
# Set the url to the amqp server.
|
185
|
+
#
|
186
|
+
# @example Set the url.
|
187
|
+
# Minion.url = "amqp://user:password@host:port/vhost"
|
188
|
+
#
|
189
|
+
# @return [ String ] The new url.
|
190
|
+
def url=(url)
|
191
|
+
@@url = url
|
192
|
+
end
|
193
|
+
|
194
|
+
private
|
195
|
+
|
196
|
+
# Get the bunny instance which is used for the synchronous communication.
|
197
|
+
#
|
198
|
+
# @example Get the bunny.
|
199
|
+
# Minion.bunny
|
200
|
+
#
|
201
|
+
# @return [ Bunny ] The new bunny, all configured.
|
202
|
+
def connect
|
203
|
+
Bunny.new(config).tap do |bunny|
|
204
|
+
bunny.start
|
205
|
+
yield(bunny) if block_given?
|
206
|
+
bunny.stop
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# Get the error handler for this class.
|
211
|
+
#
|
212
|
+
# @example Get the error handler.
|
213
|
+
# Minion.error_handling
|
214
|
+
#
|
215
|
+
# @return [ lambda, nil ] The handler or nil.
|
216
|
+
def error_handling
|
217
|
+
@@error_handling ||= nil
|
218
|
+
end
|
219
|
+
|
220
|
+
# Get the logger for this class. If nothing had been specified will default
|
221
|
+
# to a basic time/message print.
|
222
|
+
#
|
223
|
+
# @example Get the logger.
|
224
|
+
# Minion.logging
|
225
|
+
#
|
226
|
+
# @return [ lambda ] The logger.
|
227
|
+
def logging
|
228
|
+
@@logging ||= lambda { |msg| puts("#{Time.now} :minion: #{msg}") }
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|