af_minion 0.1.15.1

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 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 = "minion"
5
+ s.description = "Super simple job queue over AMQP"
6
+ s.summary = s.description
7
+ s.author = "Orion Henry"
8
+ s.email = "orion@heroku.com"
9
+ s.homepage = "http://github.com/orionz/minion"
10
+ s.rubyforge_project = "minion"
11
+ s.files = FileList["[A-Z]*", "{bin,lib,spec}/**/*"]
12
+ s.add_dependency "amqp", ">= 0.6.7"
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.15
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,214 @@
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
+
9
+ class AMQPServerConnectError < StandardError; end;
10
+
11
+ NUMBER_OF_RETRIES = 3
12
+
13
+ extend self
14
+
15
+ def url=(url)
16
+ @@config_url = url
17
+ end
18
+
19
+ def enqueue(jobs, data = {})
20
+ raise "cannot enqueue a nil job" if jobs.nil?
21
+ raise "cannot enqueue an empty job" if jobs.empty?
22
+
23
+ ## jobs can be one or more jobs
24
+ if jobs.respond_to? :shift
25
+ queue = jobs.shift
26
+ data["next_job"] = jobs unless jobs.empty?
27
+ else
28
+ queue = jobs
29
+ end
30
+
31
+ # NOTE: we encode to yaml, Minion encodes to json. The reason is that json cannot handle Ruby symbols
32
+ yamlized_data = data.to_yaml
33
+ log "send: #{queue}:#{yamlized_data}"
34
+
35
+ # NOTE: When rabbitMQ goes down, the mongrel keeps on holding the stale connection to it. We need to establish a new
36
+ # connection when this happens.
37
+
38
+ with_bunny_connection_retry do
39
+ bunny.queue(queue, :durable => false, :auto_delete => false).publish(yamlized_data)
40
+ end
41
+ end
42
+
43
+ def log(msg)
44
+ @@logger ||= proc { |m| puts "#{Time.now} :minion: #{m}" }
45
+ @@logger.call(msg)
46
+ end
47
+
48
+ def error(&blk)
49
+ @@error_handler = blk
50
+ end
51
+
52
+ def logger(&blk)
53
+ @@logger = blk
54
+ end
55
+
56
+ def job(queue, options = {}, &blk)
57
+ handler = Minion::Handler.new queue
58
+ handler.when = options[:when] if options[:when]
59
+ handler.unsub = lambda do
60
+ log "unsubscribing to #{queue}"
61
+ MQ.queue(queue, :durable => false, :auto_delete => false).unsubscribe
62
+ end
63
+
64
+ handler.sub = lambda do
65
+ log "subscribing to #{queue}"
66
+ MQ.queue(queue, :durable => false, :auto_delete => false).subscribe(:ack => false) do |h,m|
67
+ return if AMQP.closing?
68
+ begin
69
+ log "recv: #{queue}:#{m}"
70
+
71
+ args = decode_json(m)
72
+
73
+ result = yield(args)
74
+
75
+ next_job(args, result)
76
+ rescue Object => e
77
+ raise unless error_handler
78
+ error_handler.call(e,queue,m,h)
79
+ end
80
+ check_all
81
+ end
82
+ end
83
+ @@handlers ||= []
84
+ at_exit { Minion.run } if @@handlers.size == 0
85
+ @@handlers << handler
86
+ end
87
+
88
+ def decode_json(string)
89
+ if defined? ActiveSupport::JSON
90
+ ActiveSupport::JSON.decode string
91
+ else
92
+ JSON.load string
93
+ end
94
+ end
95
+
96
+ def check_all
97
+ @@handlers.each { |h| h.check }
98
+ end
99
+
100
+ def run
101
+ log "Starting minion"
102
+
103
+ Signal.trap('INT') { exit_handler }
104
+ Signal.trap('TERM'){ exit_handler }
105
+
106
+ EM.run do
107
+ connection = AMQP.start(amqp_config) do
108
+ MQ.prefetch(1)
109
+ check_all
110
+ end
111
+
112
+ # NOTE: By default, Minion gets into a wierd state when there is a connection error (i.e., the AMQP server is down).
113
+ # In short, it continues running without any subscriptions. We chose to detect this case, and kill the worker that
114
+ # has invoke run. This will allow it to be restarted by the monitoring system, and hopefully by then, the AMQP
115
+ # server will be back up.
116
+ if connection.error?
117
+ exit_handler
118
+ raise AMQPServerConnectError.new("Couldn't connect to RabbitMQ server at #{amqp_url}")
119
+ end
120
+ end
121
+ end
122
+
123
+ def amqp_url
124
+ @@amqp_url ||= ENV["AMQP_URL"] || "amqp://guest:guest@localhost/"
125
+ end
126
+
127
+ def amqp_url=(url)
128
+ clear_bunny
129
+ @@amqp_url = url
130
+ end
131
+
132
+ def delete!(*queue_names)
133
+ EM.run do
134
+ AMQP.start(amqp_config) do
135
+ queue_names.each do |queue_name|
136
+ MQ.queue(queue_name).delete
137
+ msg = "Minion: deleting '#{queue_name}' queue from the AMQP server."
138
+ ActiveRecord::Base::logger.info(msg)
139
+ log msg
140
+ end
141
+ end
142
+
143
+ AMQP.stop { EM.stop }
144
+ end
145
+
146
+ end
147
+
148
+ private
149
+
150
+ def amqp_config
151
+ uri = URI.parse(amqp_url)
152
+ {
153
+ :vhost => uri.path,
154
+ :host => uri.host,
155
+ :user => uri.user,
156
+ :port => (uri.port || 5672),
157
+ :pass => uri.password
158
+ }
159
+ rescue Object => e
160
+ raise "invalid AMQP_URL: #{uri.inspect} (#{e})"
161
+ end
162
+
163
+ def new_bunny
164
+ b = Bunny.new(amqp_config)
165
+ b.start
166
+ b
167
+ end
168
+
169
+ def bunny
170
+ @@bunny ||= new_bunny
171
+ end
172
+
173
+ def next_job(args, response)
174
+ queue = args.delete("next_job")
175
+ enqueue(queue,args.merge(response)) if queue and not queue.empty?
176
+ end
177
+
178
+ def error_handler
179
+ @@error_handler ||= nil
180
+ end
181
+
182
+ def exit_handler
183
+ AMQP.stop { EM.stop }
184
+ end
185
+
186
+ def with_bunny_connection_retry
187
+ num_executions = 1
188
+ begin
189
+ yield
190
+ rescue Bunny::ServerDownError, Bunny::ConnectionError => e
191
+ clear_bunny
192
+ if num_executions < NUMBER_OF_RETRIES
193
+ log "Retry ##{num_executions} : #{caller.first}"
194
+ num_executions += 1
195
+ sleep 0.5
196
+ retry
197
+ else
198
+ error = e.class.new(e.message + " (Retried #{NUMBER_OF_RETRIES} times. Giving up.)")
199
+ error.set_backtrace(e.backtrace)
200
+ raise error
201
+ end
202
+ rescue => e
203
+ clear_bunny
204
+ raise e
205
+ end
206
+ end
207
+
208
+ def clear_bunny
209
+ @@bunny = nil
210
+ end
211
+
212
+
213
+ end
214
+
@@ -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,129 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: af_minion
3
+ version: !ruby/object:Gem::Version
4
+ hash: 121
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 15
10
+ - 1
11
+ version: 0.1.15.1
12
+ platform: ruby
13
+ authors:
14
+ - Orion Henry
15
+ - AppFolio Dev Team
16
+ autorequire:
17
+ bindir: bin
18
+ cert_chain: []
19
+
20
+ date: 2010-08-24 00:00:00 -07:00
21
+ default_executable:
22
+ dependencies:
23
+ - !ruby/object:Gem::Dependency
24
+ name: amqp
25
+ prerelease: false
26
+ requirement: &id001 !ruby/object:Gem::Requirement
27
+ none: false
28
+ requirements:
29
+ - - ">="
30
+ - !ruby/object:Gem::Version
31
+ hash: 9
32
+ segments:
33
+ - 0
34
+ - 6
35
+ - 7
36
+ version: 0.6.7
37
+ type: :runtime
38
+ version_requirements: *id001
39
+ - !ruby/object:Gem::Dependency
40
+ name: bunny
41
+ prerelease: false
42
+ requirement: &id002 !ruby/object:Gem::Requirement
43
+ none: false
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ hash: 7
48
+ segments:
49
+ - 0
50
+ - 6
51
+ - 0
52
+ version: 0.6.0
53
+ type: :runtime
54
+ version_requirements: *id002
55
+ - !ruby/object:Gem::Dependency
56
+ name: json
57
+ prerelease: false
58
+ requirement: &id003 !ruby/object:Gem::Requirement
59
+ none: false
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ hash: 31
64
+ segments:
65
+ - 1
66
+ - 2
67
+ - 0
68
+ version: 1.2.0
69
+ type: :runtime
70
+ version_requirements: *id003
71
+ description: Super simple job queue over AMQP with modifications from AppFolio Inc.
72
+ email: tushar.ranka@appfolio.com orion@heroku.com
73
+ executables: []
74
+
75
+ extensions: []
76
+
77
+ extra_rdoc_files:
78
+ - README.rdoc
79
+ files:
80
+ - README.rdoc
81
+ - Rakefile
82
+ - VERSION
83
+ - lib/minion.rb
84
+ - lib/minion/handler.rb
85
+ - spec/base.rb
86
+ - spec/enqueue_spec.rb
87
+ - examples/math.rb
88
+ - examples/sandwich.rb
89
+ - examples/when.rb
90
+ has_rdoc: true
91
+ homepage: http://github.com/orionz/minion
92
+ licenses: []
93
+
94
+ post_install_message:
95
+ rdoc_options:
96
+ - --charset=UTF-8
97
+ require_paths:
98
+ - lib
99
+ required_ruby_version: !ruby/object:Gem::Requirement
100
+ none: false
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ hash: 3
105
+ segments:
106
+ - 0
107
+ version: "0"
108
+ required_rubygems_version: !ruby/object:Gem::Requirement
109
+ none: false
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ hash: 3
114
+ segments:
115
+ - 0
116
+ version: "0"
117
+ requirements: []
118
+
119
+ rubyforge_project: af_minion
120
+ rubygems_version: 1.3.7
121
+ signing_key:
122
+ specification_version: 3
123
+ summary: Super simple job queue over AMQP
124
+ test_files:
125
+ - spec/base.rb
126
+ - spec/enqueue_spec.rb
127
+ - examples/math.rb
128
+ - examples/sandwich.rb
129
+ - examples/when.rb