que 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +102 -0
- data/Rakefile +1 -0
- data/lib/que.rb +10 -0
- data/lib/que/job.rb +185 -0
- data/lib/que/version.rb +3 -0
- data/lib/que/worker.rb +216 -0
- data/que.gemspec +28 -0
- data/spec/spec_helper.rb +34 -0
- data/spec/unit/error_spec.rb +45 -0
- data/spec/unit/queue_spec.rb +67 -0
- data/spec/unit/work_spec.rb +168 -0
- data/spec/unit/worker_spec.rb +31 -0
- metadata +149 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: df82eb95360d4f2dd2d6ae5185a209836946a5a3
|
4
|
+
data.tar.gz: edbd98c7b468295a1d67ff135c076def834a1f0f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d93d47004b33e2f1e7e82e05351b629238d36eb9e48b8a142f421d267217f99ba51c6916267e9fb2c636a0d0ddfc5ab680c8b1168b6f089ace6cb396bb2989a1
|
7
|
+
data.tar.gz: c697670fcd1525986e22fa743cbae99b7e0107143c7e2b7e5b26ab52a4bdc4ec8f1d0fcb295cc3dab1861499af42186480058fde8e5ebc1827ad6a7ddf0a515c
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Chris Hanks
|
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,102 @@
|
|
1
|
+
# Que
|
2
|
+
|
3
|
+
A job queue that uses Postgres' advisory lock system. The aim is for job queuing to be efficient and highly concurrent (there's no need for a locked_at column or SELECT FOR UPDATE NOWAIT queries) but still very durable (with all the stability that Postgres offers).
|
4
|
+
|
5
|
+
It was extracted from an app of mine where it worked well for me, but I wouldn't recommend anyone use it until it's generalized some more. It currently requires the use of Sequel as an ORM, but I expect that it could be expanded to support ActiveRecord.
|
6
|
+
|
7
|
+
Features:
|
8
|
+
* Jobs can be queued transactionally, alongside every other change to your database.
|
9
|
+
* If a worker process crashes or segfaults, the jobs it was working are immediately released to be picked up by other workers.
|
10
|
+
* Workers are multi-threaded, similar to Sidekiq, so the same process can work many jobs at the same time.
|
11
|
+
* Workers can run in your web processes. This means if you're on Heroku, there's no need to have a worker process constantly running.
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
Add this line to your application's Gemfile:
|
16
|
+
|
17
|
+
gem 'que'
|
18
|
+
|
19
|
+
And then execute:
|
20
|
+
|
21
|
+
$ bundle
|
22
|
+
|
23
|
+
Or install it yourself as:
|
24
|
+
|
25
|
+
$ gem install que
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
|
29
|
+
Create a jobs table that looks something like:
|
30
|
+
|
31
|
+
CREATE TABLE jobs
|
32
|
+
(
|
33
|
+
priority integer NOT NULL,
|
34
|
+
run_at timestamp with time zone NOT NULL DEFAULT now(),
|
35
|
+
job_id bigserial NOT NULL,
|
36
|
+
created_at timestamp with time zone NOT NULL DEFAULT now(),
|
37
|
+
type text NOT NULL,
|
38
|
+
args json NOT NULL DEFAULT '[]'::json,
|
39
|
+
data json NOT NULL DEFAULT '{}'::json,
|
40
|
+
CONSTRAINT jobs_pkey PRIMARY KEY (priority, run_at, job_id),
|
41
|
+
CONSTRAINT valid_priority CHECK (priority >= 1 AND priority <= 5)
|
42
|
+
);
|
43
|
+
|
44
|
+
Start up the worker to process jobs:
|
45
|
+
|
46
|
+
# In an initializer:
|
47
|
+
Que::Worker.state = :async
|
48
|
+
|
49
|
+
# Alternatively, you can set state to :sync to process jobs in the same
|
50
|
+
# thread as they're queued, which is useful for testing, or set it to :off
|
51
|
+
# to simply leave them in the jobs table.
|
52
|
+
|
53
|
+
Create a class for each type of job you want to run:
|
54
|
+
|
55
|
+
class ChargeCreditCard < Que::Job
|
56
|
+
@default_priority = 1 # Highest priority.
|
57
|
+
|
58
|
+
def perform(user_id, card_id)
|
59
|
+
# Do stuff.
|
60
|
+
|
61
|
+
db.transaction do
|
62
|
+
# Write any changes you'd like to the database.
|
63
|
+
|
64
|
+
# It's best to destroy the job in the same transaction as any other
|
65
|
+
# changes you make. Que will destroy the job for you after the
|
66
|
+
# perform method if you don't do it here, but if your job writes to
|
67
|
+
# the DB but doesn't destroy the job in the same transaction, it's
|
68
|
+
# possible that the job could be repeated in the event of a crash.
|
69
|
+
destroy
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
Queue that type of job. Again, it's best to take advantage of Postgres by doing this in a transaction with other changes you're making:
|
75
|
+
|
76
|
+
DB.transaction do
|
77
|
+
# Persist credit card information
|
78
|
+
card = CreditCard.create(params[:credit_card])
|
79
|
+
ChargeCreditCard.queue(current_user.id, card.id)
|
80
|
+
end
|
81
|
+
|
82
|
+
## TODO
|
83
|
+
|
84
|
+
These aren't promises, just ideas for stuff that could be done in the future.
|
85
|
+
|
86
|
+
* Railtie, to make default setup easy.
|
87
|
+
* ActiveRecord support.
|
88
|
+
* Keep deleted jobs.
|
89
|
+
* Use LISTEN/NOTIFY for checking for new jobs, rather than signaling the worker within the same process.
|
90
|
+
* Multiple queues (in multiple tables?)
|
91
|
+
* More configurable logging that isn't tied to Rails.
|
92
|
+
* Integration with ActionMailer for easier mailings.
|
93
|
+
* Options for max_run_time and max_attempts that are specific to job classes.
|
94
|
+
* Rake tasks for creating/working/dropping/clearing queues.
|
95
|
+
|
96
|
+
## Contributing
|
97
|
+
|
98
|
+
1. Fork it
|
99
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
100
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
101
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
102
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
data/lib/que.rb
ADDED
data/lib/que/job.rb
ADDED
@@ -0,0 +1,185 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Que
|
4
|
+
class Job < Sequel::Model
|
5
|
+
# The Job priority scale:
|
6
|
+
# 1 = Urgent. Somebody's staring at a spinner waiting on this.
|
7
|
+
# 2 = ASAP. Should happen within a few minutes of the run_at time.
|
8
|
+
# 3 = Time-sensitive. Sooner is better than later.
|
9
|
+
# 4 = Time-insensitive. Shouldn't get delayed forever, though.
|
10
|
+
# 5 = Whenever. Timing doesn't matter. May be a huge backlog of these.
|
11
|
+
|
12
|
+
unrestrict_primary_key
|
13
|
+
|
14
|
+
plugin :single_table_inheritance, :type, :key_map => proc(&:to_s),
|
15
|
+
:model_map => proc{|s| const_get "::#{s}"}
|
16
|
+
|
17
|
+
class Retry < StandardError; end
|
18
|
+
|
19
|
+
class << self
|
20
|
+
# Default is lowest priority, meaning jobs can be done whenever.
|
21
|
+
def default_priority
|
22
|
+
@default_priority ||= 5
|
23
|
+
end
|
24
|
+
|
25
|
+
def queue(*args)
|
26
|
+
create values_for_args *args
|
27
|
+
end
|
28
|
+
|
29
|
+
def work(options = {})
|
30
|
+
# Since we're taking session-level advisory locks, we have to hold the
|
31
|
+
# same connection throughout the process of getting a job, working it,
|
32
|
+
# deleting it, and removing the lock.
|
33
|
+
|
34
|
+
DB.synchronize do
|
35
|
+
begin
|
36
|
+
return unless job = LOCK.call(:priority => options[:priority] || 5)
|
37
|
+
|
38
|
+
# Edge case: It's possible for the lock statement to have grabbed a
|
39
|
+
# job that's already been worked, if the statement took its MVCC
|
40
|
+
# snapshot while the job was processing (making it appear to still
|
41
|
+
# exist), but didn't actually attempt to lock it until the job was
|
42
|
+
# finished (making it appear to be unlocked). Now that we have the
|
43
|
+
# job lock, we know that a previous worker would have deleted it by
|
44
|
+
# now, so we just make sure it still exists before working it.
|
45
|
+
this = dataset.where(:priority => job[:priority], :run_at => job[:run_at], :job_id => job[:job_id])
|
46
|
+
return if this.empty?
|
47
|
+
|
48
|
+
# Split up model instantiation from the DB query, so that model
|
49
|
+
# instantiation errors can be caught.
|
50
|
+
model = sti_load(job)
|
51
|
+
|
52
|
+
# Track how long different jobs take to process.
|
53
|
+
start = Time.now
|
54
|
+
model.work
|
55
|
+
time = Time.now - start
|
56
|
+
Que.logger.info "Worked job in #{(time * 1000).round(1)} ms: #{model.inspect}" if Que.logger
|
57
|
+
|
58
|
+
# Most jobs destroy themselves transactionally in #work. If not,
|
59
|
+
# take care of them. Jobs that don't destroy themselves run the risk
|
60
|
+
# of being repeated after a crash.
|
61
|
+
model.destroy unless model.destroyed?
|
62
|
+
|
63
|
+
# Make sure to return the finished job.
|
64
|
+
model
|
65
|
+
rescue Retry
|
66
|
+
# Don't destroy the job or mark it as having errored. It can be
|
67
|
+
# retried as soon as it is unlocked.
|
68
|
+
rescue => error
|
69
|
+
if job && data = JSON.load(job[:data])
|
70
|
+
count = (data['error_count'] || 0) + 1
|
71
|
+
|
72
|
+
this.update :run_at => Time.now + (count ** 4 + 3),
|
73
|
+
:data => JSON.dump(:error_count => count, :error_message => error.message, :error_backtrace => error.backtrace.join("\n"))
|
74
|
+
end
|
75
|
+
|
76
|
+
raise
|
77
|
+
ensure
|
78
|
+
DB.get{pg_advisory_unlock(job[:job_id])} if job
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def values_for_args(*args)
|
86
|
+
opts = args.last.is_a?(Hash) ? args.pop : {}
|
87
|
+
|
88
|
+
result = {}
|
89
|
+
result[:run_at] = opts.delete(:run_at) if opts[:run_at]
|
90
|
+
result[:priority] = opts.delete(:priority) if opts[:priority]
|
91
|
+
|
92
|
+
args << opts if opts.any?
|
93
|
+
result[:args] = JSON.dump(args)
|
94
|
+
|
95
|
+
result
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Send the args attribute to the perform() method.
|
100
|
+
def work
|
101
|
+
perform(*JSON.parse(args))
|
102
|
+
end
|
103
|
+
|
104
|
+
# Call perform on a job to run it. No perform method means NOOP.
|
105
|
+
def perform(*args)
|
106
|
+
end
|
107
|
+
|
108
|
+
def destroyed?
|
109
|
+
!!@destroyed
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
# If we add any more callbacks here, make sure to also special-case them in
|
115
|
+
# queue_array above.
|
116
|
+
def before_create
|
117
|
+
self.priority ||= self.class.default_priority
|
118
|
+
|
119
|
+
# If there's no run_at set, the job needs to be run immediately, so we
|
120
|
+
# need to trigger a worker to work it after it's committed and visible.
|
121
|
+
if run_at.nil?
|
122
|
+
case Worker.state
|
123
|
+
when :sync then DB.after_commit { Job.work }
|
124
|
+
when :async then DB.after_commit { Worker.wake! }
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
super
|
129
|
+
end
|
130
|
+
|
131
|
+
def after_destroy
|
132
|
+
super
|
133
|
+
@destroyed = true
|
134
|
+
end
|
135
|
+
|
136
|
+
sql = <<-SQL
|
137
|
+
WITH RECURSIVE cte AS (
|
138
|
+
SELECT (job).*, pg_try_advisory_lock((job).job_id) AS locked
|
139
|
+
FROM (
|
140
|
+
SELECT job
|
141
|
+
FROM jobs AS job
|
142
|
+
WHERE ((run_at <= now()) AND (priority <= ?))
|
143
|
+
ORDER BY priority, run_at, job_id
|
144
|
+
LIMIT 1
|
145
|
+
) AS t1
|
146
|
+
UNION ALL (
|
147
|
+
SELECT (job).*, pg_try_advisory_lock((job).job_id) AS locked
|
148
|
+
FROM (
|
149
|
+
SELECT (
|
150
|
+
SELECT job
|
151
|
+
FROM jobs AS job
|
152
|
+
WHERE ((run_at <= now()) AND (priority <= ?) AND ((priority, run_at, job_id) > (cte.priority, cte.run_at, cte.job_id)))
|
153
|
+
ORDER BY priority, run_at, job_id
|
154
|
+
LIMIT 1
|
155
|
+
) AS job
|
156
|
+
FROM cte
|
157
|
+
WHERE NOT cte.locked
|
158
|
+
LIMIT 1
|
159
|
+
) AS t1)
|
160
|
+
)
|
161
|
+
SELECT *
|
162
|
+
FROM cte
|
163
|
+
WHERE locked
|
164
|
+
SQL
|
165
|
+
|
166
|
+
LOCK = DB[sql, :$priority, :$priority].prepare(:first, :lock_job)
|
167
|
+
|
168
|
+
# An alternate scheme using LATERAL, which will arrive in Postgres 9.3.
|
169
|
+
# Basically the same, but benchmark to see if it's faster/just as reliable.
|
170
|
+
|
171
|
+
# with recursive
|
172
|
+
# t as (select *, pg_try_advisory_lock(s.job_id) as locked
|
173
|
+
# from (select * from jobs j
|
174
|
+
# where run_at >= now()
|
175
|
+
# order by priority, run_at, job_id limit 1) s
|
176
|
+
# union all
|
177
|
+
# select j.*, pg_try_advisory_lock(j.job_id)
|
178
|
+
# from (select * from t where not locked) t,
|
179
|
+
# lateral (select * from jobs
|
180
|
+
# where run_at >= now()
|
181
|
+
# and (priority,run_at,job_id) > (t.priority,t.run_at,t.job_id)
|
182
|
+
# order by priority, run_at, job_id limit 1) j
|
183
|
+
# select * from t where locked;
|
184
|
+
end
|
185
|
+
end
|
data/lib/que/version.rb
ADDED
data/lib/que/worker.rb
ADDED
@@ -0,0 +1,216 @@
|
|
1
|
+
module Que
|
2
|
+
# There are multiple workers running at a given time, each with a given
|
3
|
+
# minimum priority that they care about for jobs. A worker will continuously
|
4
|
+
# look for jobs and work them. If there's no job available, the worker will go
|
5
|
+
# to sleep until it is awakened by an external thread.
|
6
|
+
|
7
|
+
# Use Worker.state = ... to set the current state. There are three states:
|
8
|
+
# :async => Work jobs in dedicated threads. Used in production.
|
9
|
+
# :sync => Work jobs immediately, as they're queued, in the current thread. Used in testing.
|
10
|
+
# :off => Don't work jobs at all. Must use Job#work or Job.work explicitly.
|
11
|
+
|
12
|
+
# Worker.wake! will wake up the sleeping worker with the lowest minimum job
|
13
|
+
# priority. Worker.wake! may be run by another thread handling a web request,
|
14
|
+
# or by the wrangler thread (which wakes a worker every five seconds, to
|
15
|
+
# handle scheduled jobs). It only has an effect when running in async mode.
|
16
|
+
|
17
|
+
class Worker
|
18
|
+
# Each worker has a corresponding thread, which contains two variables:
|
19
|
+
# :directive, to define what it should be doing, and :state, to define what
|
20
|
+
# it's actually doing at the moment. Need to be careful that these variables
|
21
|
+
# are only modified by a single thread at a time (hence, MonitorMixin).
|
22
|
+
include MonitorMixin
|
23
|
+
|
24
|
+
# The Worker class itself needs to be protected as well, to make sure that
|
25
|
+
# multiple threads aren't stopping/starting it at the same time.
|
26
|
+
extend MonitorMixin
|
27
|
+
|
28
|
+
# Default worker priorities. Rule of thumb: number of lowest-priority
|
29
|
+
# workers should equal number of processors available to us.
|
30
|
+
PRIORITIES = [5, 5, 5, 5, 4, 3, 2, 1].freeze
|
31
|
+
|
32
|
+
# Which errors should signal a worker that it should hold off before trying
|
33
|
+
# to grab another job, in order to avoid spamming the logs.
|
34
|
+
DELAYABLE_ERRORS = %w(
|
35
|
+
Sequel::DatabaseConnectionError
|
36
|
+
Sequel::DatabaseDisconnectError
|
37
|
+
)
|
38
|
+
|
39
|
+
# How long the wrangler thread should wait between pings of the database.
|
40
|
+
# Future directions: when we have multiple dynos, add rand() to this value
|
41
|
+
# in the wrangler loop below, so that the dynos' checks will be spaced out.
|
42
|
+
SLEEP_PERIOD = 5
|
43
|
+
|
44
|
+
# How long to sleep, in repeated increments, for something to happen.
|
45
|
+
WAIT_PERIOD = 0.0001 # 0.1 ms
|
46
|
+
|
47
|
+
# How long a worker should wait before trying to get another job, in the
|
48
|
+
# event of a database connection problem.
|
49
|
+
ERROR_PERIOD = 5
|
50
|
+
|
51
|
+
attr_reader :thread, :priority
|
52
|
+
|
53
|
+
def initialize(priority)
|
54
|
+
super() # For MonitorMixin
|
55
|
+
|
56
|
+
# These threads have a bad habit of never even having their directive and
|
57
|
+
# state set if we do it inside their threads. So instead, force the issue
|
58
|
+
# by doing it outside and holding them up via a queue until their initial
|
59
|
+
# state is set.
|
60
|
+
q = Queue.new
|
61
|
+
|
62
|
+
@priority = priority
|
63
|
+
@thread = Thread.new do
|
64
|
+
q.pop
|
65
|
+
job = nil
|
66
|
+
|
67
|
+
loop do
|
68
|
+
sleep! unless work_job
|
69
|
+
|
70
|
+
if @thread[:directive] == :sleep
|
71
|
+
@thread[:state] = :sleeping
|
72
|
+
sleep
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# All workers are working when first instantiated.
|
78
|
+
synchronize { @thread[:directive], @thread[:state] = :work, :working }
|
79
|
+
|
80
|
+
# Now the worker can start.
|
81
|
+
q.push nil
|
82
|
+
|
83
|
+
# Default thread priority is 0 - make worker threads a bit less important
|
84
|
+
# than threads that are handling requests.
|
85
|
+
@thread.priority = -1
|
86
|
+
end
|
87
|
+
|
88
|
+
# If the worker is asleep, wakes it up and returns truthy. If it's already
|
89
|
+
# awake, does nothing and returns falsy.
|
90
|
+
def wake!
|
91
|
+
synchronize do
|
92
|
+
if sleeping?
|
93
|
+
# There's a very brief period of time where the worker may be marked
|
94
|
+
# as sleeping but the thread hasn't actually gone to sleep yet.
|
95
|
+
wait until @thread.stop?
|
96
|
+
@thread[:directive] = :work
|
97
|
+
|
98
|
+
# Have to set state here so that another poke immediately after this
|
99
|
+
# one doesn't see the current state as sleeping.
|
100
|
+
@thread[:state] = :working
|
101
|
+
|
102
|
+
# Now it's safe to wake up the worker.
|
103
|
+
@thread.wakeup
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def sleep!
|
109
|
+
synchronize { @thread[:directive] = :sleep }
|
110
|
+
end
|
111
|
+
|
112
|
+
def awake?
|
113
|
+
synchronize do
|
114
|
+
%w(sleeping working).include?(@thread[:state].to_s) &&
|
115
|
+
%w(sleep work).include?(@thread[:directive].to_s) &&
|
116
|
+
%w(sleep run).include?(@thread.status)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def wait_for_sleep
|
121
|
+
wait until synchronize { sleeping? }
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
def work_job
|
127
|
+
Job.work(:priority => priority)
|
128
|
+
rescue => error
|
129
|
+
self.class.notify_error "Worker error!", error
|
130
|
+
sleep ERROR_PERIOD if DELAYABLE_ERRORS.include?(error.class.to_s)
|
131
|
+
return true # There's work available.
|
132
|
+
end
|
133
|
+
|
134
|
+
def sleeping?
|
135
|
+
@thread[:state] == :sleeping
|
136
|
+
end
|
137
|
+
|
138
|
+
def wait
|
139
|
+
sleep WAIT_PERIOD
|
140
|
+
end
|
141
|
+
|
142
|
+
# The Worker class is responsible for managing the worker instances.
|
143
|
+
class << self
|
144
|
+
def state=(state)
|
145
|
+
synchronize do
|
146
|
+
Que.logger.info "Setting Worker to #{state}..." if Que.logger
|
147
|
+
case state
|
148
|
+
when :async
|
149
|
+
# If this is the first time starting up Worker, start up all workers
|
150
|
+
# immediately, for the case of a restart during heavy app usage.
|
151
|
+
workers
|
152
|
+
# Make sure the wrangler thread is running, it'll do the rest.
|
153
|
+
@wrangler ||= Thread.new { loop { wrangle } }
|
154
|
+
when :sync, :off
|
155
|
+
# Put all the workers to sleep.
|
156
|
+
workers.each(&:sleep!).each(&:wait_for_sleep)
|
157
|
+
else
|
158
|
+
raise "Bad Worker state! #{state.inspect}"
|
159
|
+
end
|
160
|
+
|
161
|
+
Que.logger.info "Set Worker to #{state}" if Que.logger
|
162
|
+
@state = state
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def state
|
167
|
+
synchronize { @state ||= :off }
|
168
|
+
end
|
169
|
+
|
170
|
+
def async?
|
171
|
+
state == :async
|
172
|
+
end
|
173
|
+
|
174
|
+
# All workers are up and processing jobs?
|
175
|
+
def up?(*states)
|
176
|
+
synchronize { async? && workers.map(&:priority) == PRIORITIES && workers.all?(&:awake?) }
|
177
|
+
end
|
178
|
+
|
179
|
+
def workers
|
180
|
+
@workers || synchronize { @workers ||= PRIORITIES.map { |i| new(i) } }
|
181
|
+
end
|
182
|
+
|
183
|
+
# Wake up just one worker to work a job, if running async.
|
184
|
+
def wake!
|
185
|
+
synchronize { async? && workers.find(&:wake!) }
|
186
|
+
end
|
187
|
+
|
188
|
+
def notify_error(message, error)
|
189
|
+
log_error message, error
|
190
|
+
#ExceptionNotifier.notify_exception(error)
|
191
|
+
rescue => error
|
192
|
+
log_error "Error notification error!", error
|
193
|
+
end
|
194
|
+
|
195
|
+
private
|
196
|
+
|
197
|
+
# The wrangler runs this method continuously.
|
198
|
+
def wrangle
|
199
|
+
sleep SLEEP_PERIOD
|
200
|
+
wake!
|
201
|
+
rescue => error
|
202
|
+
notify_error "Wrangler Error!", error
|
203
|
+
end
|
204
|
+
|
205
|
+
def log_error(message, error)
|
206
|
+
if Que.logger
|
207
|
+
Que.logger.error <<-ERROR
|
208
|
+
#{message}
|
209
|
+
#{error.message}
|
210
|
+
#{error.backtrace.join("\n")}
|
211
|
+
ERROR
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
data/que.gemspec
ADDED
@@ -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 'que/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'que'
|
8
|
+
spec.version = Que::VERSION
|
9
|
+
spec.authors = ["Chris Hanks"]
|
10
|
+
spec.email = ['christopher.m.hanks@gmail.com']
|
11
|
+
spec.description = %q{Durable job queueing with PostgreSQL.}
|
12
|
+
spec.summary = %q{Durable, efficient job queueing with PostgreSQL.}
|
13
|
+
spec.homepage = ''
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
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_development_dependency 'bundler', '~> 1.3'
|
22
|
+
spec.add_development_dependency 'rake'
|
23
|
+
spec.add_development_dependency 'rspec', '~> 2.14.1'
|
24
|
+
spec.add_development_dependency 'pry'
|
25
|
+
|
26
|
+
spec.add_dependency 'sequel'
|
27
|
+
spec.add_dependency 'pg'
|
28
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'sequel'
|
2
|
+
require 'que'
|
3
|
+
|
4
|
+
DB = Sequel.connect "postgres://postgres:@localhost/que"
|
5
|
+
|
6
|
+
DB.drop_table? :jobs
|
7
|
+
DB.run <<-SQL
|
8
|
+
CREATE TABLE jobs
|
9
|
+
(
|
10
|
+
priority integer NOT NULL,
|
11
|
+
run_at timestamp with time zone NOT NULL DEFAULT now(),
|
12
|
+
job_id bigserial NOT NULL,
|
13
|
+
created_at timestamp with time zone NOT NULL DEFAULT now(),
|
14
|
+
type text NOT NULL,
|
15
|
+
args json NOT NULL DEFAULT '[]'::json,
|
16
|
+
data json NOT NULL DEFAULT '{}'::json,
|
17
|
+
CONSTRAINT jobs_pkey PRIMARY KEY (priority, run_at, job_id),
|
18
|
+
CONSTRAINT valid_priority CHECK (priority >= 1 AND priority <= 5)
|
19
|
+
);
|
20
|
+
SQL
|
21
|
+
|
22
|
+
RSpec.configure do |config|
|
23
|
+
config.before do
|
24
|
+
Que::Worker.state = :off
|
25
|
+
DB[:jobs].delete
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
Que::Worker.state = :async # Boot up.
|
30
|
+
|
31
|
+
# For use when debugging specs:
|
32
|
+
# require 'logger'
|
33
|
+
# Que.logger = Logger.new(STDOUT)
|
34
|
+
# DB.loggers << Logger.new(STDOUT)
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "Que::Job error handling" do
|
4
|
+
class ErrorJob < Que::Job
|
5
|
+
def perform(*args)
|
6
|
+
raise "Boo!"
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should increment the error_count, persist the error message and reschedule the job" do
|
11
|
+
ErrorJob.queue
|
12
|
+
|
13
|
+
proc { Que::Job.work }.should raise_error RuntimeError, "Boo!"
|
14
|
+
Que::Job.count.should be 1
|
15
|
+
|
16
|
+
job = Que::Job.first
|
17
|
+
data = JSON.load(job.data)
|
18
|
+
data['error_count'].should be 1
|
19
|
+
data['error_message'].should =~ /Boo!/
|
20
|
+
job.run_at.should be_within(1).of Time.now + 4
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should reschedule jobs with exponentially increasing times" do
|
24
|
+
ErrorJob.queue
|
25
|
+
Que::Job.dataset.update :data => JSON.dump(:error_count => 5)
|
26
|
+
|
27
|
+
proc { Que::Job.work }.should raise_error RuntimeError, "Boo!"
|
28
|
+
Que::Job.count.should be 1
|
29
|
+
|
30
|
+
job = Que::Job.first
|
31
|
+
data = JSON.load(job.data)
|
32
|
+
data['error_count'].should be 6
|
33
|
+
data['error_message'].should =~ /Boo!/
|
34
|
+
job.run_at.should be_within(1).of Time.now + 1299
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should handle errors from jobs that cannot be deserialized" do
|
38
|
+
DB[:jobs].insert :type => 'NonexistentJob', :priority => 1
|
39
|
+
proc { Que::Job.work }.should raise_error NameError, /uninitialized constant NonexistentJob/
|
40
|
+
|
41
|
+
job = DB[:jobs].first
|
42
|
+
JSON.load(job[:data])['error_count'].should be 1
|
43
|
+
job[:run_at].should be_within(1).of Time.now + 4
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "Que::Job.queue" do
|
4
|
+
it "should create a job in the DB" do
|
5
|
+
Que::Job.queue :param1 => 4, :param2 => 'ferret', :param3 => false
|
6
|
+
Que::Job.count.should == 1
|
7
|
+
|
8
|
+
job = Que::Job.first
|
9
|
+
job.type.should == 'Que::Job'
|
10
|
+
job.run_at.should be_within(1).of Time.now
|
11
|
+
job.priority.should be 5 # Defaults to lowest priority.
|
12
|
+
JSON.load(job.args).should == [{'param1' => 4, 'param2' => 'ferret', 'param3' => false}]
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should accept a :run_at argument" do
|
16
|
+
time = Time.at(Time.now.to_i)
|
17
|
+
Que::Job.queue :user_id => 4, :test_number => 8, :run_at => time
|
18
|
+
|
19
|
+
Que::Job.count.should == 1
|
20
|
+
job = Que::Job.first
|
21
|
+
job.type.should == 'Que::Job'
|
22
|
+
job.run_at.should == time
|
23
|
+
job.priority.should == 5
|
24
|
+
JSON.load(job.args).should == [{'user_id' => 4, 'test_number' => 8}]
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should accept a :priority argument" do
|
28
|
+
Que::Job.queue :user_id => 4, :test_number => 8, :priority => 1
|
29
|
+
|
30
|
+
Que::Job.count.should == 1
|
31
|
+
job = Que::Job.first
|
32
|
+
job.type.should == 'Que::Job'
|
33
|
+
job.run_at.should be_within(1).of Time.now
|
34
|
+
job.priority.should be 1
|
35
|
+
JSON.load(job.args).should == [{'user_id' => 4, 'test_number' => 8}]
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should respect a default_priority for the class" do
|
39
|
+
class TestPriorityJob < Que::Job
|
40
|
+
@default_priority = 2
|
41
|
+
end
|
42
|
+
|
43
|
+
TestPriorityJob.queue :user_id => 4, :test_number => 8
|
44
|
+
|
45
|
+
Que::Job.count.should == 1
|
46
|
+
job = Que::Job.first
|
47
|
+
job.type.should == 'TestPriorityJob'
|
48
|
+
job.run_at.should be_within(1).of Time.now
|
49
|
+
job.priority.should be 2
|
50
|
+
JSON.load(job.args).should == [{'user_id' => 4, 'test_number' => 8}]
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should let a :priority option override a default_priority for the class" do
|
54
|
+
class OtherTestPriorityJob < Que::Job
|
55
|
+
@default_priority = 2
|
56
|
+
end
|
57
|
+
|
58
|
+
OtherTestPriorityJob.queue :user_id => 4, :test_number => 8, :priority => 4
|
59
|
+
|
60
|
+
Que::Job.count.should == 1
|
61
|
+
job = Que::Job.first
|
62
|
+
job.type.should == 'OtherTestPriorityJob'
|
63
|
+
job.run_at.should be_within(1).of Time.now
|
64
|
+
job.priority.should be 4
|
65
|
+
JSON.load(job.args).should == [{'user_id' => 4, 'test_number' => 8}]
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "Que::Job.work" do
|
4
|
+
it "should automatically delete jobs from the database's queue" do
|
5
|
+
Que::Job.count.should be 0
|
6
|
+
Que::Job.queue
|
7
|
+
Que::Job.count.should be 1
|
8
|
+
Que::Job.work
|
9
|
+
Que::Job.count.should be 0
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should pass a job's arguments to its perform method" do
|
13
|
+
class JobWorkTest < Que::Job
|
14
|
+
def perform(*args)
|
15
|
+
$passed_args = args
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
JobWorkTest.queue 5, 'ferret', :lazy => true
|
20
|
+
|
21
|
+
Que::Job.work
|
22
|
+
$passed_args.should == [5, 'ferret', {'lazy' => true}]
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should prefer a job with higher priority" do
|
26
|
+
Que::Job.queue
|
27
|
+
Que::Job.queue :priority => 1
|
28
|
+
|
29
|
+
Que::Job.select_order_map(:priority).should == [1, 5]
|
30
|
+
Que::Job.work
|
31
|
+
Que::Job.select_order_map(:priority).should == [5]
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should prefer a job that was scheduled to run longer ago" do
|
35
|
+
now = Time.at(Time.now.to_i) # Prevent rounding errors by rounding to the nearest second.
|
36
|
+
recently = now - 60
|
37
|
+
long_ago = now - 61
|
38
|
+
|
39
|
+
Que::Job.queue :run_at => recently
|
40
|
+
Que::Job.queue :run_at => long_ago
|
41
|
+
|
42
|
+
Que::Job.select_order_map(:run_at).should == [long_ago, recently]
|
43
|
+
Que::Job.work
|
44
|
+
Que::Job.select_order_map(:run_at).should == [recently]
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should only work a job whose run_at has already passed" do
|
48
|
+
now = Time.at(Time.now.to_i) # Prevent rounding errors by rounding to the nearest second.
|
49
|
+
past = now - 60
|
50
|
+
soon = now + 60
|
51
|
+
|
52
|
+
Que::Job.queue :run_at => past
|
53
|
+
Que::Job.queue :run_at => soon
|
54
|
+
|
55
|
+
Que::Job.select_order_map(:run_at).should == [past, soon]
|
56
|
+
Que::Job.work
|
57
|
+
Que::Job.select_order_map(:run_at).should == [soon]
|
58
|
+
Que::Job.work
|
59
|
+
Que::Job.select_order_map(:run_at).should == [soon]
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should prefer a job that was scheduled earlier, and therefore has a lower job_id" do
|
63
|
+
time = Time.now - 60
|
64
|
+
Que::Job.queue :run_at => time
|
65
|
+
Que::Job.queue :run_at => time
|
66
|
+
|
67
|
+
a, b = Que::Job.select_order_map(:job_id)
|
68
|
+
Que::Job.work
|
69
|
+
Que::Job.select_order_map(:job_id).should == [b]
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should lock the job it selects" do
|
73
|
+
$q1, $q2 = Queue.new, Queue.new
|
74
|
+
|
75
|
+
class LockJob < Que::Job
|
76
|
+
def perform(*args)
|
77
|
+
$q1.push nil
|
78
|
+
$q2.pop
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
job = LockJob.queue
|
83
|
+
@thread = Thread.new { Que::Job.work }
|
84
|
+
|
85
|
+
# Wait until job is being worked.
|
86
|
+
$q1.pop
|
87
|
+
|
88
|
+
# Job should be advisory-locked...
|
89
|
+
DB.select{pg_try_advisory_lock(job.job_id)}.single_value.should be false
|
90
|
+
|
91
|
+
# ...and Job.work should ignore advisory-locked jobs.
|
92
|
+
Que::Job.work.should be nil
|
93
|
+
|
94
|
+
# Let LockJob finish.
|
95
|
+
$q2.push nil
|
96
|
+
|
97
|
+
# Make sure there aren't any errors.
|
98
|
+
@thread.join
|
99
|
+
end
|
100
|
+
|
101
|
+
it "that raises a Que::Job::Retry should abort the job, leaving it to be retried" do
|
102
|
+
class RetryJob < Que::Job
|
103
|
+
def perform(*args)
|
104
|
+
raise Que::Job::Retry
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
job = RetryJob.queue
|
109
|
+
Que::Job.count.should be 1
|
110
|
+
Que::Job.work
|
111
|
+
Que::Job.count.should be 1
|
112
|
+
|
113
|
+
same_job = Que::Job.first
|
114
|
+
same_job.job_id.should == job.job_id
|
115
|
+
same_job.run_at.should == job.run_at
|
116
|
+
same_job.data['error_count'].should be nil
|
117
|
+
end
|
118
|
+
|
119
|
+
it "should handle subclasses of other jobs" do
|
120
|
+
$class_job_array = []
|
121
|
+
|
122
|
+
class ClassJob < Que::Job
|
123
|
+
def perform(*args)
|
124
|
+
$class_job_array << 2
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
class SubclassJob < ClassJob
|
129
|
+
def perform(*args)
|
130
|
+
$class_job_array << 1
|
131
|
+
super
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
SubclassJob.queue
|
136
|
+
Que::Job.get(:type).should == 'SubclassJob'
|
137
|
+
Que::Job.work
|
138
|
+
$class_job_array.should == [1, 2]
|
139
|
+
end
|
140
|
+
|
141
|
+
describe "should support a logger" do
|
142
|
+
before do
|
143
|
+
@logger = Object.new
|
144
|
+
def @logger.method_missing(m, message)
|
145
|
+
@messages ||= []
|
146
|
+
@messages << message
|
147
|
+
end
|
148
|
+
Que.logger = @logger
|
149
|
+
end
|
150
|
+
|
151
|
+
after do
|
152
|
+
Que.logger = nil
|
153
|
+
end
|
154
|
+
|
155
|
+
def messages
|
156
|
+
@logger.instance_variable_get(:@messages)
|
157
|
+
end
|
158
|
+
|
159
|
+
it "should write messages to the logger" do
|
160
|
+
Que::Job.queue
|
161
|
+
Que::Job.work
|
162
|
+
|
163
|
+
messages.should be_an_instance_of Array
|
164
|
+
messages.length.should == 1
|
165
|
+
messages[0].should =~ /\AWorked job in/
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Que::Worker do
|
4
|
+
class ExceptionJob < Que::Job
|
5
|
+
def perform(*args)
|
6
|
+
raise "Blah!"
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should not be taken out by an error, and keep looking for jobs" do
|
11
|
+
ExceptionJob.queue
|
12
|
+
Que::Job.queue
|
13
|
+
|
14
|
+
Que::Worker.state = :async
|
15
|
+
Que::Worker.wake!
|
16
|
+
@worker = Que::Worker.workers.first
|
17
|
+
|
18
|
+
{} until @worker.thread[:state] == :sleeping
|
19
|
+
|
20
|
+
# Job was worked, ExceptionJob remains.
|
21
|
+
Que::Job.select_map(:type).should == ['ExceptionJob']
|
22
|
+
end
|
23
|
+
|
24
|
+
it "#async? and #up? should return whether Worker is async and whether there are workers running, respectively" do
|
25
|
+
Que::Worker.should_not be_async
|
26
|
+
Que::Worker.should_not be_up
|
27
|
+
Que::Worker.state = :async
|
28
|
+
Que::Worker.should be_async
|
29
|
+
Que::Worker.should be_up
|
30
|
+
end
|
31
|
+
end
|
metadata
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: que
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Chris Hanks
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-11-07 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.3'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 2.14.1
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 2.14.1
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: pry
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: sequel
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: pg
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description: Durable job queueing with PostgreSQL.
|
98
|
+
email:
|
99
|
+
- christopher.m.hanks@gmail.com
|
100
|
+
executables: []
|
101
|
+
extensions: []
|
102
|
+
extra_rdoc_files: []
|
103
|
+
files:
|
104
|
+
- .gitignore
|
105
|
+
- .rspec
|
106
|
+
- Gemfile
|
107
|
+
- LICENSE.txt
|
108
|
+
- README.md
|
109
|
+
- Rakefile
|
110
|
+
- lib/que.rb
|
111
|
+
- lib/que/job.rb
|
112
|
+
- lib/que/version.rb
|
113
|
+
- lib/que/worker.rb
|
114
|
+
- que.gemspec
|
115
|
+
- spec/spec_helper.rb
|
116
|
+
- spec/unit/error_spec.rb
|
117
|
+
- spec/unit/queue_spec.rb
|
118
|
+
- spec/unit/work_spec.rb
|
119
|
+
- spec/unit/worker_spec.rb
|
120
|
+
homepage: ''
|
121
|
+
licenses:
|
122
|
+
- MIT
|
123
|
+
metadata: {}
|
124
|
+
post_install_message:
|
125
|
+
rdoc_options: []
|
126
|
+
require_paths:
|
127
|
+
- lib
|
128
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
129
|
+
requirements:
|
130
|
+
- - '>='
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '0'
|
133
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - '>='
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0'
|
138
|
+
requirements: []
|
139
|
+
rubyforge_project:
|
140
|
+
rubygems_version: 2.1.9
|
141
|
+
signing_key:
|
142
|
+
specification_version: 4
|
143
|
+
summary: Durable, efficient job queueing with PostgreSQL.
|
144
|
+
test_files:
|
145
|
+
- spec/spec_helper.rb
|
146
|
+
- spec/unit/error_spec.rb
|
147
|
+
- spec/unit/queue_spec.rb
|
148
|
+
- spec/unit/work_spec.rb
|
149
|
+
- spec/unit/worker_spec.rb
|