minion_kim 0.1.16
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +106 -0
- data/Rakefile +21 -0
- data/VERSION +1 -0
- data/lib/minion/handler.rb +30 -0
- data/lib/minion.rb +144 -0
- data/spec/base.rb +21 -0
- data/spec/enqueue_spec.rb +10 -0
- metadata +87 -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,21 @@
|
|
1
|
+
require 'jeweler'
|
2
|
+
|
3
|
+
Jeweler::Tasks.new do |s|
|
4
|
+
s.name = "minion_kim"
|
5
|
+
s.description = "Super simple job queue over AMQP"
|
6
|
+
s.summary = "Fork containing one simple patch. Will not be maintained if pull request #6 gets accepted upstream"
|
7
|
+
s.authors = ["Orion Henry", "Kim Altintop"]
|
8
|
+
s.email = "kim@soundcloud.com"
|
9
|
+
s.homepage = "http://github.com/kim/minion"
|
10
|
+
s.files = FileList["[A-Z]*", "{bin,lib,spec}/**/*"]
|
11
|
+
s.add_dependency "amqp", ">= 0.6.7"
|
12
|
+
s.add_dependency "bunny", ">= 0.6.0"
|
13
|
+
s.add_dependency "json", ">= 1.2.0"
|
14
|
+
end
|
15
|
+
|
16
|
+
desc 'Run specs'
|
17
|
+
task :spec do
|
18
|
+
sh 'bacon -s spec/*_spec.rb'
|
19
|
+
end
|
20
|
+
|
21
|
+
task :default => :spec
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.16
|
@@ -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/lib/minion.rb
ADDED
@@ -0,0 +1,144 @@
|
|
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
|
+
encoded = JSON.dump(data)
|
27
|
+
log "send: #{queue}:#{encoded}"
|
28
|
+
bunny.queue(queue, :durable => true, :auto_delete => false).publish(encoded)
|
29
|
+
end
|
30
|
+
|
31
|
+
def log(msg)
|
32
|
+
@@logger ||= proc { |m| puts "#{Time.now} :minion: #{m}" }
|
33
|
+
@@logger.call(msg)
|
34
|
+
end
|
35
|
+
|
36
|
+
def error(&blk)
|
37
|
+
@@error_handler = blk
|
38
|
+
end
|
39
|
+
|
40
|
+
def logger(&blk)
|
41
|
+
@@logger = blk
|
42
|
+
end
|
43
|
+
|
44
|
+
def job(queue, options = {}, &blk)
|
45
|
+
handler = Minion::Handler.new queue
|
46
|
+
handler.when = options[:when] if options[:when]
|
47
|
+
handler.unsub = lambda do
|
48
|
+
log "unsubscribing to #{queue}"
|
49
|
+
MQ.queue(queue, :durable => true, :auto_delete => false).unsubscribe
|
50
|
+
end
|
51
|
+
handler.sub = lambda do
|
52
|
+
log "subscribing to #{queue}"
|
53
|
+
MQ.queue(queue, :durable => true, :auto_delete => false).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,h)
|
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
|
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 run
|
89
|
+
log "Starting minion"
|
90
|
+
|
91
|
+
Signal.trap('INT') { AMQP.stop{ EM.stop } }
|
92
|
+
Signal.trap('TERM'){ AMQP.stop{ EM.stop } }
|
93
|
+
|
94
|
+
EM.run do
|
95
|
+
AMQP.start(amqp_config) do
|
96
|
+
MQ.prefetch(1)
|
97
|
+
check_all
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def amqp_url
|
103
|
+
@@amqp_url ||= ENV["AMQP_URL"] || "amqp://guest:guest@localhost/"
|
104
|
+
end
|
105
|
+
|
106
|
+
def amqp_url=(url)
|
107
|
+
@@amqp_url = url
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
def amqp_config
|
113
|
+
uri = URI.parse(amqp_url)
|
114
|
+
{
|
115
|
+
:vhost => uri.path,
|
116
|
+
:host => uri.host,
|
117
|
+
:user => uri.user,
|
118
|
+
:port => (uri.port || 5672),
|
119
|
+
:pass => uri.password
|
120
|
+
}
|
121
|
+
rescue Object => e
|
122
|
+
raise "invalid AMQP_URL: #{uri.inspect} (#{e})"
|
123
|
+
end
|
124
|
+
|
125
|
+
def new_bunny
|
126
|
+
b = Bunny.new(amqp_config)
|
127
|
+
b.start
|
128
|
+
b
|
129
|
+
end
|
130
|
+
|
131
|
+
def bunny
|
132
|
+
@@bunny ||= new_bunny
|
133
|
+
end
|
134
|
+
|
135
|
+
def next_job(args, response)
|
136
|
+
queue = args.delete("next_job")
|
137
|
+
enqueue(queue,args.merge(response)) if queue and not queue.empty?
|
138
|
+
end
|
139
|
+
|
140
|
+
def error_handler
|
141
|
+
@@error_handler ||= nil
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
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,87 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: minion_kim
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.16
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Orion Henry
|
9
|
+
- Kim Altintop
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2011-07-13 00:00:00.000000000Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: amqp
|
17
|
+
requirement: &2153999176 !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 0.6.7
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: *2153999176
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: bunny
|
28
|
+
requirement: &2153998504 !ruby/object:Gem::Requirement
|
29
|
+
none: false
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.6.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: *2153998504
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: json
|
39
|
+
requirement: &2153997832 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ! '>='
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: 1.2.0
|
45
|
+
type: :runtime
|
46
|
+
prerelease: false
|
47
|
+
version_requirements: *2153997832
|
48
|
+
description: Super simple job queue over AMQP
|
49
|
+
email: kim@soundcloud.com
|
50
|
+
executables: []
|
51
|
+
extensions: []
|
52
|
+
extra_rdoc_files:
|
53
|
+
- README.rdoc
|
54
|
+
files:
|
55
|
+
- README.rdoc
|
56
|
+
- Rakefile
|
57
|
+
- VERSION
|
58
|
+
- lib/minion.rb
|
59
|
+
- lib/minion/handler.rb
|
60
|
+
- spec/base.rb
|
61
|
+
- spec/enqueue_spec.rb
|
62
|
+
homepage: http://github.com/kim/minion
|
63
|
+
licenses: []
|
64
|
+
post_install_message:
|
65
|
+
rdoc_options: []
|
66
|
+
require_paths:
|
67
|
+
- lib
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
69
|
+
none: false
|
70
|
+
requirements:
|
71
|
+
- - ! '>='
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '0'
|
74
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
75
|
+
none: false
|
76
|
+
requirements:
|
77
|
+
- - ! '>='
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '0'
|
80
|
+
requirements: []
|
81
|
+
rubyforge_project:
|
82
|
+
rubygems_version: 1.8.5
|
83
|
+
signing_key:
|
84
|
+
specification_version: 3
|
85
|
+
summary: ! 'Fork containing one simple patch. Will not be maintained if pull request
|
86
|
+
#6 gets accepted upstream'
|
87
|
+
test_files: []
|