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