tchandy-minion 0.1.13

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