que 0.3.0 → 0.4.0
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 +4 -4
- data/CHANGELOG.md +20 -0
- data/README.md +12 -31
- data/docs/advanced_setup.md +50 -0
- data/docs/error_handling.md +17 -0
- data/docs/inspecting_the_queue.md +100 -0
- data/docs/managing_workers.md +67 -0
- data/docs/using_plain_connections.md +34 -0
- data/docs/using_sequel.md +27 -0
- data/docs/writing_reliable_jobs.md +62 -0
- data/lib/que.rb +33 -5
- data/lib/que/adapters/base.rb +9 -1
- data/lib/que/adapters/pg.rb +2 -0
- data/lib/que/adapters/sequel.rb +4 -0
- data/lib/que/job.rb +30 -64
- data/lib/que/rake_tasks.rb +0 -1
- data/lib/que/sql.rb +119 -74
- data/lib/que/version.rb +1 -1
- data/lib/que/worker.rb +39 -26
- data/spec/adapters/active_record_spec.rb +7 -26
- data/spec/adapters/connection_pool_spec.rb +1 -2
- data/spec/adapters/pg_spec.rb +34 -0
- data/spec/adapters/sequel_spec.rb +16 -28
- data/spec/spec_helper.rb +46 -34
- data/spec/support/shared_examples/adapter.rb +16 -3
- data/spec/support/shared_examples/{multithreaded_adapter.rb → multi_threaded_adapter.rb} +3 -1
- data/spec/unit/helper_spec.rb +0 -5
- data/spec/unit/pool_spec.rb +75 -23
- data/spec/unit/queue_spec.rb +5 -1
- data/spec/unit/states_spec.rb +52 -0
- data/spec/unit/stats_spec.rb +42 -0
- data/spec/unit/work_spec.rb +15 -20
- data/spec/unit/worker_spec.rb +18 -3
- data/tasks/safe_shutdown.rb +5 -3
- metadata +16 -5
data/lib/que.rb
CHANGED
@@ -39,19 +39,47 @@ module Que
|
|
39
39
|
execute "DELETE FROM que_jobs"
|
40
40
|
end
|
41
41
|
|
42
|
+
def job_stats
|
43
|
+
execute :job_stats
|
44
|
+
end
|
45
|
+
|
46
|
+
def worker_states
|
47
|
+
execute :worker_states
|
48
|
+
end
|
49
|
+
|
42
50
|
def execute(command, *args)
|
43
|
-
case command
|
44
|
-
|
45
|
-
|
46
|
-
|
51
|
+
indifferentiate case command
|
52
|
+
when Symbol then adapter.execute_prepared(command, *args)
|
53
|
+
when String then adapter.execute(command, *args)
|
54
|
+
end.to_a
|
47
55
|
end
|
48
56
|
|
49
57
|
def log(level, text)
|
50
58
|
logger.send level, "[Que] #{text}" if logger
|
51
59
|
end
|
52
60
|
|
61
|
+
# Helper for making hashes indifferently-accessible, even when nested
|
62
|
+
# within each other and within arrays.
|
63
|
+
def indifferentiate(object)
|
64
|
+
case object
|
65
|
+
when Hash
|
66
|
+
h = if {}.respond_to?(:with_indifferent_access) # Better support for Rails.
|
67
|
+
{}.with_indifferent_access
|
68
|
+
else
|
69
|
+
Hash.new { |hash, key| hash[key.to_s] if Symbol === key }
|
70
|
+
end
|
71
|
+
|
72
|
+
object.each { |k, v| h[k] = indifferentiate(v) }
|
73
|
+
h
|
74
|
+
when Array
|
75
|
+
object.map { |v| indifferentiate(v) }
|
76
|
+
else
|
77
|
+
object
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
53
81
|
# Copy some of the Worker class' config methods here for convenience.
|
54
|
-
[:mode, :mode=, :worker_count=, :
|
82
|
+
[:mode, :mode=, :worker_count, :worker_count=, :wake_interval, :wake_interval=, :stop!, :wake!, :wake_all!].each do |meth|
|
55
83
|
define_method(meth) { |*args| Worker.send(meth, *args) }
|
56
84
|
end
|
57
85
|
end
|
data/lib/que/adapters/base.rb
CHANGED
@@ -12,11 +12,19 @@ module Que
|
|
12
12
|
|
13
13
|
# The only method that adapters really need to implement. Should lock a
|
14
14
|
# PG::Connection (or something that acts like a PG::Connection) so that
|
15
|
-
# no other threads are using it and yield it to the block.
|
15
|
+
# no other threads are using it and yield it to the block. Should also
|
16
|
+
# be re-entrant.
|
16
17
|
def checkout(&block)
|
17
18
|
raise NotImplementedError
|
18
19
|
end
|
19
20
|
|
21
|
+
# Called after a job is queued in async mode, to prompts a worker to
|
22
|
+
# wake up after the current transaction commits. Not all adapters will
|
23
|
+
# implement this.
|
24
|
+
def wake_worker_after_commit
|
25
|
+
false
|
26
|
+
end
|
27
|
+
|
20
28
|
def execute(*args)
|
21
29
|
checkout { |conn| conn.async_exec(*args) }
|
22
30
|
end
|
data/lib/que/adapters/pg.rb
CHANGED
data/lib/que/adapters/sequel.rb
CHANGED
data/lib/que/job.rb
CHANGED
@@ -2,8 +2,11 @@ require 'multi_json'
|
|
2
2
|
|
3
3
|
module Que
|
4
4
|
class Job
|
5
|
+
attr_reader :attrs
|
6
|
+
|
5
7
|
def initialize(attrs)
|
6
|
-
@attrs
|
8
|
+
@attrs = attrs
|
9
|
+
@attrs[:args] = Que.indifferentiate MultiJson.load(@attrs[:args])
|
7
10
|
end
|
8
11
|
|
9
12
|
# Subclasses should define their own run methods, but keep an empty one
|
@@ -12,18 +15,16 @@ module Que
|
|
12
15
|
end
|
13
16
|
|
14
17
|
def _run
|
15
|
-
|
16
|
-
|
17
|
-
run *@attrs[:args]
|
18
|
+
time = Time.now
|
19
|
+
run *attrs[:args]
|
18
20
|
destroy unless @destroyed
|
19
|
-
|
20
|
-
Que.log :info, "Worked job in #{((Time.now - start) * 1000).round(1)} ms: #{inspect}"
|
21
|
+
Que.log :info, "Worked job in #{((Time.now - time) * 1000).round(1)} ms: #{inspect}"
|
21
22
|
end
|
22
23
|
|
23
24
|
private
|
24
25
|
|
25
26
|
def destroy
|
26
|
-
Que.execute :destroy_job,
|
27
|
+
Que.execute :destroy_job, attrs.values_at(:priority, :run_at, :job_id)
|
27
28
|
@destroyed = true
|
28
29
|
end
|
29
30
|
|
@@ -38,18 +39,20 @@ module Que
|
|
38
39
|
|
39
40
|
attrs = {:job_class => to_s, :args => MultiJson.dump(args)}
|
40
41
|
|
41
|
-
if
|
42
|
-
attrs[:run_at] =
|
42
|
+
if time = run_at || @default_run_at && @default_run_at.call
|
43
|
+
attrs[:run_at] = time
|
43
44
|
end
|
44
45
|
|
45
|
-
if
|
46
|
-
attrs[:priority] =
|
46
|
+
if pty = priority || @default_priority
|
47
|
+
attrs[:priority] = pty
|
47
48
|
end
|
48
49
|
|
49
|
-
if Que.mode == :sync && !
|
50
|
+
if Que.mode == :sync && !time
|
50
51
|
run_job(attrs)
|
51
52
|
else
|
52
|
-
Que.execute
|
53
|
+
values = Que.execute(:insert_job, attrs.values_at(:priority, :run_at, :job_class, :args)).first
|
54
|
+
Que.adapter.wake_worker_after_commit unless time
|
55
|
+
new(values)
|
53
56
|
end
|
54
57
|
end
|
55
58
|
|
@@ -65,7 +68,7 @@ module Que
|
|
65
68
|
# deleting it, and removing the lock.
|
66
69
|
Que.adapter.checkout do
|
67
70
|
begin
|
68
|
-
if
|
71
|
+
if job = Que.execute(:lock_job).first
|
69
72
|
# Edge case: It's possible for the lock_job query to have
|
70
73
|
# grabbed a job that's already been worked, if it took its MVCC
|
71
74
|
# snapshot while the job was processing, but didn't attempt the
|
@@ -76,21 +79,21 @@ module Que
|
|
76
79
|
# Note that there is currently no spec for this behavior, since
|
77
80
|
# I'm not sure how to reliably commit a transaction that deletes
|
78
81
|
# the job in a separate thread between lock_job and check_job.
|
79
|
-
return true if Que.execute(:check_job,
|
82
|
+
return true if Que.execute(:check_job, job.values_at(:priority, :run_at, :job_id)).none?
|
80
83
|
|
81
|
-
run_job(
|
84
|
+
run_job(job)
|
82
85
|
else
|
83
86
|
Que.log :info, "No jobs available..."
|
84
87
|
nil
|
85
88
|
end
|
86
89
|
rescue => error
|
87
90
|
begin
|
88
|
-
if
|
91
|
+
if job
|
89
92
|
# Borrowed the backoff formula and error data format from delayed_job.
|
90
|
-
count =
|
91
|
-
|
93
|
+
count = job[:error_count].to_i + 1
|
94
|
+
delay = count ** 4 + 3
|
92
95
|
message = "#{error.message}\n#{error.backtrace.join("\n")}"
|
93
|
-
Que.execute :set_error, [count,
|
96
|
+
Que.execute :set_error, [count, delay, message] + job.values_at(:priority, :run_at, :job_id)
|
94
97
|
end
|
95
98
|
rescue
|
96
99
|
# If we can't reach the database for some reason, too bad, but
|
@@ -110,57 +113,20 @@ module Que
|
|
110
113
|
return !error.is_a?(PG::Error)
|
111
114
|
ensure
|
112
115
|
# Clear the advisory lock we took when locking the job. Important
|
113
|
-
# to do this so that they don't pile up in the database.
|
114
|
-
|
116
|
+
# to do this so that they don't pile up in the database. Again, if
|
117
|
+
# we can't reach the database, don't crash the work loop.
|
118
|
+
begin
|
119
|
+
Que.execute "SELECT pg_advisory_unlock($1)", [job[:job_id]] if job
|
120
|
+
rescue
|
121
|
+
end
|
115
122
|
end
|
116
123
|
end
|
117
124
|
end
|
118
125
|
|
119
126
|
private
|
120
127
|
|
121
|
-
# Column names are not escaped, so this method should not be called with untrusted hashes.
|
122
|
-
def insert_sql(hash)
|
123
|
-
number = 0
|
124
|
-
columns = []
|
125
|
-
placeholders = []
|
126
|
-
values = []
|
127
|
-
|
128
|
-
hash.each do |key, value|
|
129
|
-
columns << key
|
130
|
-
placeholders << "$#{number += 1}"
|
131
|
-
values << value
|
132
|
-
end
|
133
|
-
|
134
|
-
["INSERT INTO que_jobs (#{columns.join(', ')}) VALUES (#{placeholders.join(', ')})", values]
|
135
|
-
end
|
136
|
-
|
137
128
|
def run_job(attrs)
|
138
|
-
attrs
|
139
|
-
attrs[:args] = indifferentiate(MultiJson.load(attrs[:args]))
|
140
|
-
klass = attrs[:job_class].split('::').inject(Object, &:const_get)
|
141
|
-
klass.new(attrs).tap(&:_run)
|
142
|
-
end
|
143
|
-
|
144
|
-
def indifferentiate(input)
|
145
|
-
case input
|
146
|
-
when Hash
|
147
|
-
h = indifferent_hash
|
148
|
-
input.each { |k, v| h[k] = indifferentiate(v) }
|
149
|
-
h
|
150
|
-
when Array
|
151
|
-
input.map { |v| indifferentiate(v) }
|
152
|
-
else
|
153
|
-
input
|
154
|
-
end
|
155
|
-
end
|
156
|
-
|
157
|
-
def indifferent_hash
|
158
|
-
# Tiny hack to better support Rails.
|
159
|
-
if {}.respond_to?(:with_indifferent_access)
|
160
|
-
{}.with_indifferent_access
|
161
|
-
else
|
162
|
-
Hash.new { |hash, key| hash[key.to_s] if Symbol === key }
|
163
|
-
end
|
129
|
+
attrs[:job_class].split('::').inject(Object, &:const_get).new(attrs).tap(&:_run)
|
164
130
|
end
|
165
131
|
end
|
166
132
|
end
|
data/lib/que/rake_tasks.rb
CHANGED
data/lib/que/sql.rb
CHANGED
@@ -1,87 +1,132 @@
|
|
1
1
|
module Que
|
2
2
|
SQL = {
|
3
|
-
# Thanks to RhodiumToad in #postgresql for the job lock CTE.
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
SELECT
|
3
|
+
# Thanks to RhodiumToad in #postgresql for help with the job lock CTE.
|
4
|
+
:lock_job => %{
|
5
|
+
WITH RECURSIVE job AS (
|
6
|
+
SELECT (j).*, pg_try_advisory_lock((j).job_id) AS locked
|
7
|
+
FROM (
|
8
|
+
SELECT j
|
9
|
+
FROM que_jobs AS j
|
10
|
+
WHERE run_at <= now()
|
11
|
+
ORDER BY priority, run_at, job_id
|
12
|
+
LIMIT 1
|
13
|
+
) AS t1
|
14
|
+
UNION ALL (
|
15
|
+
SELECT (j).*, pg_try_advisory_lock((j).job_id) AS locked
|
9
16
|
FROM (
|
10
|
-
SELECT
|
11
|
-
|
12
|
-
|
13
|
-
|
17
|
+
SELECT (
|
18
|
+
SELECT j
|
19
|
+
FROM que_jobs AS j
|
20
|
+
WHERE run_at <= now() AND (priority, run_at, job_id) > (job.priority, job.run_at, job.job_id)
|
21
|
+
ORDER BY priority, run_at, job_id
|
22
|
+
LIMIT 1
|
23
|
+
) AS j
|
24
|
+
FROM job
|
25
|
+
WHERE NOT job.locked
|
14
26
|
LIMIT 1
|
15
27
|
) AS t1
|
16
|
-
UNION ALL (
|
17
|
-
SELECT (job).*, pg_try_advisory_lock((job).job_id) AS locked
|
18
|
-
FROM (
|
19
|
-
SELECT (
|
20
|
-
SELECT job
|
21
|
-
FROM que_jobs AS job
|
22
|
-
WHERE run_at <= now() AND (priority, run_at, job_id) > (cte.priority, cte.run_at, cte.job_id)
|
23
|
-
ORDER BY priority, run_at, job_id
|
24
|
-
LIMIT 1
|
25
|
-
) AS job
|
26
|
-
FROM cte
|
27
|
-
WHERE NOT cte.locked
|
28
|
-
LIMIT 1
|
29
|
-
) AS t1
|
30
|
-
)
|
31
28
|
)
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
29
|
+
)
|
30
|
+
SELECT priority, run_at, job_id, job_class, args, error_count
|
31
|
+
FROM job
|
32
|
+
WHERE locked
|
33
|
+
LIMIT 1
|
34
|
+
}.freeze,
|
38
35
|
|
39
|
-
:check_job =>
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
SQL
|
47
|
-
).freeze,
|
36
|
+
:check_job => %{
|
37
|
+
SELECT 1 AS one
|
38
|
+
FROM que_jobs
|
39
|
+
WHERE priority = $1::integer
|
40
|
+
AND run_at = $2::timestamptz
|
41
|
+
AND job_id = $3::bigint
|
42
|
+
}.freeze,
|
48
43
|
|
49
|
-
:set_error =>
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
SQL
|
59
|
-
).freeze,
|
44
|
+
:set_error => %{
|
45
|
+
UPDATE que_jobs
|
46
|
+
SET error_count = $1::integer,
|
47
|
+
run_at = now() + $2::integer * '1 second'::interval,
|
48
|
+
last_error = $3::text
|
49
|
+
WHERE priority = $4::integer
|
50
|
+
AND run_at = $5::timestamptz
|
51
|
+
AND job_id = $6::bigint
|
52
|
+
}.freeze,
|
60
53
|
|
61
|
-
:
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
).freeze,
|
54
|
+
:insert_job => %{
|
55
|
+
INSERT INTO que_jobs
|
56
|
+
(priority, run_at, job_class, args)
|
57
|
+
VALUES
|
58
|
+
(coalesce($1, 1)::integer, coalesce($2, 'now')::timestamptz, $3::text, coalesce($4, '[]')::json)
|
59
|
+
RETURNING *
|
60
|
+
}.freeze,
|
69
61
|
|
70
|
-
:
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
job_id bigserial NOT NULL,
|
77
|
-
job_class text NOT NULL,
|
78
|
-
args json NOT NULL DEFAULT '[]'::json,
|
79
|
-
error_count integer NOT NULL DEFAULT 0,
|
80
|
-
last_error text,
|
62
|
+
:destroy_job => %{
|
63
|
+
DELETE FROM que_jobs
|
64
|
+
WHERE priority = $1::integer
|
65
|
+
AND run_at = $2::timestamptz
|
66
|
+
AND job_id = $3::bigint
|
67
|
+
}.freeze,
|
81
68
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
69
|
+
:job_stats => %{
|
70
|
+
SELECT job_class,
|
71
|
+
count(*) AS count,
|
72
|
+
count(locks.job_id) AS count_working,
|
73
|
+
sum((error_count > 0)::int) AS count_errored,
|
74
|
+
max(error_count) AS highest_error_count,
|
75
|
+
min(run_at) AS oldest_run_at
|
76
|
+
FROM que_jobs
|
77
|
+
LEFT JOIN (
|
78
|
+
SELECT (classid::bigint << 32) + objid::bigint AS job_id
|
79
|
+
FROM pg_locks
|
80
|
+
WHERE locktype = 'advisory'
|
81
|
+
) locks USING (job_id)
|
82
|
+
GROUP BY job_class
|
83
|
+
ORDER BY count(*) DESC
|
84
|
+
}.freeze,
|
85
|
+
|
86
|
+
:worker_states => %{
|
87
|
+
SELECT que_jobs.*,
|
88
|
+
pg.pid AS pg_backend_pid,
|
89
|
+
pg.state AS pg_state,
|
90
|
+
pg.state_change AS pg_state_changed_at,
|
91
|
+
pg.query AS pg_last_query,
|
92
|
+
pg.query_start AS pg_last_query_started_at,
|
93
|
+
pg.xact_start AS pg_transaction_started_at,
|
94
|
+
pg.waiting AS pg_waiting_on_lock
|
95
|
+
FROM que_jobs
|
96
|
+
JOIN (
|
97
|
+
SELECT (classid::bigint << 32) + objid::bigint AS job_id, pg_stat_activity.*
|
98
|
+
FROM pg_locks
|
99
|
+
JOIN pg_stat_activity USING (pid)
|
100
|
+
WHERE locktype = 'advisory'
|
101
|
+
) pg USING (job_id)
|
102
|
+
}.freeze,
|
103
|
+
|
104
|
+
:create_table => %{
|
105
|
+
CREATE TABLE que_jobs
|
106
|
+
(
|
107
|
+
priority integer NOT NULL DEFAULT 1,
|
108
|
+
run_at timestamptz NOT NULL DEFAULT now(),
|
109
|
+
job_id bigserial NOT NULL,
|
110
|
+
job_class text NOT NULL,
|
111
|
+
args json NOT NULL DEFAULT '[]'::json,
|
112
|
+
error_count integer NOT NULL DEFAULT 0,
|
113
|
+
last_error text,
|
114
|
+
|
115
|
+
CONSTRAINT que_jobs_pkey PRIMARY KEY (priority, run_at, job_id)
|
116
|
+
)
|
117
|
+
}.freeze
|
118
|
+
|
119
|
+
# Note: if schema changes to the que_jobs table become necessary later on,
|
120
|
+
# a simple versioning scheme would be:
|
121
|
+
|
122
|
+
# Set version:
|
123
|
+
# COMMENT ON TABLE que_jobs IS '2'
|
124
|
+
|
125
|
+
# Get version:
|
126
|
+
# SELECT description
|
127
|
+
# FROM pg_description
|
128
|
+
# JOIN pg_class
|
129
|
+
# ON pg_description.objoid = pg_class.oid
|
130
|
+
# WHERE relname = 'que_jobs'
|
86
131
|
}
|
87
132
|
end
|