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 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: []