queue_classic_plus 0.0.2
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.
- checksums.yaml +7 -0
- data/.gitignore +23 -0
- data/.rspec +2 -0
- data/.rvmrc +1 -0
- data/Gemfile +11 -0
- data/LICENSE.txt +22 -0
- data/README.md +97 -0
- data/Rakefile +2 -0
- data/lib/queue_classic_plus/base.rb +127 -0
- data/lib/queue_classic_plus/metrics.rb +29 -0
- data/lib/queue_classic_plus/railtie.rb +12 -0
- data/lib/queue_classic_plus/tasks/work.rake +23 -0
- data/lib/queue_classic_plus/update_metrics.rb +62 -0
- data/lib/queue_classic_plus/version.rb +3 -0
- data/lib/queue_classic_plus/worker.rb +54 -0
- data/lib/queue_classic_plus.rb +49 -0
- data/lib/rails/generators/qc_plus_job/qc_plus_job_generator.rb +11 -0
- data/lib/rails/generators/qc_plus_job/templates/job.rb.erb +8 -0
- data/lib/rails/generators/qc_plus_job/templates/job_spec.rb.erb +13 -0
- data/queue_classic_plus.gemspec +28 -0
- data/spec/base_spec.rb +156 -0
- data/spec/sample_jobs.rb +43 -0
- data/spec/spec_helper.rb +31 -0
- data/spec/worker_spec.rb +129 -0
- metadata +173 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 8acdaf6cd41362f50d7ddb9c50fa87555c00adc9
|
4
|
+
data.tar.gz: 58c26d31a05c2390504975b6a7513179f0397ede
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 453a051c64c4c539f488f9ea7f0c01da9ac326de11891746b6b0abea14bc4103bb347afd4020bedb0b37c0262d1c1a983659f39c0b35909521db6af2951dcacb
|
7
|
+
data.tar.gz: 37263c41d6929a351e74920a8abe5d2d4d3bbc3c5c9bdb941f4cd2e6758ef2443ddf580dd16298124809ca0232ce7df02b3533fac75fef8fa911f21c45ecb597
|
data/.gitignore
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
23
|
+
tags
|
data/.rspec
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use 2.1.3@queue_classic_plus --create
|
data/Gemfile
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in queue_classic_plus.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
|
7
|
+
gem 'rspec'
|
8
|
+
gem 'pg'
|
9
|
+
gem 'timecop'
|
10
|
+
gem 'queue_classic-later', github: 'jipiboily/queue_classic-later', branch: "add-qc-3-to-custom-columns" # This is until the 3.0 work is merged into original repo
|
11
|
+
gem 'queue_classic_matchers'
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 CLDRDR Inc.
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
# QueueClassicPlus
|
2
|
+
|
3
|
+
QueueClassic is a simple Postgresql back DB queue. However, it's a little too simple to use it as the main queueing system of a medium to large app.
|
4
|
+
|
5
|
+
QueueClassicPlus adds many lacking features to QueueClassic.
|
6
|
+
|
7
|
+
- Standard job format
|
8
|
+
- Retry on specific exceptions
|
9
|
+
- Singleton jobs
|
10
|
+
- Metrics
|
11
|
+
- Error logging / handling
|
12
|
+
- Transactions
|
13
|
+
|
14
|
+
## Installation
|
15
|
+
|
16
|
+
Add these line to your application's Gemfile:
|
17
|
+
|
18
|
+
gem 'queue_classic_plus'
|
19
|
+
gem "queue_classic-later", github: "dpiddy/queue_classic-later"
|
20
|
+
|
21
|
+
And then execute:
|
22
|
+
|
23
|
+
$ bundle
|
24
|
+
|
25
|
+
Run the migration
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
QueueClassicPlus.migrate
|
29
|
+
```
|
30
|
+
|
31
|
+
## Usage
|
32
|
+
|
33
|
+
### Create a new job
|
34
|
+
|
35
|
+
```bash
|
36
|
+
rails g my_job test_job
|
37
|
+
```
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
# /app/jobs/my_job.rb
|
41
|
+
class Jobs::MyJob < QueueClassicPlus::Base
|
42
|
+
# Specified the queue name
|
43
|
+
@queue = :low
|
44
|
+
|
45
|
+
# Extry up to 5 times when SomeException is raised
|
46
|
+
retry! on: SomeException, max: 5
|
47
|
+
|
48
|
+
def self.perform(a, b)
|
49
|
+
# ...
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# In your code, you can enqueue this task like so:
|
54
|
+
Jobs::MyJob.do(1, "foo")
|
55
|
+
|
56
|
+
# You can also schedule a job in the future by doing
|
57
|
+
|
58
|
+
Jobs::MyJob.enqueue_perform_in(1.hour, 1, "foo")
|
59
|
+
```
|
60
|
+
|
61
|
+
### Run the QueueClassicPlus worker
|
62
|
+
|
63
|
+
```
|
64
|
+
QUEUE=low bundle exec qc_plus:work
|
65
|
+
```
|
66
|
+
|
67
|
+
## Advance configuration
|
68
|
+
|
69
|
+
If you want to log exceptions in your favorite exception tracker. You can configured it like sso:
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
QueueClassicPlus.exception_handler = -> (exception, job) do
|
73
|
+
Raven.capture_exception(exception, extra: {job: job, env: ENV})
|
74
|
+
end
|
75
|
+
```
|
76
|
+
|
77
|
+
If you use Librato, we push useful metrics directly to them.
|
78
|
+
|
79
|
+
Push metrics to your metric provider (only Librato is supported for now).
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
QueueClassicPlus.update_metrics
|
83
|
+
```
|
84
|
+
|
85
|
+
Call this is a cron job or something similar.
|
86
|
+
|
87
|
+
## TODO
|
88
|
+
|
89
|
+
- Remove dep on ActiveRecord
|
90
|
+
|
91
|
+
## Contributing
|
92
|
+
|
93
|
+
1. Fork it ( https://github.com/[my-github-username]/queue_classic_plus/fork )
|
94
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
95
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
96
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
97
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
module QueueClassicPlus
|
2
|
+
class Base
|
3
|
+
def self.queue
|
4
|
+
QC::Queue.new(@queue)
|
5
|
+
end
|
6
|
+
|
7
|
+
class_attribute :locked
|
8
|
+
class_attribute :skip_transaction
|
9
|
+
class_attribute :retries_on
|
10
|
+
class_attribute :max_retries
|
11
|
+
|
12
|
+
self.max_retries = 0
|
13
|
+
self.retries_on = {}
|
14
|
+
|
15
|
+
def self.retry!(on: RuntimeError, max: 5)
|
16
|
+
Array(on).each {|e| self.retries_on[e] = true}
|
17
|
+
self.max_retries = max
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.retries_on? exception
|
21
|
+
self.retries_on[exception.class] || self.retries_on.keys.any? {|klass| exception.is_a? klass}
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.lock!
|
25
|
+
self.locked = true
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.skip_transaction!
|
29
|
+
self.skip_transaction = true
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.locked?
|
33
|
+
!!self.locked
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.logger
|
37
|
+
QueueClassicPlus.logger
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.can_enqueue?(method, *args)
|
41
|
+
if locked?
|
42
|
+
lock_key = [@queue, method, args].to_json
|
43
|
+
max_lock_time = ENV.fetch("QUEUE_CLASSIC_MAX_LOCK_TIME", 10 * 60).to_i
|
44
|
+
|
45
|
+
ActiveRecord::Base.with_advisory_lock(lock_key, 0.1) do
|
46
|
+
q = "SELECT COUNT(1) AS count
|
47
|
+
FROM
|
48
|
+
(
|
49
|
+
(
|
50
|
+
SELECT 1
|
51
|
+
FROM queue_classic_jobs
|
52
|
+
WHERE q_name = $1 AND method = $2 AND args::text = $3::text
|
53
|
+
AND (locked_at IS NULL OR locked_at > current_timestamp - interval '#{max_lock_time} seconds')
|
54
|
+
LIMIT 1
|
55
|
+
)
|
56
|
+
|
57
|
+
UNION
|
58
|
+
|
59
|
+
(
|
60
|
+
SELECT 1
|
61
|
+
FROM queue_classic_later_jobs
|
62
|
+
WHERE q_name = $4 AND method = $5 AND args = $6::text
|
63
|
+
LIMIT 1
|
64
|
+
)
|
65
|
+
)
|
66
|
+
AS x"
|
67
|
+
|
68
|
+
result = QC.default_conn_adapter.execute(q, @queue, method, args.to_json, @queue, method, args.to_json)
|
69
|
+
result['count'].to_i == 0
|
70
|
+
end
|
71
|
+
else
|
72
|
+
true
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.enqueue(method, *args)
|
77
|
+
if can_enqueue?(method, *args)
|
78
|
+
queue.enqueue(method, *args)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.enqueue_perform(*args)
|
83
|
+
enqueue("#{self.to_s}._perform", *args)
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.enqueue_perform_in(time, *args)
|
87
|
+
raise "Can't enqueue in the future for locked jobs" if locked?
|
88
|
+
queue.enqueue_in(time, "#{self.to_s}._perform", *args)
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.restart_in(time, remaining_retries, *args)
|
92
|
+
queue.enqueue_in_with_custom(time, "#{self.to_s}._perform", {'remaining_retries' => remaining_retries}, *args)
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.do(*args)
|
96
|
+
Metrics.timing("qc_enqueue_time", source: librato_key) do
|
97
|
+
enqueue_perform(*args)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def self._perform(*args)
|
102
|
+
Metrics.timing("qu_perform_time", source: librato_key) do
|
103
|
+
if skip_transaction
|
104
|
+
perform *args
|
105
|
+
else
|
106
|
+
transaction do
|
107
|
+
perform *args
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def self.librato_key
|
114
|
+
(self.name || "").underscore.gsub(/\//, ".")
|
115
|
+
end
|
116
|
+
|
117
|
+
def self.transaction(options = {}, &block)
|
118
|
+
ActiveRecord::Base.transaction(options, &block)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Debugging
|
122
|
+
def self.list
|
123
|
+
q = "SELECT * FROM queue_classic_jobs WHERE q_name = '#{@queue}'"
|
124
|
+
ActiveRecord::Base.connection.execute(q).to_a
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module QueueClassicPlus
|
2
|
+
class Empty
|
3
|
+
def self.method_missing(*)
|
4
|
+
yield if block_given?
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
class Metrics
|
9
|
+
def self.timing(*args, &block)
|
10
|
+
provider.timing *args, &block
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.increment(*args)
|
14
|
+
provider.increment(*args)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.measure(*args)
|
18
|
+
provider.measure(*args)
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.provider
|
22
|
+
if defined?(Librato)
|
23
|
+
Librato
|
24
|
+
else
|
25
|
+
Empty
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
namespace :qc_plus do
|
2
|
+
desc "Start a new worker for the (default or $QUEUE) queue"
|
3
|
+
task :work => :environment do
|
4
|
+
puts "Starting up worker for queue #{ENV['QUEUE']}"
|
5
|
+
@worker = QueueClassicPlus::CustomWorker.new
|
6
|
+
|
7
|
+
trap('INT') do
|
8
|
+
$stderr.puts("Received INT. Shutting down.")
|
9
|
+
if !@worker.running
|
10
|
+
$stderr.puts("Worker has stopped running. Exit.")
|
11
|
+
exit(1)
|
12
|
+
end
|
13
|
+
@worker.stop
|
14
|
+
end
|
15
|
+
|
16
|
+
trap('TERM') do
|
17
|
+
$stderr.puts("Received Term. Shutting down.")
|
18
|
+
@worker.stop
|
19
|
+
end
|
20
|
+
|
21
|
+
@worker.start
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module QueueClassicPlus
|
2
|
+
module UpdateMetrics
|
3
|
+
def self.update
|
4
|
+
{
|
5
|
+
"queued" => "queue_classic_jobs",
|
6
|
+
"scheduled" => "queue_classic_later_jobs",
|
7
|
+
}.each do |type, table|
|
8
|
+
next unless ActiveRecord::Base.connection.table_exists?(table)
|
9
|
+
q = "SELECT q_name, COUNT(1) FROM #{table} GROUP BY q_name"
|
10
|
+
results = execute(q)
|
11
|
+
|
12
|
+
# Log individual queue sizes
|
13
|
+
results.each do |h|
|
14
|
+
Metrics.measure("qc.jobs_#{type}", h.fetch('count').to_i, source: h.fetch('q_name'))
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Log oldest locked_at and created_at
|
19
|
+
['locked_at', 'created_at'].each do |column|
|
20
|
+
age = max_age(column)
|
21
|
+
Metrics.measure("qc.max_#{column}", age)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Log oldes unlocked jobs
|
25
|
+
age = max_age("created_at", "locked_at IS NULL")
|
26
|
+
Metrics.measure("qc.max_created_at.unlocked", age)
|
27
|
+
|
28
|
+
if ActiveRecord::Base.connection.table_exists?('queue_classic_later_jobs')
|
29
|
+
lag = execute("SELECT MAX(EXTRACT(EPOCH FROM now() - not_before)) AS lag
|
30
|
+
FROM queue_classic_later_jobs").first
|
31
|
+
lag = lag ? lag['lag'] : 0
|
32
|
+
|
33
|
+
Metrics.measure("qc.jobs_delayed.lag", lag.to_f)
|
34
|
+
|
35
|
+
nb_late = execute("SELECT COUNT(1)
|
36
|
+
FROM queue_classic_later_jobs
|
37
|
+
WHERE not_before < NOW()").first
|
38
|
+
nb_late = nb_late ? nb_late['count'] : 0
|
39
|
+
|
40
|
+
Metrics.measure("qc.jobs_delayed.late_count", nb_late.to_i)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
def self.max_age(column, *conditions)
|
46
|
+
conditions.unshift "q_name != '#{::QueueClassicPlus::CustomWorker::FailedQueue.name}'"
|
47
|
+
|
48
|
+
q = "SELECT EXTRACT(EPOCH FROM now() - #{column}) AS age_in_seconds
|
49
|
+
FROM queue_classic_jobs
|
50
|
+
WHERE #{conditions.join(" AND ")}
|
51
|
+
ORDER BY age_in_seconds DESC
|
52
|
+
"
|
53
|
+
age_info = execute(q).first
|
54
|
+
|
55
|
+
age_info ? age_info['age_in_seconds'].to_i : 0
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.execute(q)
|
59
|
+
ActiveRecord::Base.connection.execute(q)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module QueueClassicPlus
|
2
|
+
class CustomWorker < QC::Worker
|
3
|
+
class QueueClassicJob < ActiveRecord::Base
|
4
|
+
end
|
5
|
+
|
6
|
+
BACKOFF_WIDTH = 10
|
7
|
+
FailedQueue = QC::Queue.new("failed_jobs")
|
8
|
+
|
9
|
+
def enqueue_failed(job, e)
|
10
|
+
ActiveRecord::Base.transaction do
|
11
|
+
FailedQueue.enqueue(job[:method], *job[:args])
|
12
|
+
new_job = QueueClassicJob.order(:id).where(q_name: 'failed_jobs').last
|
13
|
+
new_job.last_error = if e.backtrace then ([e.message] + e.backtrace ).join("\n") else e.message end
|
14
|
+
new_job.save!
|
15
|
+
end
|
16
|
+
|
17
|
+
QueueClassicPlus.exception_handler.call(e, job)
|
18
|
+
Metrics.increment("qc.errors", source: @q_name)
|
19
|
+
end
|
20
|
+
|
21
|
+
def handle_failure(job, e)
|
22
|
+
QueueClassicPlus.logger.info "Handling exception #{e.message} for job #{job[:id]}"
|
23
|
+
klass = job_klass(job)
|
24
|
+
|
25
|
+
model = QueueClassicJob.find(job[:id])
|
26
|
+
|
27
|
+
# The mailers doesn't have a retries_on?
|
28
|
+
if klass && klass.respond_to?(:retries_on?) && klass.retries_on?(e)
|
29
|
+
remaining_retries = model.remaining_retries || klass.max_retries
|
30
|
+
remaining_retries -= 1
|
31
|
+
|
32
|
+
if remaining_retries > 0
|
33
|
+
klass.restart_in((klass.max_retries - remaining_retries) * BACKOFF_WIDTH,
|
34
|
+
remaining_retries,
|
35
|
+
*job[:args])
|
36
|
+
else
|
37
|
+
enqueue_failed(job, e)
|
38
|
+
end
|
39
|
+
else
|
40
|
+
enqueue_failed(job, e)
|
41
|
+
end
|
42
|
+
model.destroy
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
def job_klass(job)
|
47
|
+
begin
|
48
|
+
Object.const_get(job[:method].split('.')[0])
|
49
|
+
rescue NameError
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require "queue_classic"
|
2
|
+
require "queue_classic/later"
|
3
|
+
require "active_record"
|
4
|
+
require "with_advisory_lock"
|
5
|
+
require "active_support/core_ext/class/attribute"
|
6
|
+
|
7
|
+
require "queue_classic_plus/version"
|
8
|
+
require "queue_classic_plus/metrics"
|
9
|
+
require "queue_classic_plus/update_metrics"
|
10
|
+
require "queue_classic_plus/base"
|
11
|
+
require "queue_classic_plus/worker"
|
12
|
+
|
13
|
+
module QueueClassicPlus
|
14
|
+
require 'queue_classic_plus/railtie' if defined?(Rails)
|
15
|
+
|
16
|
+
def self.migrate(c = QC::default_conn_adapter.connection)
|
17
|
+
conn = QC::ConnAdapter.new(c)
|
18
|
+
conn.execute("ALTER TABLE queue_classic_jobs ADD COLUMN last_error TEXT")
|
19
|
+
conn.execute("ALTER TABLE queue_classic_jobs ADD COLUMN remaining_retries INTEGER")
|
20
|
+
conn.execute("ALTER TABLE queue_classic_later_jobs ADD COLUMN remaining_retries INTEGER")
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.demigrate(c = QC::default_conn_adapter.connection)
|
24
|
+
conn = QC::ConnAdapter.new(c)
|
25
|
+
conn.execute("ALTER TABLE queue_classic_jobs DROP COLUMN last_error")
|
26
|
+
conn.execute("ALTER TABLE queue_classic_jobs DROP COLUMN remaining_retries")
|
27
|
+
conn.execute("ALTER TABLE queue_classic_later_jobs DROP COLUMN remaining_retries")
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.exception_handler
|
31
|
+
@exception_handler ||= -> (exception, job) { nil }
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.exception_handler=(handler)
|
35
|
+
@exception_handler = handler
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.update_metrics
|
39
|
+
UpdateMetrics.update
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.logger
|
43
|
+
@logger ||= defined?(Rails) ? Rails.logger : Logger.new(STDOUT)
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.logger=(l)
|
47
|
+
@logger = l
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class QcPlusJobGenerator < Rails::Generators::NamedBase
|
2
|
+
source_root File.expand_path('../templates', __FILE__)
|
3
|
+
|
4
|
+
def generate_lib
|
5
|
+
template "job.rb.erb", File.join("app", "jobs", class_path, "#{file_name}.rb")
|
6
|
+
if defined?(RSpec)
|
7
|
+
template "job_spec.rb.erb", File.join("spec", "jobs", class_path, "#{file_name}_spec.rb")
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
@@ -0,0 +1,13 @@
|
|
1
|
+
<% module_namespacing do -%>
|
2
|
+
describe Jobs::<%= class_name %>, type: :job do
|
3
|
+
it_should_behave_like "a queueable class"
|
4
|
+
|
5
|
+
describe ".perform" do
|
6
|
+
it "does not die" do
|
7
|
+
expect do
|
8
|
+
described_class.perform
|
9
|
+
end.to_not raise_error
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
<% end -%>
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'queue_classic_plus/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "queue_classic_plus"
|
8
|
+
spec.version = QueueClassicPlus::VERSION
|
9
|
+
spec.authors = ["Simon Mathieu", "Russell Smith", "Jean-Philippe Boily"]
|
10
|
+
spec.email = ["simon.math@gmail.com", "russ@rainforestqa.com", "j@jipi.ca"]
|
11
|
+
spec.summary = %q{Useful extras for Queue Classic}
|
12
|
+
spec.description = %q{}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "queue_classic"
|
22
|
+
spec.add_dependency "queue_classic-later"
|
23
|
+
spec.add_dependency "activerecord", "> 3.0"
|
24
|
+
spec.add_dependency "activesupport", "> 3.0"
|
25
|
+
spec.add_dependency "with_advisory_lock"
|
26
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
27
|
+
spec.add_development_dependency "rake"
|
28
|
+
end
|
data/spec/base_spec.rb
ADDED
@@ -0,0 +1,156 @@
|
|
1
|
+
|
2
|
+
describe QueueClassicPlus::Base do
|
3
|
+
context "A child of QueueClassicPlus::Base" do
|
4
|
+
context "that is locked" do
|
5
|
+
subject do
|
6
|
+
Class.new(QueueClassicPlus::Base) do
|
7
|
+
@queue = :test
|
8
|
+
lock!
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
it "does not allow multiple enqueues" do
|
13
|
+
subject.do
|
14
|
+
subject.do
|
15
|
+
subject.should have_queue_size_of(1)
|
16
|
+
end
|
17
|
+
|
18
|
+
it "does allow multiple enqueues if something got locked for too long" do
|
19
|
+
subject.do
|
20
|
+
ActiveRecord::Base.connection.execute "
|
21
|
+
UPDATE queue_classic_jobs SET locked_at = '#{1.day.ago.to_s}' WHERE q_name = 'test'
|
22
|
+
"
|
23
|
+
subject.do
|
24
|
+
subject.should have_queue_size_of(2)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context "with default settings" do
|
29
|
+
subject do
|
30
|
+
Class.new(QueueClassicPlus::Base) do
|
31
|
+
@queue = :test
|
32
|
+
|
33
|
+
def self.perform
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.name
|
37
|
+
"Funky::Name"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
it "calls perform in a transaction" do
|
43
|
+
ActiveRecord::Base.should_receive(:transaction).and_call_original
|
44
|
+
subject._perform
|
45
|
+
end
|
46
|
+
|
47
|
+
it "measures the time" do
|
48
|
+
QueueClassicPlus::Metrics.should_receive(:timing).with("qu_perform_time", {source: "funky.name"}).and_call_original
|
49
|
+
subject._perform
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context "skips transaction" do
|
54
|
+
subject do
|
55
|
+
Class.new(QueueClassicPlus::Base) do
|
56
|
+
@queue = :test
|
57
|
+
skip_transaction!
|
58
|
+
|
59
|
+
def self.perform
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
it "calls perform outside of a transaction" do
|
65
|
+
ActiveRecord::Base.should_not_receive(:transaction)
|
66
|
+
subject._perform
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
context "retries on single exception" do
|
71
|
+
subject do
|
72
|
+
Class.new(QueueClassicPlus::Base) do
|
73
|
+
@queue = :test
|
74
|
+
retry! on: SomeException, max: 5
|
75
|
+
skip_transaction!
|
76
|
+
|
77
|
+
def self.perform
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
it "retries on specified exception" do
|
83
|
+
subject.retries_on?(SomeException.new).should be(true)
|
84
|
+
end
|
85
|
+
|
86
|
+
it "does not retry on unspecified exceptions" do
|
87
|
+
subject.retries_on?(RuntimeError).should be(false)
|
88
|
+
end
|
89
|
+
|
90
|
+
it "sets max retries" do
|
91
|
+
subject.max_retries.should == 5
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
context "retries on multiple exceptions" do
|
96
|
+
subject do
|
97
|
+
Class.new(QueueClassicPlus::Base) do
|
98
|
+
@queue = :test
|
99
|
+
retry! on: [SomeException, SomeOtherException], max: 5
|
100
|
+
skip_transaction!
|
101
|
+
|
102
|
+
def self.perform
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
it "retries on all specified exceptions" do
|
108
|
+
subject.retries_on?(SomeException.new).should be(true)
|
109
|
+
subject.retries_on?(SomeOtherException.new).should be(true)
|
110
|
+
end
|
111
|
+
|
112
|
+
it "does not retry on unspecified exceptions" do
|
113
|
+
subject.retries_on?(RuntimeError).should be(false)
|
114
|
+
end
|
115
|
+
|
116
|
+
it "sets max retries" do
|
117
|
+
subject.max_retries.should == 5
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
context "handles exception subclasses" do
|
122
|
+
class ServiceReallyUnavailable < SomeException
|
123
|
+
end
|
124
|
+
|
125
|
+
subject do
|
126
|
+
Class.new(QueueClassicPlus::Base) do
|
127
|
+
@queue = :test
|
128
|
+
retry! on: SomeException, max: 5
|
129
|
+
skip_transaction!
|
130
|
+
|
131
|
+
def self.perform
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
it "retries on a subclass of a specified exception" do
|
137
|
+
subject.retries_on?(ServiceReallyUnavailable.new).should be(true)
|
138
|
+
end
|
139
|
+
|
140
|
+
it "does not retry on unspecified exceptions" do
|
141
|
+
subject.retries_on?(RuntimeError).should be(false)
|
142
|
+
end
|
143
|
+
|
144
|
+
it "sets max retries" do
|
145
|
+
subject.max_retries.should == 5
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
describe ".librato_key" do
|
151
|
+
it "removes unsupported caracter from the classname" do
|
152
|
+
Jobs::Tests::TestJob.librato_key.should == 'jobs.tests.test_job'
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
data/spec/sample_jobs.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
class SomeException < RuntimeError
|
2
|
+
end
|
3
|
+
|
4
|
+
class SomeOtherException < RuntimeError
|
5
|
+
end
|
6
|
+
|
7
|
+
module Jobs
|
8
|
+
module Tests
|
9
|
+
class LockedTestJob < QueueClassicPlus::Base
|
10
|
+
lock!
|
11
|
+
|
12
|
+
@queue = :low
|
13
|
+
retry! on: SomeException, max: 5
|
14
|
+
|
15
|
+
def self.perform should_raise
|
16
|
+
raise SomeException if should_raise
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
class TestJobNoRetry < QueueClassicPlus::Base
|
22
|
+
class Custom < RuntimeError
|
23
|
+
end
|
24
|
+
|
25
|
+
@queue = :low
|
26
|
+
|
27
|
+
def self.perform should_raise
|
28
|
+
raise Custom if should_raise
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
class TestJob < QueueClassicPlus::Base
|
34
|
+
@queue = :low
|
35
|
+
retry! on: SomeException, max: 5
|
36
|
+
|
37
|
+
def self.perform should_raise
|
38
|
+
raise SomeException if should_raise
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'queue_classic_plus'
|
2
|
+
require 'pg'
|
3
|
+
require 'timecop'
|
4
|
+
require 'queue_classic_matchers'
|
5
|
+
require_relative './sample_jobs'
|
6
|
+
|
7
|
+
RSpec.configure do |config|
|
8
|
+
config.before(:suite) do
|
9
|
+
ActiveRecord::Base.establish_connection(
|
10
|
+
:adapter => "postgresql",
|
11
|
+
:username => "postgres",
|
12
|
+
:database => "queue_classic_plus_test",
|
13
|
+
:host => 'localhost',
|
14
|
+
)
|
15
|
+
|
16
|
+
ActiveRecord::Base.connection.execute "drop schema public cascade; create schema public;"
|
17
|
+
|
18
|
+
QC.default_conn_adapter = QC::ConnAdapter.new(ActiveRecord::Base.connection.raw_connection)
|
19
|
+
QC::Setup.create
|
20
|
+
QC::Later::Setup.create
|
21
|
+
QueueClassicPlus.migrate
|
22
|
+
end
|
23
|
+
|
24
|
+
config.before(:each) do
|
25
|
+
tables = ActiveRecord::Base.connection.tables.select do |table|
|
26
|
+
table != "schema_migrations"
|
27
|
+
end
|
28
|
+
ActiveRecord::Base.connection.execute("TRUNCATE #{tables.join(', ')} CASCADE") unless tables.empty?
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
data/spec/worker_spec.rb
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe QueueClassicPlus::CustomWorker do
|
4
|
+
class QueueClassicLaterJob < ActiveRecord::Base
|
5
|
+
end
|
6
|
+
|
7
|
+
class QueueClassicJob < ActiveRecord::Base
|
8
|
+
end
|
9
|
+
|
10
|
+
let(:failed_queue) { described_class::FailedQueue }
|
11
|
+
|
12
|
+
context "failure" do
|
13
|
+
let(:queue) { QC::Queue.new("test") }
|
14
|
+
let(:worker) { described_class.new q_name: queue.name }
|
15
|
+
|
16
|
+
it "record failures in the failed queue" do
|
17
|
+
queue.enqueue("Kerklfadsjflaksj", 1, 2, 3)
|
18
|
+
failed_queue.count.should == 0
|
19
|
+
worker.work
|
20
|
+
failed_queue.count.should == 1
|
21
|
+
job = failed_queue.lock
|
22
|
+
job[:method].should == "Kerklfadsjflaksj"
|
23
|
+
job[:args].should == [1, 2, 3]
|
24
|
+
QueueClassicJob.last.last_error.should be_present
|
25
|
+
end
|
26
|
+
|
27
|
+
it "records normal errors" do
|
28
|
+
queue.enqueue("Jobs::Tests::TestJobNoRetry.perform", true)
|
29
|
+
failed_queue.count.should == 0
|
30
|
+
worker.work
|
31
|
+
failed_queue.count.should == 1
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context "retry" do
|
36
|
+
let(:job_type) { Jobs::Tests::LockedTestJob }
|
37
|
+
let(:worker) { described_class.new q_name: job_type.queue.name }
|
38
|
+
let(:enqueue_expected_ts) { described_class::BACKOFF_WIDTH.seconds.from_now }
|
39
|
+
|
40
|
+
before do
|
41
|
+
job_type.skip_transaction!
|
42
|
+
end
|
43
|
+
|
44
|
+
it "retries" do
|
45
|
+
expect do
|
46
|
+
job_type.enqueue_perform(true)
|
47
|
+
end.to change_queue_size_of(job_type).by(1)
|
48
|
+
|
49
|
+
Jobs::Tests::LockedTestJob.should have_queue_size_of(1)
|
50
|
+
failed_queue.count.should == 0
|
51
|
+
QueueClassicMatchers::QueueClassicRspec.find_by_args('low', 'Jobs::Tests::LockedTestJob._perform', [true]).first['remaining_retries'].should be_nil
|
52
|
+
|
53
|
+
Timecop.freeze do
|
54
|
+
expect do
|
55
|
+
worker.work
|
56
|
+
end.to change_queue_size_of(job_type).by(-1)
|
57
|
+
|
58
|
+
failed_queue.count.should == 0 # not enqueued on Failed
|
59
|
+
Jobs::Tests::LockedTestJob.should have_scheduled(true).at(Time.now + described_class::BACKOFF_WIDTH.seconds.to_i) # should have scheduled a retry for later
|
60
|
+
end
|
61
|
+
|
62
|
+
Timecop.freeze(Time.now + (described_class::BACKOFF_WIDTH.seconds.to_i * 2)) do
|
63
|
+
QC::Later.tick(true)
|
64
|
+
# the job should be re-enqueued with a decremented retry count
|
65
|
+
jobs = QueueClassicMatchers::QueueClassicRspec.find_by_args('low', 'Jobs::Tests::LockedTestJob._perform', [true])
|
66
|
+
jobs.size.should == 1
|
67
|
+
job = jobs.first
|
68
|
+
job['remaining_retries'].to_i.should == job_type.max_retries - 1
|
69
|
+
job['locked_by'].should be_nil
|
70
|
+
job['locked_at'].should be_nil
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
it "enqueues in the failed queue when retries have been exhausted" do
|
75
|
+
job_type.max_retries = 0
|
76
|
+
expect do
|
77
|
+
job_type.enqueue_perform(true)
|
78
|
+
end.to change_queue_size_of(job_type).by(1)
|
79
|
+
|
80
|
+
Jobs::Tests::LockedTestJob.should have_queue_size_of(1)
|
81
|
+
failed_queue.count.should == 0
|
82
|
+
QueueClassicMatchers::QueueClassicRspec.find_by_args('low', 'Jobs::Tests::LockedTestJob._perform', [true]).first['remaining_retries'].should be_nil
|
83
|
+
|
84
|
+
Timecop.freeze do
|
85
|
+
expect do
|
86
|
+
worker.work
|
87
|
+
end.to change_queue_size_of(job_type).by(-1)
|
88
|
+
|
89
|
+
failed_queue.count.should == 1 # not enqueued on Failed
|
90
|
+
Jobs::Tests::LockedTestJob.should_not have_scheduled(true).at(Time.now + described_class::BACKOFF_WIDTH.seconds.to_i) # should have scheduled a retry for later
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
context "enqueuing during a retry" do
|
96
|
+
let(:job_type) { Jobs::Tests::LockedTestJob }
|
97
|
+
let(:worker) { described_class.new q_name: job_type.queue.name }
|
98
|
+
let(:enqueue_expected_ts) { described_class::BACKOFF_WIDTH.seconds.from_now }
|
99
|
+
|
100
|
+
before do
|
101
|
+
job_type.max_retries = 5
|
102
|
+
job_type.skip_transaction!
|
103
|
+
end
|
104
|
+
|
105
|
+
it "does not enqueue in main queue while retrying" do
|
106
|
+
expect do
|
107
|
+
job_type.enqueue_perform(true)
|
108
|
+
end.to change_queue_size_of(job_type).by(1)
|
109
|
+
|
110
|
+
Jobs::Tests::LockedTestJob.should have_queue_size_of(1)
|
111
|
+
failed_queue.count.should == 0
|
112
|
+
QueueClassicMatchers::QueueClassicRspec.find_by_args('low', 'Jobs::Tests::LockedTestJob._perform', [true]).first['remaining_retries'].should be_nil
|
113
|
+
|
114
|
+
Timecop.freeze do
|
115
|
+
expect do
|
116
|
+
worker.work
|
117
|
+
end.to change_queue_size_of(job_type).by(-1)
|
118
|
+
|
119
|
+
failed_queue.count.should == 0 # not enqueued on Failed
|
120
|
+
Jobs::Tests::LockedTestJob.should have_scheduled(true).at(Time.now + described_class::BACKOFF_WIDTH.seconds.to_i) # should have scheduled a retry for later
|
121
|
+
|
122
|
+
expect do
|
123
|
+
job_type.enqueue_perform(true)
|
124
|
+
end.to change_queue_size_of(job_type).by(0)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
metadata
ADDED
@@ -0,0 +1,173 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: queue_classic_plus
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Simon Mathieu
|
8
|
+
- Russell Smith
|
9
|
+
- Jean-Philippe Boily
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2014-10-29 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: queue_classic
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
requirements:
|
19
|
+
- - ">="
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
version: '0'
|
29
|
+
- !ruby/object:Gem::Dependency
|
30
|
+
name: queue_classic-later
|
31
|
+
requirement: !ruby/object:Gem::Requirement
|
32
|
+
requirements:
|
33
|
+
- - ">="
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: '0'
|
36
|
+
type: :runtime
|
37
|
+
prerelease: false
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '0'
|
43
|
+
- !ruby/object:Gem::Dependency
|
44
|
+
name: activerecord
|
45
|
+
requirement: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">"
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '3.0'
|
50
|
+
type: :runtime
|
51
|
+
prerelease: false
|
52
|
+
version_requirements: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">"
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '3.0'
|
57
|
+
- !ruby/object:Gem::Dependency
|
58
|
+
name: activesupport
|
59
|
+
requirement: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">"
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: '3.0'
|
64
|
+
type: :runtime
|
65
|
+
prerelease: false
|
66
|
+
version_requirements: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">"
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '3.0'
|
71
|
+
- !ruby/object:Gem::Dependency
|
72
|
+
name: with_advisory_lock
|
73
|
+
requirement: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
type: :runtime
|
79
|
+
prerelease: false
|
80
|
+
version_requirements: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '0'
|
85
|
+
- !ruby/object:Gem::Dependency
|
86
|
+
name: bundler
|
87
|
+
requirement: !ruby/object:Gem::Requirement
|
88
|
+
requirements:
|
89
|
+
- - "~>"
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: '1.6'
|
92
|
+
type: :development
|
93
|
+
prerelease: false
|
94
|
+
version_requirements: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - "~>"
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '1.6'
|
99
|
+
- !ruby/object:Gem::Dependency
|
100
|
+
name: rake
|
101
|
+
requirement: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: '0'
|
106
|
+
type: :development
|
107
|
+
prerelease: false
|
108
|
+
version_requirements: !ruby/object:Gem::Requirement
|
109
|
+
requirements:
|
110
|
+
- - ">="
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: '0'
|
113
|
+
description: ''
|
114
|
+
email:
|
115
|
+
- simon.math@gmail.com
|
116
|
+
- russ@rainforestqa.com
|
117
|
+
- j@jipi.ca
|
118
|
+
executables: []
|
119
|
+
extensions: []
|
120
|
+
extra_rdoc_files: []
|
121
|
+
files:
|
122
|
+
- ".gitignore"
|
123
|
+
- ".rspec"
|
124
|
+
- ".rvmrc"
|
125
|
+
- Gemfile
|
126
|
+
- LICENSE.txt
|
127
|
+
- README.md
|
128
|
+
- Rakefile
|
129
|
+
- lib/queue_classic_plus.rb
|
130
|
+
- lib/queue_classic_plus/base.rb
|
131
|
+
- lib/queue_classic_plus/metrics.rb
|
132
|
+
- lib/queue_classic_plus/railtie.rb
|
133
|
+
- lib/queue_classic_plus/tasks/work.rake
|
134
|
+
- lib/queue_classic_plus/update_metrics.rb
|
135
|
+
- lib/queue_classic_plus/version.rb
|
136
|
+
- lib/queue_classic_plus/worker.rb
|
137
|
+
- lib/rails/generators/qc_plus_job/qc_plus_job_generator.rb
|
138
|
+
- lib/rails/generators/qc_plus_job/templates/job.rb.erb
|
139
|
+
- lib/rails/generators/qc_plus_job/templates/job_spec.rb.erb
|
140
|
+
- queue_classic_plus.gemspec
|
141
|
+
- spec/base_spec.rb
|
142
|
+
- spec/sample_jobs.rb
|
143
|
+
- spec/spec_helper.rb
|
144
|
+
- spec/worker_spec.rb
|
145
|
+
homepage: ''
|
146
|
+
licenses:
|
147
|
+
- MIT
|
148
|
+
metadata: {}
|
149
|
+
post_install_message:
|
150
|
+
rdoc_options: []
|
151
|
+
require_paths:
|
152
|
+
- lib
|
153
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
154
|
+
requirements:
|
155
|
+
- - ">="
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: '0'
|
158
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
159
|
+
requirements:
|
160
|
+
- - ">="
|
161
|
+
- !ruby/object:Gem::Version
|
162
|
+
version: '0'
|
163
|
+
requirements: []
|
164
|
+
rubyforge_project:
|
165
|
+
rubygems_version: 2.2.2
|
166
|
+
signing_key:
|
167
|
+
specification_version: 4
|
168
|
+
summary: Useful extras for Queue Classic
|
169
|
+
test_files:
|
170
|
+
- spec/base_spec.rb
|
171
|
+
- spec/sample_jobs.rb
|
172
|
+
- spec/spec_helper.rb
|
173
|
+
- spec/worker_spec.rb
|