tchandy-minion 0.1.13
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/README.rdoc +106 -0
- data/Rakefile +24 -0
- data/VERSION +1 -0
- data/examples/math.rb +36 -0
- data/examples/sandwich.rb +32 -0
- data/examples/when.rb +28 -0
- data/lib/minion.rb +152 -0
- data/lib/minion/handler.rb +30 -0
- data/spec/base.rb +21 -0
- data/spec/enqueue_spec.rb +10 -0
- metadata +113 -0
data/README.rdoc
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
|
2
|
+
= Minion: super simple job queue over amqp
|
3
|
+
|
4
|
+
Minion makes processing jobs over AMQP simple and easy.
|
5
|
+
|
6
|
+
== Setup
|
7
|
+
|
8
|
+
Minion pulls the AMQP credentials out the environment via AMQP_URL.
|
9
|
+
|
10
|
+
$ export AMQP_URL="amqp://johndoe:abc123@localhost/my_vhost"
|
11
|
+
|
12
|
+
Alternativly you can explicitly set it programmatically like this:
|
13
|
+
|
14
|
+
Minion.amqp_url = "amqp://johndoe:abc123@localhost/my_vhost"
|
15
|
+
|
16
|
+
If no URL is supplied, Minion defaults to "amqp://guest:guest@localhost/" which
|
17
|
+
is the default credentials for Rabbitmq running locally.
|
18
|
+
|
19
|
+
== Principles
|
20
|
+
|
21
|
+
Minion treats your jobs with respect. The queues are durable and not
|
22
|
+
autodelete. When popping jobs off the queue, they will not receive an ack
|
23
|
+
until the job is done. You can rest assured that once queued, the job will not
|
24
|
+
be lost.
|
25
|
+
|
26
|
+
Sends are done synchronously and receives are done asynchronously. This allows
|
27
|
+
you to Minion.enqueue() from the console, or in a mongrel and you don't need to
|
28
|
+
worry about eventmachine. It also means that when enqueue returns, the AMQP
|
29
|
+
server has received your message. Daemons set to receive messages however use
|
30
|
+
eventmachine.
|
31
|
+
|
32
|
+
Message processing is done one at a time (prefetch 1). If you want tasks done
|
33
|
+
in parallel, run two minions.
|
34
|
+
|
35
|
+
== Push a job onto the queue
|
36
|
+
|
37
|
+
Its easy to push a job onto the queue.
|
38
|
+
|
39
|
+
Minion.enqueue("make.sandwich", { "for" => "me", "with" => "bread" })
|
40
|
+
|
41
|
+
Minion expects a queue name (and will create it if needed). The second argument
|
42
|
+
needs to be a hash.
|
43
|
+
|
44
|
+
== Processing a job
|
45
|
+
|
46
|
+
require 'minion'
|
47
|
+
|
48
|
+
include Minion
|
49
|
+
|
50
|
+
job "make.sandwich" do |args|
|
51
|
+
Sandwich.make(args["for"],args["with"])
|
52
|
+
end
|
53
|
+
|
54
|
+
== Chaining multiple steps
|
55
|
+
|
56
|
+
If you have a task that requires more than one step just pass an array of
|
57
|
+
queues when you enqueue.
|
58
|
+
|
59
|
+
Minion.enqueue([ "make.sandwich", "eat.sandwich" ], "for" => "me")
|
60
|
+
|
61
|
+
job "make.sandwich" do
|
62
|
+
## this return value is merged with for => me and sent to the next queue
|
63
|
+
{ "type" => "ham on rye" }
|
64
|
+
end
|
65
|
+
|
66
|
+
job "eat.sandwich" do |args|
|
67
|
+
puts "I have #{args["type"]} sandwich for #{args["me"]}"
|
68
|
+
end
|
69
|
+
|
70
|
+
== Conditional Processing
|
71
|
+
|
72
|
+
If you want a minion worker to only subscribe to a queue under specific
|
73
|
+
conditions there is a :when parameter that takes a lambda as an argument. For
|
74
|
+
example, if you had a queue that makes sandwiches but only if there is bread
|
75
|
+
on hand, it would be.
|
76
|
+
|
77
|
+
job "make.sandwich", :when => lambda { not Bread.out? } do
|
78
|
+
Sandwich.make
|
79
|
+
end
|
80
|
+
|
81
|
+
== Error handling
|
82
|
+
|
83
|
+
When an error is thrown in a job handler, the job is requeued to be done later
|
84
|
+
and the minion process exits. If you define an error handler, however, the
|
85
|
+
error handler is run and the job is removed from the queue.
|
86
|
+
|
87
|
+
error do |e|
|
88
|
+
puts "got an error! #{e}"
|
89
|
+
end
|
90
|
+
|
91
|
+
== Logging
|
92
|
+
|
93
|
+
Minion logs to stdout via "puts". You can specify a custom logger like this:
|
94
|
+
|
95
|
+
logger do |msg|
|
96
|
+
puts msg
|
97
|
+
end
|
98
|
+
|
99
|
+
== Meta
|
100
|
+
|
101
|
+
Created by Orion Henry
|
102
|
+
|
103
|
+
Patches contributed by Adam Wiggins, Kyle Drake
|
104
|
+
|
105
|
+
Released under the MIT License: www.opensource.org/licenses/mit-license.php
|
106
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'jeweler'
|
2
|
+
|
3
|
+
Jeweler::Tasks.new do |s|
|
4
|
+
s.name = "tchandy-minion"
|
5
|
+
s.description = "Super simple job queue over AMQP"
|
6
|
+
s.summary = s.description
|
7
|
+
s.author = "Orion Henry, Thiago Pradi"
|
8
|
+
s.email = "orion@heroku.com, tchandy@gmail.com"
|
9
|
+
s.homepage = "http://github.com/tchandy/minion"
|
10
|
+
s.rubyforge_project = "minion"
|
11
|
+
s.files = FileList["[A-Z]*", "{bin,lib,spec}/**/*"]
|
12
|
+
s.add_dependency "amqp", ">= 0.6.6"
|
13
|
+
s.add_dependency "bunny", ">= 0.6.0"
|
14
|
+
s.add_dependency "json", ">= 1.2.0"
|
15
|
+
end
|
16
|
+
|
17
|
+
Jeweler::RubyforgeTasks.new
|
18
|
+
|
19
|
+
desc 'Run specs'
|
20
|
+
task :spec do
|
21
|
+
sh 'bacon -s spec/*_spec.rb'
|
22
|
+
end
|
23
|
+
|
24
|
+
task :default => :spec
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.13
|
data/examples/math.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$:.unshift File.dirname(__FILE__) + '/../lib'
|
4
|
+
require 'rubygems'
|
5
|
+
require 'minion'
|
6
|
+
|
7
|
+
include Minion
|
8
|
+
|
9
|
+
error do |exception,queue,message,headers|
|
10
|
+
puts "got an error processing queue #{queue}"
|
11
|
+
puts exception.message
|
12
|
+
puts exception.backtrace
|
13
|
+
end
|
14
|
+
|
15
|
+
logger do |msg|
|
16
|
+
puts "--> #{msg}"
|
17
|
+
end
|
18
|
+
|
19
|
+
job "math.incr" do |args|
|
20
|
+
{ "number" => (1 + args["number"].to_i) }
|
21
|
+
end
|
22
|
+
|
23
|
+
job "math.double" do |args|
|
24
|
+
{ "number" => (2 * args["number"].to_i) }
|
25
|
+
end
|
26
|
+
|
27
|
+
job "math.square" do |args|
|
28
|
+
{ "number" => (args["number"].to_i * args["number"].to_i) }
|
29
|
+
end
|
30
|
+
|
31
|
+
job "math.print" do |args|
|
32
|
+
puts "NUMBER -----> #{args["number"]}"
|
33
|
+
end
|
34
|
+
|
35
|
+
enqueue([ "math.incr", "math.double", "math.square", "math.incr", "math.double", "math.print" ], { :number => 3 })
|
36
|
+
|
@@ -0,0 +1,32 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$:.unshift File.dirname(__FILE__) + '/../lib'
|
4
|
+
require 'rubygems'
|
5
|
+
require 'minion'
|
6
|
+
|
7
|
+
include Minion
|
8
|
+
|
9
|
+
error do |exception,queue,message,headers|
|
10
|
+
puts "got an error processing queue #{queue}"
|
11
|
+
puts exception.message
|
12
|
+
puts exception.backtrace
|
13
|
+
end
|
14
|
+
|
15
|
+
job "add.bread" do |args|
|
16
|
+
{ "bread" => "sourdough" }
|
17
|
+
end
|
18
|
+
|
19
|
+
job "add.meat" do |args|
|
20
|
+
{ "meat" => "turkey" }
|
21
|
+
end
|
22
|
+
|
23
|
+
job "add.condiments" do |args|
|
24
|
+
{ "condiments" => "mayo" }
|
25
|
+
end
|
26
|
+
|
27
|
+
job "eat.sandwich" do |args|
|
28
|
+
puts "YUM! A #{args['meat']} on #{args['bread']} sandwich with #{args['condiments']}"
|
29
|
+
end
|
30
|
+
|
31
|
+
enqueue(["add.bread", "add.meat", "add.condiments", "eat.sandwich" ])
|
32
|
+
|
data/examples/when.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$:.unshift File.dirname(__FILE__) + '/../lib'
|
4
|
+
require 'rubygems'
|
5
|
+
require 'minion'
|
6
|
+
|
7
|
+
include Minion
|
8
|
+
|
9
|
+
error do |exception,queue,message,headers|
|
10
|
+
puts "got an error processing queue #{queue}"
|
11
|
+
puts exception.message
|
12
|
+
puts exception.backtrace
|
13
|
+
end
|
14
|
+
|
15
|
+
logger do |msg|
|
16
|
+
puts "--> #{msg}"
|
17
|
+
end
|
18
|
+
|
19
|
+
$listen = true
|
20
|
+
|
21
|
+
job "do.once", :when => lambda { $listen } do |args|
|
22
|
+
puts "Do this one action - then unsubscribe..."
|
23
|
+
$listen = false
|
24
|
+
end
|
25
|
+
|
26
|
+
enqueue("do.once",[])
|
27
|
+
enqueue("do.once",[])
|
28
|
+
|
data/lib/minion.rb
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'json' unless defined? ActiveSupport::JSON
|
3
|
+
require 'mq'
|
4
|
+
require 'bunny'
|
5
|
+
require 'minion/handler'
|
6
|
+
|
7
|
+
module Minion
|
8
|
+
extend self
|
9
|
+
|
10
|
+
def url=(url)
|
11
|
+
@@config_url = url
|
12
|
+
end
|
13
|
+
|
14
|
+
def enqueue(jobs, data = {})
|
15
|
+
raise "cannot enqueue a nil job" if jobs.nil?
|
16
|
+
raise "cannot enqueue an empty job" if jobs.empty?
|
17
|
+
|
18
|
+
## jobs can be one or more jobs
|
19
|
+
if jobs.respond_to? :shift
|
20
|
+
queue = jobs.shift
|
21
|
+
data["next_job"] = jobs unless jobs.empty?
|
22
|
+
else
|
23
|
+
queue = jobs
|
24
|
+
end
|
25
|
+
|
26
|
+
log "send: #{queue}:#{data.to_json}"
|
27
|
+
bunny.queue(queue, :durable => true, :auto_delete => false).publish(data.to_json)
|
28
|
+
end
|
29
|
+
|
30
|
+
def log(msg)
|
31
|
+
@@logger ||= proc { |m| puts "#{Time.now} :minion: #{m}" }
|
32
|
+
@@logger.call(msg)
|
33
|
+
end
|
34
|
+
|
35
|
+
def error(&blk)
|
36
|
+
@@error_handler = blk
|
37
|
+
end
|
38
|
+
|
39
|
+
def logger(&blk)
|
40
|
+
@@logger = blk
|
41
|
+
end
|
42
|
+
|
43
|
+
def job(queue, options = {}, &blk)
|
44
|
+
handler = Minion::Handler.new queue
|
45
|
+
handler.when = options[:when] if options[:when]
|
46
|
+
handler.unsub = lambda do
|
47
|
+
log "unsubscribing to #{queue}"
|
48
|
+
MQ.queue(queue).unsubscribe
|
49
|
+
end
|
50
|
+
handler.sub = lambda do
|
51
|
+
@@ack = true
|
52
|
+
log "subscribing to #{queue}"
|
53
|
+
MQ.queue(queue).subscribe(:ack => true) do |h,m|
|
54
|
+
return if AMQP.closing?
|
55
|
+
begin
|
56
|
+
log "recv: #{queue}:#{m}"
|
57
|
+
|
58
|
+
args = decode_json(m)
|
59
|
+
|
60
|
+
result = yield(args)
|
61
|
+
|
62
|
+
next_job(args, result)
|
63
|
+
rescue Object => e
|
64
|
+
raise unless error_handler
|
65
|
+
error_handler.call(e,queue,m,h)
|
66
|
+
end
|
67
|
+
h.ack if should_ack?
|
68
|
+
check_all
|
69
|
+
end
|
70
|
+
end
|
71
|
+
@@handlers ||= []
|
72
|
+
at_exit { Minion.run } if @@handlers.size == 0
|
73
|
+
@@handlers << handler
|
74
|
+
end
|
75
|
+
|
76
|
+
def decode_json(string)
|
77
|
+
if defined? ActiveSupport::JSON
|
78
|
+
ActiveSupport::JSON.decode string
|
79
|
+
else
|
80
|
+
JSON.load string
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def check_all
|
85
|
+
@@handlers.each { |h| h.check }
|
86
|
+
end
|
87
|
+
|
88
|
+
def should_ack?
|
89
|
+
@@ack
|
90
|
+
end
|
91
|
+
|
92
|
+
def not_ack()
|
93
|
+
@@ack = false
|
94
|
+
end
|
95
|
+
|
96
|
+
def run
|
97
|
+
log "Starting minion"
|
98
|
+
|
99
|
+
Signal.trap('INT') { AMQP.stop{ EM.stop } }
|
100
|
+
Signal.trap('TERM'){ AMQP.stop{ EM.stop } }
|
101
|
+
|
102
|
+
EM.run do
|
103
|
+
AMQP.start(amqp_config) do
|
104
|
+
MQ.prefetch(1)
|
105
|
+
check_all
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def amqp_url
|
111
|
+
@@amqp_url ||= ENV["AMQP_URL"] || "amqp://guest:guest@localhost/"
|
112
|
+
end
|
113
|
+
|
114
|
+
def amqp_url=(url)
|
115
|
+
@@amqp_url = url
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
def amqp_config
|
121
|
+
uri = URI.parse(amqp_url)
|
122
|
+
{
|
123
|
+
:vhost => uri.path,
|
124
|
+
:host => uri.host,
|
125
|
+
:user => uri.user,
|
126
|
+
:port => (uri.port || 5672),
|
127
|
+
:pass => uri.password
|
128
|
+
}
|
129
|
+
rescue
|
130
|
+
raise "invalid AMQP_URL: #{uri.inspect} (#{e})"
|
131
|
+
end
|
132
|
+
|
133
|
+
def new_bunny
|
134
|
+
b = Bunny.new(amqp_config)
|
135
|
+
b.start
|
136
|
+
b
|
137
|
+
end
|
138
|
+
|
139
|
+
def bunny
|
140
|
+
@@bunny ||= new_bunny
|
141
|
+
end
|
142
|
+
|
143
|
+
def next_job(args, response)
|
144
|
+
queue = args.delete("next_job")
|
145
|
+
enqueue(queue,args.merge(response)) if queue and not queue.empty?
|
146
|
+
end
|
147
|
+
|
148
|
+
def error_handler
|
149
|
+
@@error_handler ||= nil
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Minion
|
2
|
+
class Handler
|
3
|
+
attr_accessor :queue, :sub, :unsub, :when, :on
|
4
|
+
def initialize(queue)
|
5
|
+
@queue = queue
|
6
|
+
@when = lambda { true }
|
7
|
+
@sub = lambda {}
|
8
|
+
@unsub = lambda {}
|
9
|
+
@on = false
|
10
|
+
end
|
11
|
+
|
12
|
+
def should_sub?
|
13
|
+
@when.call
|
14
|
+
end
|
15
|
+
|
16
|
+
def check
|
17
|
+
if should_sub?
|
18
|
+
@sub.call unless @on
|
19
|
+
@on = true
|
20
|
+
else
|
21
|
+
@unsub.call if @on
|
22
|
+
@on = false
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_s
|
27
|
+
"<handler queue=#{@queue} on=#{@on}>"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/spec/base.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../lib/minion'
|
2
|
+
|
3
|
+
require 'bacon'
|
4
|
+
require 'mocha/standalone'
|
5
|
+
require 'mocha/object'
|
6
|
+
|
7
|
+
class Bacon::Context
|
8
|
+
include Mocha::API
|
9
|
+
|
10
|
+
def initialize(name, &block)
|
11
|
+
@name = name
|
12
|
+
@before, @after = [
|
13
|
+
[lambda { mocha_setup }],
|
14
|
+
[lambda { mocha_verify ; mocha_teardown }]
|
15
|
+
]
|
16
|
+
@block = block
|
17
|
+
end
|
18
|
+
|
19
|
+
def xit(desc, &bk)
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/base'
|
2
|
+
|
3
|
+
describe Minion do
|
4
|
+
it "should throw an exception when passed a nil queue" do
|
5
|
+
lambda { Minion.enqueue(nil, {}) }.should.raise(RuntimeError)
|
6
|
+
end
|
7
|
+
it "should throw an exception when passed an empty queue" do
|
8
|
+
lambda { Minion.enqueue([], {}) }.should.raise(RuntimeError)
|
9
|
+
end
|
10
|
+
end
|
metadata
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tchandy-minion
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 13
|
9
|
+
version: 0.1.13
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Orion Henry, Thiago Pradi
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-05-13 00:00:00 -03:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: amqp
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 0
|
29
|
+
- 6
|
30
|
+
- 6
|
31
|
+
version: 0.6.6
|
32
|
+
type: :runtime
|
33
|
+
version_requirements: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: bunny
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
segments:
|
42
|
+
- 0
|
43
|
+
- 6
|
44
|
+
- 0
|
45
|
+
version: 0.6.0
|
46
|
+
type: :runtime
|
47
|
+
version_requirements: *id002
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: json
|
50
|
+
prerelease: false
|
51
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
segments:
|
56
|
+
- 1
|
57
|
+
- 2
|
58
|
+
- 0
|
59
|
+
version: 1.2.0
|
60
|
+
type: :runtime
|
61
|
+
version_requirements: *id003
|
62
|
+
description: Super simple job queue over AMQP
|
63
|
+
email: orion@heroku.com, tchandy@gmail.com
|
64
|
+
executables: []
|
65
|
+
|
66
|
+
extensions: []
|
67
|
+
|
68
|
+
extra_rdoc_files:
|
69
|
+
- README.rdoc
|
70
|
+
files:
|
71
|
+
- README.rdoc
|
72
|
+
- Rakefile
|
73
|
+
- VERSION
|
74
|
+
- lib/minion.rb
|
75
|
+
- lib/minion/handler.rb
|
76
|
+
- spec/base.rb
|
77
|
+
- spec/enqueue_spec.rb
|
78
|
+
has_rdoc: true
|
79
|
+
homepage: http://github.com/tchandy/minion
|
80
|
+
licenses: []
|
81
|
+
|
82
|
+
post_install_message:
|
83
|
+
rdoc_options:
|
84
|
+
- --charset=UTF-8
|
85
|
+
require_paths:
|
86
|
+
- lib
|
87
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
88
|
+
requirements:
|
89
|
+
- - ">="
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
segments:
|
92
|
+
- 0
|
93
|
+
version: "0"
|
94
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
segments:
|
99
|
+
- 0
|
100
|
+
version: "0"
|
101
|
+
requirements: []
|
102
|
+
|
103
|
+
rubyforge_project: minion
|
104
|
+
rubygems_version: 1.3.6
|
105
|
+
signing_key:
|
106
|
+
specification_version: 3
|
107
|
+
summary: Super simple job queue over AMQP
|
108
|
+
test_files:
|
109
|
+
- spec/base.rb
|
110
|
+
- spec/enqueue_spec.rb
|
111
|
+
- examples/math.rb
|
112
|
+
- examples/sandwich.rb
|
113
|
+
- examples/when.rb
|