que 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|