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.
@@ -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