que 0.0.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.
- 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
|