que 0.5.0 → 0.6.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/.gitignore +1 -1
- data/.travis.yml +1 -1
- data/CHANGELOG.md +21 -1
- data/Gemfile +5 -0
- data/README.md +7 -6
- data/docs/advanced_setup.md +14 -4
- data/docs/customizing_que.md +4 -4
- data/docs/error_handling.md +13 -1
- data/docs/managing_workers.md +2 -2
- data/docs/migrating.md +26 -0
- data/docs/multiple_queues.md +13 -0
- data/docs/shutting_down_safely.md +7 -0
- data/docs/writing_reliable_jobs.md +43 -0
- data/lib/generators/que/templates/add_que.rb +1 -1
- data/lib/que.rb +27 -41
- data/lib/que/adapters/base.rb +75 -4
- data/lib/que/job.rb +45 -28
- data/lib/que/migrations.rb +3 -2
- data/lib/que/migrations/{1-down.sql → 1/down.sql} +0 -0
- data/lib/que/migrations/{1-up.sql → 1/up.sql} +0 -0
- data/lib/que/migrations/{2-down.sql → 2/down.sql} +0 -0
- data/lib/que/migrations/{2-up.sql → 2/up.sql} +0 -0
- data/lib/que/migrations/3/down.sql +5 -0
- data/lib/que/migrations/3/up.sql +5 -0
- data/lib/que/sql.rb +24 -17
- data/lib/que/version.rb +1 -1
- data/lib/que/worker.rb +6 -5
- data/spec/adapters/active_record_spec.rb +6 -6
- data/spec/adapters/sequel_spec.rb +4 -4
- data/spec/gemfiles/Gemfile1 +18 -0
- data/spec/gemfiles/Gemfile2 +18 -0
- data/spec/support/helpers.rb +2 -1
- data/spec/support/shared_examples/adapter.rb +7 -3
- data/spec/support/shared_examples/multi_threaded_adapter.rb +2 -2
- data/spec/travis.rb +12 -4
- data/spec/unit/customization_spec.rb +148 -0
- data/spec/unit/{queue_spec.rb → enqueue_spec.rb} +115 -14
- data/spec/unit/logging_spec.rb +3 -2
- data/spec/unit/migrations_spec.rb +3 -2
- data/spec/unit/pool_spec.rb +30 -6
- data/spec/unit/run_spec.rb +12 -0
- data/spec/unit/states_spec.rb +29 -31
- data/spec/unit/stats_spec.rb +16 -14
- data/spec/unit/work_spec.rb +120 -25
- data/spec/unit/worker_spec.rb +55 -9
- data/tasks/safe_shutdown.rb +1 -1
- metadata +30 -17
data/lib/que/adapters/base.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'time' # For Time.parse.
|
2
|
+
|
1
3
|
module Que
|
2
4
|
module Adapters
|
3
5
|
autoload :ActiveRecord, 'que/adapters/active_record'
|
@@ -25,11 +27,35 @@ module Que
|
|
25
27
|
false
|
26
28
|
end
|
27
29
|
|
28
|
-
def execute(
|
30
|
+
def execute(command, params = [])
|
31
|
+
params = params.map do |param|
|
32
|
+
case param
|
33
|
+
# The pg gem unfortunately doesn't convert fractions of time instances, so cast them to a string.
|
34
|
+
when Time then param.strftime("%Y-%m-%d %H:%M:%S.%6N %z")
|
35
|
+
when Array, Hash then JSON_MODULE.dump(param)
|
36
|
+
else param
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
cast_result \
|
41
|
+
case command
|
42
|
+
when Symbol then execute_prepared(command, params)
|
43
|
+
when String then execute_sql(command, params)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def in_transaction?
|
48
|
+
checkout { |conn| conn.transaction_status != ::PG::PQTRANS_IDLE }
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def execute_sql(sql, params)
|
54
|
+
args = params.empty? ? [sql] : [sql, params]
|
29
55
|
checkout { |conn| conn.async_exec(*args) }
|
30
56
|
end
|
31
57
|
|
32
|
-
def execute_prepared(name, params
|
58
|
+
def execute_prepared(name, params)
|
33
59
|
checkout do |conn|
|
34
60
|
statements = @prepared_statements[conn] ||= {}
|
35
61
|
|
@@ -42,8 +68,53 @@ module Que
|
|
42
68
|
end
|
43
69
|
end
|
44
70
|
|
45
|
-
|
46
|
-
|
71
|
+
HASH_DEFAULT_PROC = proc { |hash, key| hash[key.to_s] if Symbol === key }
|
72
|
+
|
73
|
+
INDIFFERENTIATOR = proc do |object|
|
74
|
+
case object
|
75
|
+
when Array
|
76
|
+
object.each(&INDIFFERENTIATOR)
|
77
|
+
when Hash
|
78
|
+
object.default_proc = HASH_DEFAULT_PROC
|
79
|
+
object.each { |key, value| object[key] = INDIFFERENTIATOR.call(value) }
|
80
|
+
object
|
81
|
+
else
|
82
|
+
object
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
CAST_PROCS = {}
|
87
|
+
|
88
|
+
# Integer, bigint, smallint:
|
89
|
+
CAST_PROCS[23] = CAST_PROCS[20] = CAST_PROCS[21] = proc(&:to_i)
|
90
|
+
|
91
|
+
# Timestamp with time zone.
|
92
|
+
CAST_PROCS[1184] = Time.method(:parse)
|
93
|
+
|
94
|
+
# JSON.
|
95
|
+
CAST_PROCS[114] = JSON_MODULE.method(:load)
|
96
|
+
|
97
|
+
# Boolean:
|
98
|
+
CAST_PROCS[16] = 't'.method(:==)
|
99
|
+
|
100
|
+
def cast_result(result)
|
101
|
+
output = result.to_a
|
102
|
+
|
103
|
+
result.fields.each_with_index do |field, index|
|
104
|
+
if converter = CAST_PROCS[result.ftype(index)]
|
105
|
+
output.each do |hash|
|
106
|
+
unless (value = hash[field]).nil?
|
107
|
+
hash[field] = converter.call(value)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
if result.first.respond_to?(:with_indifferent_access)
|
114
|
+
output.map(&:with_indifferent_access)
|
115
|
+
else
|
116
|
+
output.each(&INDIFFERENTIATOR)
|
117
|
+
end
|
47
118
|
end
|
48
119
|
end
|
49
120
|
end
|
data/lib/que/job.rb
CHANGED
@@ -3,12 +3,11 @@ module Que
|
|
3
3
|
attr_reader :attrs
|
4
4
|
|
5
5
|
def initialize(attrs)
|
6
|
-
@attrs
|
7
|
-
@attrs[:args] = Que.indifferentiate JSON_MODULE.load(@attrs[:args])
|
6
|
+
@attrs = attrs
|
8
7
|
end
|
9
8
|
|
10
9
|
# Subclasses should define their own run methods, but keep an empty one
|
11
|
-
# here so that Que::Job.
|
10
|
+
# here so that Que::Job.enqueue can queue an empty job in testing.
|
12
11
|
def run(*args)
|
13
12
|
end
|
14
13
|
|
@@ -20,45 +19,61 @@ module Que
|
|
20
19
|
private
|
21
20
|
|
22
21
|
def destroy
|
23
|
-
Que.execute :destroy_job, attrs.values_at(:priority, :run_at, :job_id)
|
22
|
+
Que.execute :destroy_job, attrs.values_at(:queue, :priority, :run_at, :job_id)
|
24
23
|
@destroyed = true
|
25
24
|
end
|
26
25
|
|
26
|
+
@retry_interval = proc { |count| count ** 4 + 3 }
|
27
|
+
|
27
28
|
class << self
|
28
|
-
|
29
|
+
attr_reader :retry_interval
|
30
|
+
|
31
|
+
def enqueue(*args)
|
29
32
|
if args.last.is_a?(Hash)
|
30
|
-
options
|
31
|
-
|
32
|
-
|
33
|
+
options = args.pop
|
34
|
+
queue = options.delete(:queue) || '' if options.key?(:queue)
|
35
|
+
job_class = options.delete(:job_class)
|
36
|
+
run_at = options.delete(:run_at)
|
37
|
+
priority = options.delete(:priority)
|
33
38
|
args << options if options.any?
|
34
39
|
end
|
35
40
|
|
36
|
-
attrs = {:job_class => to_s, :args =>
|
41
|
+
attrs = {:job_class => job_class || to_s, :args => args}
|
42
|
+
|
43
|
+
if t = run_at || @run_at && @run_at.call || @default_run_at && @default_run_at.call
|
44
|
+
attrs[:run_at] = t
|
45
|
+
end
|
37
46
|
|
38
|
-
if
|
39
|
-
attrs[:
|
47
|
+
if p = priority || @priority || @default_priority
|
48
|
+
attrs[:priority] = p
|
40
49
|
end
|
41
50
|
|
42
|
-
if
|
43
|
-
attrs[:
|
51
|
+
if q = queue || @queue
|
52
|
+
attrs[:queue] = q
|
44
53
|
end
|
45
54
|
|
46
|
-
if Que.mode == :sync && !
|
47
|
-
|
55
|
+
if Que.mode == :sync && !t
|
56
|
+
run(*attrs[:args])
|
48
57
|
else
|
49
|
-
values = Que.execute(:insert_job, attrs.values_at(:priority, :run_at, :job_class, :args)).first
|
50
|
-
Que.adapter.wake_worker_after_commit unless
|
58
|
+
values = Que.execute(:insert_job, attrs.values_at(:queue, :priority, :run_at, :job_class, :args)).first
|
59
|
+
Que.adapter.wake_worker_after_commit unless t
|
51
60
|
new(values)
|
52
61
|
end
|
53
62
|
end
|
54
63
|
|
55
|
-
|
64
|
+
alias queue enqueue
|
65
|
+
|
66
|
+
def run(*args)
|
67
|
+
new(:args => args).tap(&:_run)
|
68
|
+
end
|
69
|
+
|
70
|
+
def work(queue = '')
|
56
71
|
# Since we're taking session-level advisory locks, we have to hold the
|
57
72
|
# same connection throughout the process of getting a job, working it,
|
58
73
|
# deleting it, and removing the lock.
|
59
74
|
Que.adapter.checkout do
|
60
75
|
begin
|
61
|
-
if job = Que.execute(:lock_job).first
|
76
|
+
if job = Que.execute(:lock_job, [queue]).first
|
62
77
|
# Edge case: It's possible for the lock_job query to have
|
63
78
|
# grabbed a job that's already been worked, if it took its MVCC
|
64
79
|
# snapshot while the job was processing, but didn't attempt the
|
@@ -69,10 +84,12 @@ module Que
|
|
69
84
|
# Note that there is currently no spec for this behavior, since
|
70
85
|
# I'm not sure how to reliably commit a transaction that deletes
|
71
86
|
# the job in a separate thread between lock_job and check_job.
|
72
|
-
if Que.execute(:check_job, job.values_at(:priority, :run_at, :job_id)).none?
|
87
|
+
if Que.execute(:check_job, job.values_at(:queue, :priority, :run_at, :job_id)).none?
|
73
88
|
{:event => :job_race_condition}
|
74
89
|
else
|
75
|
-
|
90
|
+
klass = class_for(job[:job_class])
|
91
|
+
klass.new(job)._run
|
92
|
+
{:event => :job_worked, :job => job}
|
76
93
|
end
|
77
94
|
else
|
78
95
|
{:event => :job_unavailable}
|
@@ -80,11 +97,11 @@ module Que
|
|
80
97
|
rescue => error
|
81
98
|
begin
|
82
99
|
if job
|
83
|
-
|
84
|
-
|
85
|
-
delay
|
86
|
-
message
|
87
|
-
Que.execute :set_error, [count, delay, message] + job.values_at(:priority, :run_at, :job_id)
|
100
|
+
count = job[:error_count].to_i + 1
|
101
|
+
interval = klass && klass.retry_interval || retry_interval
|
102
|
+
delay = interval.respond_to?(:call) ? interval.call(count) : interval
|
103
|
+
message = "#{error.message}\n#{error.backtrace.join("\n")}"
|
104
|
+
Que.execute :set_error, [count, delay, message] + job.values_at(:queue, :priority, :run_at, :job_id)
|
88
105
|
end
|
89
106
|
rescue
|
90
107
|
# If we can't reach the database for some reason, too bad, but
|
@@ -111,8 +128,8 @@ module Que
|
|
111
128
|
|
112
129
|
private
|
113
130
|
|
114
|
-
def
|
115
|
-
|
131
|
+
def class_for(string)
|
132
|
+
string.split('::').inject(Object, &:const_get)
|
116
133
|
end
|
117
134
|
end
|
118
135
|
end
|
data/lib/que/migrations.rb
CHANGED
@@ -3,7 +3,7 @@ module Que
|
|
3
3
|
# In order to ship a schema change, add the relevant up and down sql files
|
4
4
|
# to the migrations directory, and bump the version both here and in the
|
5
5
|
# add_que generator template.
|
6
|
-
CURRENT_VERSION =
|
6
|
+
CURRENT_VERSION = 3
|
7
7
|
|
8
8
|
class << self
|
9
9
|
def migrate!(options = {:version => CURRENT_VERSION})
|
@@ -21,7 +21,8 @@ module Que
|
|
21
21
|
end
|
22
22
|
|
23
23
|
steps.each do |step|
|
24
|
-
|
24
|
+
sql = File.read("#{File.dirname(__FILE__)}/migrations/#{step}/#{direction}.sql")
|
25
|
+
Que.execute(sql)
|
25
26
|
end
|
26
27
|
|
27
28
|
set_db_version(version)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
data/lib/que/sql.rb
CHANGED
@@ -7,7 +7,8 @@ module Que
|
|
7
7
|
FROM (
|
8
8
|
SELECT j
|
9
9
|
FROM que_jobs AS j
|
10
|
-
WHERE
|
10
|
+
WHERE queue = $1::text
|
11
|
+
AND run_at <= now()
|
11
12
|
ORDER BY priority, run_at, job_id
|
12
13
|
LIMIT 1
|
13
14
|
) AS t1
|
@@ -17,7 +18,9 @@ module Que
|
|
17
18
|
SELECT (
|
18
19
|
SELECT j
|
19
20
|
FROM que_jobs AS j
|
20
|
-
WHERE
|
21
|
+
WHERE queue = $1::text
|
22
|
+
AND run_at <= now()
|
23
|
+
AND (priority, run_at, job_id) > (job.priority, job.run_at, job.job_id)
|
21
24
|
ORDER BY priority, run_at, job_id
|
22
25
|
LIMIT 1
|
23
26
|
) AS j
|
@@ -27,7 +30,7 @@ module Que
|
|
27
30
|
) AS t1
|
28
31
|
)
|
29
32
|
)
|
30
|
-
SELECT priority, run_at, job_id, job_class, args, error_count
|
33
|
+
SELECT queue, priority, run_at, job_id, job_class, args, error_count
|
31
34
|
FROM job
|
32
35
|
WHERE locked
|
33
36
|
LIMIT 1
|
@@ -36,9 +39,10 @@ module Que
|
|
36
39
|
:check_job => %{
|
37
40
|
SELECT 1 AS one
|
38
41
|
FROM que_jobs
|
39
|
-
WHERE
|
40
|
-
AND
|
41
|
-
AND
|
42
|
+
WHERE queue = $1::text
|
43
|
+
AND priority = $2::smallint
|
44
|
+
AND run_at = $3::timestamptz
|
45
|
+
AND job_id = $4::bigint
|
42
46
|
}.freeze,
|
43
47
|
|
44
48
|
:set_error => %{
|
@@ -46,28 +50,31 @@ module Que
|
|
46
50
|
SET error_count = $1::integer,
|
47
51
|
run_at = now() + $2::integer * '1 second'::interval,
|
48
52
|
last_error = $3::text
|
49
|
-
WHERE
|
50
|
-
AND
|
51
|
-
AND
|
53
|
+
WHERE queue = $4::text
|
54
|
+
AND priority = $5::smallint
|
55
|
+
AND run_at = $6::timestamptz
|
56
|
+
AND job_id = $7::bigint
|
52
57
|
}.freeze,
|
53
58
|
|
54
59
|
:insert_job => %{
|
55
60
|
INSERT INTO que_jobs
|
56
|
-
(priority, run_at, job_class, args)
|
61
|
+
(queue, priority, run_at, job_class, args)
|
57
62
|
VALUES
|
58
|
-
(coalesce($1,
|
63
|
+
(coalesce($1, '')::text, coalesce($2, 100)::smallint, coalesce($3, 'now')::timestamptz, $4::text, coalesce($5, '[]')::json)
|
59
64
|
RETURNING *
|
60
65
|
}.freeze,
|
61
66
|
|
62
67
|
:destroy_job => %{
|
63
68
|
DELETE FROM que_jobs
|
64
|
-
WHERE
|
65
|
-
AND
|
66
|
-
AND
|
69
|
+
WHERE queue = $1::text
|
70
|
+
AND priority = $2::smallint
|
71
|
+
AND run_at = $3::timestamptz
|
72
|
+
AND job_id = $4::bigint
|
67
73
|
}.freeze,
|
68
74
|
|
69
75
|
:job_stats => %{
|
70
|
-
SELECT
|
76
|
+
SELECT queue,
|
77
|
+
job_class,
|
71
78
|
count(*) AS count,
|
72
79
|
count(locks.job_id) AS count_working,
|
73
80
|
sum((error_count > 0)::int) AS count_errored,
|
@@ -79,7 +86,7 @@ module Que
|
|
79
86
|
FROM pg_locks
|
80
87
|
WHERE locktype = 'advisory'
|
81
88
|
) locks USING (job_id)
|
82
|
-
GROUP BY job_class
|
89
|
+
GROUP BY queue, job_class
|
83
90
|
ORDER BY count(*) DESC
|
84
91
|
}.freeze,
|
85
92
|
|
@@ -100,5 +107,5 @@ module Que
|
|
100
107
|
WHERE locktype = 'advisory'
|
101
108
|
) pg USING (job_id)
|
102
109
|
}.freeze
|
103
|
-
}
|
110
|
+
}.freeze
|
104
111
|
end
|
data/lib/que/version.rb
CHANGED
data/lib/que/worker.rb
CHANGED
@@ -8,10 +8,11 @@ module Que
|
|
8
8
|
# synchronize access to it.
|
9
9
|
include MonitorMixin
|
10
10
|
|
11
|
-
attr_reader :thread, :state
|
11
|
+
attr_reader :thread, :state, :queue
|
12
12
|
|
13
|
-
def initialize
|
14
|
-
super # For MonitorMixin.
|
13
|
+
def initialize(queue = '')
|
14
|
+
super() # For MonitorMixin.
|
15
|
+
@queue = queue
|
15
16
|
@state = :working
|
16
17
|
@thread = Thread.new { work_loop }
|
17
18
|
end
|
@@ -71,7 +72,7 @@ module Que
|
|
71
72
|
loop do
|
72
73
|
time = Time.now
|
73
74
|
cycle = nil
|
74
|
-
result = Job.work
|
75
|
+
result = Job.work(queue)
|
75
76
|
|
76
77
|
case result[:event]
|
77
78
|
when :job_unavailable
|
@@ -167,7 +168,7 @@ module Que
|
|
167
168
|
Que.log :event => 'worker_count_change', :value => count.to_s
|
168
169
|
|
169
170
|
if count > worker_count
|
170
|
-
workers.push *(count - worker_count).times.map{new}
|
171
|
+
workers.push *(count - worker_count).times.map{new(ENV['QUE_QUEUE'] || '')}
|
171
172
|
elsif count < worker_count
|
172
173
|
workers.pop(worker_count - count).each(&:stop).each(&:wait_until_stopped)
|
173
174
|
end
|
@@ -22,7 +22,7 @@ unless defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
|
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
25
|
-
ActiveRecordJob.
|
25
|
+
ActiveRecordJob.enqueue
|
26
26
|
Que::Job.work
|
27
27
|
|
28
28
|
$pid1.should == $pid2
|
@@ -32,7 +32,7 @@ unless defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
|
|
32
32
|
end
|
33
33
|
|
34
34
|
it "should instantiate args as ActiveSupport::HashWithIndifferentAccess" do
|
35
|
-
ArgsJob.
|
35
|
+
ArgsJob.enqueue :param => 2
|
36
36
|
Que::Job.work
|
37
37
|
$passed_args.first[:param].should == 2
|
38
38
|
$passed_args.first.should be_an_instance_of ActiveSupport::HashWithIndifferentAccess
|
@@ -42,7 +42,7 @@ unless defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
|
|
42
42
|
Que.mode = :async
|
43
43
|
sleep_until { Que::Worker.workers.all? &:sleeping? }
|
44
44
|
|
45
|
-
Que::Job.
|
45
|
+
Que::Job.enqueue :run_at => 1.minute.ago
|
46
46
|
DB[:que_jobs].get(:run_at).should be_within(3).of Time.now - 60
|
47
47
|
|
48
48
|
Que.wake_interval = 0.005.seconds
|
@@ -54,17 +54,17 @@ unless defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
|
|
54
54
|
sleep_until { Que::Worker.workers.all? &:sleeping? }
|
55
55
|
|
56
56
|
# Wakes a worker immediately when not in a transaction.
|
57
|
-
Que::Job.
|
57
|
+
Que::Job.enqueue
|
58
58
|
sleep_until { Que::Worker.workers.all?(&:sleeping?) && DB[:que_jobs].empty? }
|
59
59
|
|
60
60
|
ActiveRecord::Base.transaction do
|
61
|
-
Que::Job.
|
61
|
+
Que::Job.enqueue
|
62
62
|
Que::Worker.workers.each { |worker| worker.should be_sleeping }
|
63
63
|
end
|
64
64
|
sleep_until { Que::Worker.workers.all?(&:sleeping?) && DB[:que_jobs].empty? }
|
65
65
|
|
66
66
|
# Do nothing when queueing with a specific :run_at.
|
67
|
-
BlockJob.
|
67
|
+
BlockJob.enqueue :run_at => Time.now
|
68
68
|
Que::Worker.workers.each { |worker| worker.should be_sleeping }
|
69
69
|
end
|
70
70
|
|