que 1.0.0.beta4 → 1.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/.github/workflows/{ruby.yml → tests.yml} +8 -4
- data/CHANGELOG.md +409 -293
- data/Dockerfile +20 -0
- data/README.md +129 -28
- data/auto/dev +21 -0
- data/auto/pre-push-hook +30 -0
- data/auto/psql +9 -0
- data/auto/test +5 -0
- data/auto/test-postgres-14 +17 -0
- data/bin/command_line_interface.rb +14 -6
- data/docker-compose.yml +46 -0
- data/docs/README.md +17 -18
- data/lib/que/active_record/model.rb +1 -1
- data/lib/que/connection.rb +4 -0
- data/lib/que/job.rb +35 -17
- data/lib/que/job_buffer.rb +28 -28
- data/lib/que/locker.rb +24 -12
- data/lib/que/migrations/4/up.sql +3 -1
- data/lib/que/migrations/5/down.sql +73 -0
- data/lib/que/migrations/5/up.sql +76 -0
- data/lib/que/migrations.rb +1 -2
- data/lib/que/poller.rb +10 -3
- data/lib/que/version.rb +5 -1
- data/lib/que/worker.rb +12 -4
- data/lib/que.rb +2 -1
- data/scripts/docker-entrypoint +14 -0
- data/scripts/test +5 -0
- metadata +20 -10
- data/CHANGELOG.1.0.beta.md +0 -137
data/lib/que/job.rb
CHANGED
@@ -12,7 +12,7 @@ module Que
|
|
12
12
|
SQL[:insert_job] =
|
13
13
|
%{
|
14
14
|
INSERT INTO public.que_jobs
|
15
|
-
(queue, priority, run_at, job_class, args, data)
|
15
|
+
(queue, priority, run_at, job_class, args, data, job_schema_version)
|
16
16
|
VALUES
|
17
17
|
(
|
18
18
|
coalesce($1, 'default')::text,
|
@@ -20,7 +20,8 @@ module Que
|
|
20
20
|
coalesce($3, now())::timestamptz,
|
21
21
|
$4::text,
|
22
22
|
coalesce($5, '[]')::jsonb,
|
23
|
-
coalesce($6, '{}')::jsonb
|
23
|
+
coalesce($6, '{}')::jsonb,
|
24
|
+
#{Que.job_schema_version}
|
24
25
|
)
|
25
26
|
RETURNING *
|
26
27
|
}
|
@@ -57,22 +58,18 @@ module Que
|
|
57
58
|
|
58
59
|
def enqueue(
|
59
60
|
*args,
|
60
|
-
|
61
|
-
priority: nil,
|
62
|
-
run_at: nil,
|
63
|
-
job_class: nil,
|
64
|
-
tags: nil,
|
61
|
+
job_options: {},
|
65
62
|
**arg_opts
|
66
63
|
)
|
67
|
-
|
64
|
+
arg_opts, job_options = _extract_job_options(arg_opts, job_options.dup)
|
68
65
|
args << arg_opts if arg_opts.any?
|
69
66
|
|
70
|
-
if tags
|
71
|
-
if tags.length > MAXIMUM_TAGS_COUNT
|
72
|
-
raise Que::Error, "Can't enqueue a job with more than #{MAXIMUM_TAGS_COUNT} tags! (passed #{tags.length})"
|
67
|
+
if job_options[:tags]
|
68
|
+
if job_options[:tags].length > MAXIMUM_TAGS_COUNT
|
69
|
+
raise Que::Error, "Can't enqueue a job with more than #{MAXIMUM_TAGS_COUNT} tags! (passed #{job_options[:tags].length})"
|
73
70
|
end
|
74
71
|
|
75
|
-
tags.each do |tag|
|
72
|
+
job_options[:tags].each do |tag|
|
76
73
|
if tag.length > MAXIMUM_TAG_LENGTH
|
77
74
|
raise Que::Error, "Can't enqueue a job with a tag longer than 100 characters! (\"#{tag}\")"
|
78
75
|
end
|
@@ -80,13 +77,13 @@ module Que
|
|
80
77
|
end
|
81
78
|
|
82
79
|
attrs = {
|
83
|
-
queue: queue || resolve_que_setting(:queue) || Que.default_queue,
|
84
|
-
priority: priority || resolve_que_setting(:priority),
|
85
|
-
run_at: run_at || resolve_que_setting(:run_at),
|
80
|
+
queue: job_options[:queue] || resolve_que_setting(:queue) || Que.default_queue,
|
81
|
+
priority: job_options[:priority] || resolve_que_setting(:priority),
|
82
|
+
run_at: job_options[:run_at] || resolve_que_setting(:run_at),
|
86
83
|
args: Que.serialize_json(args),
|
87
|
-
data: tags ? Que.serialize_json(tags: tags) : "{}",
|
84
|
+
data: job_options[:tags] ? Que.serialize_json(tags: job_options[:tags]) : "{}",
|
88
85
|
job_class: \
|
89
|
-
job_class || name ||
|
86
|
+
job_options[:job_class] || name ||
|
90
87
|
raise(Error, "Can't enqueue an anonymous subclass of Que::Job"),
|
91
88
|
}
|
92
89
|
|
@@ -139,6 +136,27 @@ module Que
|
|
139
136
|
end
|
140
137
|
end
|
141
138
|
end
|
139
|
+
|
140
|
+
def _extract_job_options(arg_opts, job_options)
|
141
|
+
deprecated_job_option_names = []
|
142
|
+
|
143
|
+
%i[queue priority run_at job_class tags].each do |option_name|
|
144
|
+
next unless arg_opts.key?(option_name) && job_options[option_name].nil?
|
145
|
+
|
146
|
+
job_options[option_name] = arg_opts.delete(option_name)
|
147
|
+
deprecated_job_option_names << option_name
|
148
|
+
end
|
149
|
+
|
150
|
+
_log_job_options_deprecation(deprecated_job_option_names)
|
151
|
+
|
152
|
+
[arg_opts, job_options]
|
153
|
+
end
|
154
|
+
|
155
|
+
def _log_job_options_deprecation(deprecated_job_option_names)
|
156
|
+
return unless deprecated_job_option_names.any?
|
157
|
+
|
158
|
+
warn "Passing job options like (#{deprecated_job_option_names.join(', ')}) to `JobClass.enqueue` as top level keyword args has been deprecated and will be removed in version 2.0. Please wrap job options in an explicit `job_options` keyword arg instead."
|
159
|
+
end
|
142
160
|
end
|
143
161
|
|
144
162
|
# Set up some defaults.
|
data/lib/que/job_buffer.rb
CHANGED
@@ -6,7 +6,7 @@
|
|
6
6
|
|
7
7
|
module Que
|
8
8
|
class JobBuffer
|
9
|
-
attr_reader :maximum_size, :
|
9
|
+
attr_reader :maximum_size, :priority_queues
|
10
10
|
|
11
11
|
# Since we use a mutex, which is not reentrant, we have to be a little
|
12
12
|
# careful to not call a method that locks the mutex when we've already
|
@@ -17,20 +17,11 @@ module Que
|
|
17
17
|
|
18
18
|
def initialize(
|
19
19
|
maximum_size:,
|
20
|
-
minimum_size:,
|
21
20
|
priorities:
|
22
21
|
)
|
23
22
|
@maximum_size = Que.assert(Integer, maximum_size)
|
24
23
|
Que.assert(maximum_size >= 0) { "maximum_size for a JobBuffer must be at least zero!" }
|
25
24
|
|
26
|
-
@minimum_size = Que.assert(Integer, minimum_size)
|
27
|
-
Que.assert(minimum_size >= 0) { "minimum_size for a JobBuffer must be at least zero!" }
|
28
|
-
|
29
|
-
Que.assert(minimum_size <= maximum_size) do
|
30
|
-
"minimum buffer size (#{minimum_size}) is " \
|
31
|
-
"greater than the maximum buffer size (#{maximum_size})!"
|
32
|
-
end
|
33
|
-
|
34
25
|
@stop = false
|
35
26
|
@array = []
|
36
27
|
@mutex = Mutex.new
|
@@ -59,10 +50,8 @@ module Que
|
|
59
50
|
|
60
51
|
# Relying on the hash's contents being sorted, here.
|
61
52
|
priority_queues.reverse_each do |_, pq|
|
62
|
-
pq.
|
63
|
-
|
64
|
-
break if job.nil? # False would mean we're stopping.
|
65
|
-
pq.push(job)
|
53
|
+
pq.populate do
|
54
|
+
_shift_job(pq.priority)
|
66
55
|
end
|
67
56
|
end
|
68
57
|
|
@@ -75,7 +64,7 @@ module Que
|
|
75
64
|
|
76
65
|
def shift(priority = nil)
|
77
66
|
queue = priority_queues.fetch(priority) { raise Error, "not a permitted priority! #{priority}" }
|
78
|
-
queue.pop
|
67
|
+
queue.pop || shift_job(priority)
|
79
68
|
end
|
80
69
|
|
81
70
|
def shift_job(priority = nil)
|
@@ -158,6 +147,10 @@ module Que
|
|
158
147
|
sync { _stopping? }
|
159
148
|
end
|
160
149
|
|
150
|
+
def job_available?(priority)
|
151
|
+
(job = @array.first) && job.priority_sufficient?(priority)
|
152
|
+
end
|
153
|
+
|
161
154
|
private
|
162
155
|
|
163
156
|
def _buffer_space
|
@@ -210,15 +203,14 @@ module Que
|
|
210
203
|
def pop
|
211
204
|
sync do
|
212
205
|
loop do
|
213
|
-
|
214
|
-
|
215
|
-
|
206
|
+
if @stopping
|
207
|
+
return false
|
208
|
+
elsif item = @items.pop
|
216
209
|
return item
|
210
|
+
elsif job_buffer.job_available?(priority)
|
211
|
+
return false
|
217
212
|
end
|
218
213
|
|
219
|
-
job = job_buffer.shift_job(priority)
|
220
|
-
return job unless job.nil? # False means we're stopping.
|
221
|
-
|
222
214
|
@waiting += 1
|
223
215
|
@cv.wait(mutex)
|
224
216
|
@waiting -= 1
|
@@ -226,18 +218,20 @@ module Que
|
|
226
218
|
end
|
227
219
|
end
|
228
220
|
|
229
|
-
def
|
221
|
+
def stop
|
230
222
|
sync do
|
231
|
-
|
232
|
-
@
|
233
|
-
@cv.signal
|
223
|
+
@stopping = true
|
224
|
+
@cv.broadcast
|
234
225
|
end
|
235
226
|
end
|
236
227
|
|
237
|
-
def
|
228
|
+
def populate
|
238
229
|
sync do
|
239
|
-
|
240
|
-
|
230
|
+
waiting_count.times do
|
231
|
+
job = yield
|
232
|
+
break if job.nil? # False would mean we're stopping.
|
233
|
+
_push(job)
|
234
|
+
end
|
241
235
|
end
|
242
236
|
end
|
243
237
|
|
@@ -250,6 +244,12 @@ module Que
|
|
250
244
|
def sync(&block)
|
251
245
|
mutex.synchronize(&block)
|
252
246
|
end
|
247
|
+
|
248
|
+
def _push(item)
|
249
|
+
Que.assert(waiting_count > 0)
|
250
|
+
@items << item
|
251
|
+
@cv.signal
|
252
|
+
end
|
253
253
|
end
|
254
254
|
end
|
255
255
|
end
|
data/lib/que/locker.rb
CHANGED
@@ -24,12 +24,12 @@ module Que
|
|
24
24
|
|
25
25
|
SQL[:register_locker] =
|
26
26
|
%{
|
27
|
-
INSERT INTO public.que_lockers (pid, worker_count, worker_priorities, ruby_pid, ruby_hostname, listening, queues)
|
28
|
-
VALUES (pg_backend_pid(), $1::integer, $2::integer[], $3::integer, $4::text, $5::boolean, $6::text[])
|
27
|
+
INSERT INTO public.que_lockers (pid, worker_count, worker_priorities, ruby_pid, ruby_hostname, listening, queues, job_schema_version)
|
28
|
+
VALUES (pg_backend_pid(), $1::integer, $2::integer[], $3::integer, $4::text, $5::boolean, $6::text[], $7::integer)
|
29
29
|
}
|
30
30
|
|
31
31
|
class Locker
|
32
|
-
attr_reader :thread, :workers, :job_buffer, :locks
|
32
|
+
attr_reader :thread, :workers, :job_buffer, :locks, :queues, :poll_interval
|
33
33
|
|
34
34
|
MESSAGE_RESOLVERS = {}
|
35
35
|
RESULT_RESOLVERS = {}
|
@@ -45,7 +45,6 @@ module Que
|
|
45
45
|
|
46
46
|
DEFAULT_POLL_INTERVAL = 5.0
|
47
47
|
DEFAULT_WAIT_PERIOD = 50
|
48
|
-
DEFAULT_MINIMUM_BUFFER_SIZE = 2
|
49
48
|
DEFAULT_MAXIMUM_BUFFER_SIZE = 8
|
50
49
|
DEFAULT_WORKER_PRIORITIES = [10, 30, 50, nil, nil, nil].freeze
|
51
50
|
|
@@ -57,7 +56,6 @@ module Que
|
|
57
56
|
poll_interval: DEFAULT_POLL_INTERVAL,
|
58
57
|
wait_period: DEFAULT_WAIT_PERIOD,
|
59
58
|
maximum_buffer_size: DEFAULT_MAXIMUM_BUFFER_SIZE,
|
60
|
-
minimum_buffer_size: DEFAULT_MINIMUM_BUFFER_SIZE,
|
61
59
|
worker_priorities: DEFAULT_WORKER_PRIORITIES,
|
62
60
|
on_worker_start: nil
|
63
61
|
)
|
@@ -77,7 +75,6 @@ module Que
|
|
77
75
|
# ResultQueue to receive messages from workers.
|
78
76
|
@job_buffer = JobBuffer.new(
|
79
77
|
maximum_size: maximum_buffer_size,
|
80
|
-
minimum_size: minimum_buffer_size,
|
81
78
|
priorities: worker_priorities.uniq,
|
82
79
|
)
|
83
80
|
|
@@ -93,7 +90,6 @@ module Que
|
|
93
90
|
poll_interval: poll_interval,
|
94
91
|
wait_period: wait_period,
|
95
92
|
maximum_buffer_size: maximum_buffer_size,
|
96
|
-
minimum_buffer_size: minimum_buffer_size,
|
97
93
|
worker_priorities: worker_priorities,
|
98
94
|
}
|
99
95
|
end
|
@@ -101,7 +97,20 @@ module Que
|
|
101
97
|
# Local cache of which advisory locks are held by this connection.
|
102
98
|
@locks = Set.new
|
103
99
|
|
104
|
-
@
|
100
|
+
@poll_interval = poll_interval
|
101
|
+
|
102
|
+
if queues.is_a?(Hash)
|
103
|
+
@queue_names = queues.keys
|
104
|
+
@queues = queues.transform_values do |interval|
|
105
|
+
interval || poll_interval
|
106
|
+
end
|
107
|
+
else
|
108
|
+
@queue_names = queues
|
109
|
+
@queues = queues.map do |queue_name|
|
110
|
+
[queue_name, poll_interval]
|
111
|
+
end.to_h
|
112
|
+
end
|
113
|
+
|
105
114
|
@wait_period = wait_period.to_f / 1000 # Milliseconds to seconds.
|
106
115
|
|
107
116
|
@workers =
|
@@ -183,11 +192,11 @@ module Que
|
|
183
192
|
|
184
193
|
@pollers =
|
185
194
|
if poll
|
186
|
-
queues.map do |
|
195
|
+
@queues.map do |queue_name, interval|
|
187
196
|
Poller.new(
|
188
197
|
connection: @connection,
|
189
|
-
queue:
|
190
|
-
poll_interval: interval
|
198
|
+
queue: queue_name,
|
199
|
+
poll_interval: interval,
|
191
200
|
)
|
192
201
|
end
|
193
202
|
end
|
@@ -266,6 +275,7 @@ module Que
|
|
266
275
|
CURRENT_HOSTNAME,
|
267
276
|
!!@listener,
|
268
277
|
"{\"#{@queue_names.join('","')}\"}",
|
278
|
+
Que.job_schema_version,
|
269
279
|
]
|
270
280
|
end
|
271
281
|
|
@@ -393,10 +403,12 @@ module Que
|
|
393
403
|
}
|
394
404
|
end
|
395
405
|
|
406
|
+
materalize_cte = connection.server_version >= 12_00_00
|
407
|
+
|
396
408
|
jobs =
|
397
409
|
connection.execute \
|
398
410
|
<<-SQL
|
399
|
-
WITH jobs AS (SELECT * FROM que_jobs WHERE id IN (#{ids.join(', ')}))
|
411
|
+
WITH jobs AS #{materalize_cte ? 'MATERIALIZED' : ''} (SELECT * FROM que_jobs WHERE id IN (#{ids.join(', ')}))
|
400
412
|
SELECT * FROM jobs WHERE pg_try_advisory_lock(id)
|
401
413
|
SQL
|
402
414
|
|
data/lib/que/migrations/4/up.sql
CHANGED
@@ -146,7 +146,9 @@ CREATE FUNCTION que_job_notify() RETURNS trigger AS $$
|
|
146
146
|
FROM (
|
147
147
|
SELECT *
|
148
148
|
FROM public.que_lockers ql, generate_series(1, ql.worker_count) AS id
|
149
|
-
WHERE
|
149
|
+
WHERE
|
150
|
+
listening AND
|
151
|
+
queues @> ARRAY[NEW.queue]
|
150
152
|
ORDER BY md5(pid::text || id::text)
|
151
153
|
) t1
|
152
154
|
) t2
|
@@ -0,0 +1,73 @@
|
|
1
|
+
DROP TRIGGER que_job_notify ON que_jobs;
|
2
|
+
DROP FUNCTION que_job_notify();
|
3
|
+
|
4
|
+
DROP INDEX que_poll_idx_with_job_schema_version;
|
5
|
+
|
6
|
+
ALTER TABLE que_jobs
|
7
|
+
DROP COLUMN job_schema_version;
|
8
|
+
|
9
|
+
ALTER TABLE que_lockers
|
10
|
+
DROP COLUMN job_schema_version;
|
11
|
+
|
12
|
+
CREATE FUNCTION que_job_notify() RETURNS trigger AS $$
|
13
|
+
DECLARE
|
14
|
+
locker_pid integer;
|
15
|
+
sort_key json;
|
16
|
+
BEGIN
|
17
|
+
-- Don't do anything if the job is scheduled for a future time.
|
18
|
+
IF NEW.run_at IS NOT NULL AND NEW.run_at > now() THEN
|
19
|
+
RETURN null;
|
20
|
+
END IF;
|
21
|
+
|
22
|
+
-- Pick a locker to notify of the job's insertion, weighted by their number
|
23
|
+
-- of workers. Should bounce pseudorandomly between lockers on each
|
24
|
+
-- invocation, hence the md5-ordering, but still touch each one equally,
|
25
|
+
-- hence the modulo using the job_id.
|
26
|
+
SELECT pid
|
27
|
+
INTO locker_pid
|
28
|
+
FROM (
|
29
|
+
SELECT *, last_value(row_number) OVER () + 1 AS count
|
30
|
+
FROM (
|
31
|
+
SELECT *, row_number() OVER () - 1 AS row_number
|
32
|
+
FROM (
|
33
|
+
SELECT *
|
34
|
+
FROM public.que_lockers ql, generate_series(1, ql.worker_count) AS id
|
35
|
+
WHERE
|
36
|
+
listening AND
|
37
|
+
queues @> ARRAY[NEW.queue]
|
38
|
+
ORDER BY md5(pid::text || id::text)
|
39
|
+
) t1
|
40
|
+
) t2
|
41
|
+
) t3
|
42
|
+
WHERE NEW.id % count = row_number;
|
43
|
+
|
44
|
+
IF locker_pid IS NOT NULL THEN
|
45
|
+
-- There's a size limit to what can be broadcast via LISTEN/NOTIFY, so
|
46
|
+
-- rather than throw errors when someone enqueues a big job, just
|
47
|
+
-- broadcast the most pertinent information, and let the locker query for
|
48
|
+
-- the record after it's taken the lock. The worker will have to hit the
|
49
|
+
-- DB in order to make sure the job is still visible anyway.
|
50
|
+
SELECT row_to_json(t)
|
51
|
+
INTO sort_key
|
52
|
+
FROM (
|
53
|
+
SELECT
|
54
|
+
'job_available' AS message_type,
|
55
|
+
NEW.queue AS queue,
|
56
|
+
NEW.priority AS priority,
|
57
|
+
NEW.id AS id,
|
58
|
+
-- Make sure we output timestamps as UTC ISO 8601
|
59
|
+
to_char(NEW.run_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"') AS run_at
|
60
|
+
) t;
|
61
|
+
|
62
|
+
PERFORM pg_notify('que_listener_' || locker_pid::text, sort_key::text);
|
63
|
+
END IF;
|
64
|
+
|
65
|
+
RETURN null;
|
66
|
+
END
|
67
|
+
$$
|
68
|
+
LANGUAGE plpgsql;
|
69
|
+
|
70
|
+
CREATE TRIGGER que_job_notify
|
71
|
+
AFTER INSERT ON que_jobs
|
72
|
+
FOR EACH ROW
|
73
|
+
EXECUTE PROCEDURE public.que_job_notify();
|
@@ -0,0 +1,76 @@
|
|
1
|
+
DROP TRIGGER que_job_notify ON que_jobs;
|
2
|
+
DROP FUNCTION que_job_notify();
|
3
|
+
|
4
|
+
ALTER TABLE que_jobs
|
5
|
+
ADD COLUMN job_schema_version INTEGER DEFAULT 1;
|
6
|
+
|
7
|
+
ALTER TABLE que_lockers
|
8
|
+
ADD COLUMN job_schema_version INTEGER DEFAULT 1;
|
9
|
+
|
10
|
+
CREATE INDEX que_poll_idx_with_job_schema_version
|
11
|
+
ON que_jobs (job_schema_version, queue, priority, run_at, id)
|
12
|
+
WHERE (finished_at IS NULL AND expired_at IS NULL);
|
13
|
+
|
14
|
+
CREATE FUNCTION que_job_notify() RETURNS trigger AS $$
|
15
|
+
DECLARE
|
16
|
+
locker_pid integer;
|
17
|
+
sort_key json;
|
18
|
+
BEGIN
|
19
|
+
-- Don't do anything if the job is scheduled for a future time.
|
20
|
+
IF NEW.run_at IS NOT NULL AND NEW.run_at > now() THEN
|
21
|
+
RETURN null;
|
22
|
+
END IF;
|
23
|
+
|
24
|
+
-- Pick a locker to notify of the job's insertion, weighted by their number
|
25
|
+
-- of workers. Should bounce pseudorandomly between lockers on each
|
26
|
+
-- invocation, hence the md5-ordering, but still touch each one equally,
|
27
|
+
-- hence the modulo using the job_id.
|
28
|
+
SELECT pid
|
29
|
+
INTO locker_pid
|
30
|
+
FROM (
|
31
|
+
SELECT *, last_value(row_number) OVER () + 1 AS count
|
32
|
+
FROM (
|
33
|
+
SELECT *, row_number() OVER () - 1 AS row_number
|
34
|
+
FROM (
|
35
|
+
SELECT *
|
36
|
+
FROM public.que_lockers ql, generate_series(1, ql.worker_count) AS id
|
37
|
+
WHERE
|
38
|
+
listening AND
|
39
|
+
queues @> ARRAY[NEW.queue] AND
|
40
|
+
ql.job_schema_version = NEW.job_schema_version
|
41
|
+
ORDER BY md5(pid::text || id::text)
|
42
|
+
) t1
|
43
|
+
) t2
|
44
|
+
) t3
|
45
|
+
WHERE NEW.id % count = row_number;
|
46
|
+
|
47
|
+
IF locker_pid IS NOT NULL THEN
|
48
|
+
-- There's a size limit to what can be broadcast via LISTEN/NOTIFY, so
|
49
|
+
-- rather than throw errors when someone enqueues a big job, just
|
50
|
+
-- broadcast the most pertinent information, and let the locker query for
|
51
|
+
-- the record after it's taken the lock. The worker will have to hit the
|
52
|
+
-- DB in order to make sure the job is still visible anyway.
|
53
|
+
SELECT row_to_json(t)
|
54
|
+
INTO sort_key
|
55
|
+
FROM (
|
56
|
+
SELECT
|
57
|
+
'job_available' AS message_type,
|
58
|
+
NEW.queue AS queue,
|
59
|
+
NEW.priority AS priority,
|
60
|
+
NEW.id AS id,
|
61
|
+
-- Make sure we output timestamps as UTC ISO 8601
|
62
|
+
to_char(NEW.run_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"') AS run_at
|
63
|
+
) t;
|
64
|
+
|
65
|
+
PERFORM pg_notify('que_listener_' || locker_pid::text, sort_key::text);
|
66
|
+
END IF;
|
67
|
+
|
68
|
+
RETURN null;
|
69
|
+
END
|
70
|
+
$$
|
71
|
+
LANGUAGE plpgsql;
|
72
|
+
|
73
|
+
CREATE TRIGGER que_job_notify
|
74
|
+
AFTER INSERT ON que_jobs
|
75
|
+
FOR EACH ROW
|
76
|
+
EXECUTE PROCEDURE public.que_job_notify();
|
data/lib/que/migrations.rb
CHANGED
@@ -4,7 +4,7 @@ module Que
|
|
4
4
|
module Migrations
|
5
5
|
# In order to ship a schema change, add the relevant up and down sql files
|
6
6
|
# to the migrations directory, and bump the version here.
|
7
|
-
CURRENT_VERSION =
|
7
|
+
CURRENT_VERSION = 5
|
8
8
|
|
9
9
|
class << self
|
10
10
|
def migrate!(version:)
|
@@ -28,7 +28,6 @@ module Que
|
|
28
28
|
step,
|
29
29
|
direction,
|
30
30
|
].join('/') << '.sql'
|
31
|
-
|
32
31
|
Que.execute(File.read(filename))
|
33
32
|
end
|
34
33
|
|
data/lib/que/poller.rb
CHANGED
@@ -68,6 +68,7 @@ module Que
|
|
68
68
|
SELECT j
|
69
69
|
FROM public.que_jobs AS j
|
70
70
|
WHERE queue = $1::text
|
71
|
+
AND job_schema_version = #{Que.job_schema_version}
|
71
72
|
AND NOT id = ANY($2::bigint[])
|
72
73
|
AND priority <= pg_temp.que_highest_remaining_priority($3::jsonb)
|
73
74
|
AND run_at <= now()
|
@@ -88,6 +89,7 @@ module Que
|
|
88
89
|
SELECT j
|
89
90
|
FROM public.que_jobs AS j
|
90
91
|
WHERE queue = $1::text
|
92
|
+
AND job_schema_version = #{Que.job_schema_version}
|
91
93
|
AND NOT id = ANY($2::bigint[])
|
92
94
|
AND priority <= pg_temp.que_highest_remaining_priority(jobs.remaining_priorities)
|
93
95
|
AND run_at <= now()
|
@@ -144,8 +146,6 @@ module Que
|
|
144
146
|
|
145
147
|
return unless should_poll?
|
146
148
|
|
147
|
-
expected_count = priorities.inject(0){|s,(_,c)| s + c}
|
148
|
-
|
149
149
|
jobs =
|
150
150
|
connection.execute_prepared(
|
151
151
|
:poll_jobs,
|
@@ -157,7 +157,7 @@ module Que
|
|
157
157
|
)
|
158
158
|
|
159
159
|
@last_polled_at = Time.now
|
160
|
-
@last_poll_satisfied =
|
160
|
+
@last_poll_satisfied = poll_satisfied?(priorities, jobs)
|
161
161
|
|
162
162
|
Que.internal_log :poller_polled, self do
|
163
163
|
{
|
@@ -263,5 +263,12 @@ module Que
|
|
263
263
|
SQL
|
264
264
|
end
|
265
265
|
end
|
266
|
+
|
267
|
+
private
|
268
|
+
|
269
|
+
def poll_satisfied?(priorities, jobs)
|
270
|
+
lowest_priority = priorities.keys.max
|
271
|
+
jobs.count >= priorities[lowest_priority]
|
272
|
+
end
|
266
273
|
end
|
267
274
|
end
|
data/lib/que/version.rb
CHANGED
data/lib/que/worker.rb
CHANGED
@@ -54,10 +54,17 @@ module Que
|
|
54
54
|
private
|
55
55
|
|
56
56
|
def work_loop
|
57
|
-
# Blocks until a job of the appropriate priority is available.
|
58
|
-
#
|
57
|
+
# Blocks until a job of the appropriate priority is available.
|
58
|
+
# `fetch_next_metajob` normally returns a job to be processed.
|
59
|
+
# If the queue is shutting down it will return false, which breaks the loop and
|
59
60
|
# lets the thread finish.
|
60
|
-
while metajob = fetch_next_metajob
|
61
|
+
while (metajob = fetch_next_metajob) != false
|
62
|
+
# If metajob is nil instead of false, we've hit a rare race condition where
|
63
|
+
# there was a job in the buffer when the worker code checked, but the job was
|
64
|
+
# picked up by the time we got around to shifting it off the buffer.
|
65
|
+
# Letting this case go unhandled leads to worker threads exiting pre-maturely, so
|
66
|
+
# we check explicitly and continue the loop.
|
67
|
+
next if metajob.nil?
|
61
68
|
id = metajob.id
|
62
69
|
|
63
70
|
Que.internal_log(:worker_received_job, self) { {id: id} }
|
@@ -130,6 +137,7 @@ module Que
|
|
130
137
|
error: {
|
131
138
|
class: error.class.to_s,
|
132
139
|
message: error.message,
|
140
|
+
backtrace: (error.backtrace || []).join("\n").slice(0, 10000),
|
133
141
|
},
|
134
142
|
)
|
135
143
|
|
@@ -157,7 +165,7 @@ module Que
|
|
157
165
|
Que.execute :set_error, [
|
158
166
|
delay,
|
159
167
|
"#{error.class}: #{error.message}".slice(0, 500),
|
160
|
-
error.backtrace.join("\n").slice(0, 10000),
|
168
|
+
(error.backtrace || []).join("\n").slice(0, 10000),
|
161
169
|
job.fetch(:id),
|
162
170
|
]
|
163
171
|
end
|
data/lib/que.rb
CHANGED
@@ -31,6 +31,8 @@ module Que
|
|
31
31
|
require_relative 'que/utils/queue_management'
|
32
32
|
require_relative 'que/utils/transactions'
|
33
33
|
|
34
|
+
require_relative 'que/version'
|
35
|
+
|
34
36
|
require_relative 'que/connection'
|
35
37
|
require_relative 'que/connection_pool'
|
36
38
|
require_relative 'que/job_methods'
|
@@ -41,7 +43,6 @@ module Que
|
|
41
43
|
require_relative 'que/migrations'
|
42
44
|
require_relative 'que/poller'
|
43
45
|
require_relative 'que/result_queue'
|
44
|
-
require_relative 'que/version'
|
45
46
|
require_relative 'que/worker'
|
46
47
|
|
47
48
|
class << self
|
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
set -Eeuo pipefail
|
4
|
+
|
5
|
+
# For using your own dotfiles within the Docker container
|
6
|
+
if [ -f /.docker-rc.d/.docker-bashrc ]; then
|
7
|
+
echo "source /.docker-rc.d/.docker-bashrc" >> ~/.bashrc
|
8
|
+
fi
|
9
|
+
|
10
|
+
gem list -i -e bundler -v "$RUBY_BUNDLER_VERSION" >/dev/null || gem install bundler -v "$RUBY_BUNDLER_VERSION"
|
11
|
+
|
12
|
+
bundle check --dry-run || bundle install
|
13
|
+
|
14
|
+
exec "${@-bash}"
|