que 0.11.3 → 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github/workflows/tests.yml +51 -0
- data/.gitignore +2 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +502 -97
- data/Dockerfile +20 -0
- data/LICENSE.txt +1 -1
- data/README.md +205 -59
- 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/que +8 -81
- data/docker-compose.yml +47 -0
- data/docs/README.md +881 -0
- data/lib/que/active_job/extensions.rb +114 -0
- data/lib/que/active_record/connection.rb +51 -0
- data/lib/que/active_record/model.rb +48 -0
- data/lib/que/command_line_interface.rb +259 -0
- data/lib/que/connection.rb +198 -0
- data/lib/que/connection_pool.rb +78 -0
- data/lib/que/job.rb +210 -103
- data/lib/que/job_buffer.rb +255 -0
- data/lib/que/job_methods.rb +176 -0
- data/lib/que/listener.rb +176 -0
- data/lib/que/locker.rb +507 -0
- data/lib/que/metajob.rb +47 -0
- data/lib/que/migrations/4/down.sql +48 -0
- data/lib/que/migrations/4/up.sql +267 -0
- data/lib/que/migrations/5/down.sql +73 -0
- data/lib/que/migrations/5/up.sql +76 -0
- data/lib/que/migrations/6/down.sql +8 -0
- data/lib/que/migrations/6/up.sql +8 -0
- data/lib/que/migrations/7/down.sql +5 -0
- data/lib/que/migrations/7/up.sql +13 -0
- data/lib/que/migrations.rb +37 -18
- data/lib/que/poller.rb +274 -0
- data/lib/que/rails/railtie.rb +12 -0
- data/lib/que/result_queue.rb +35 -0
- data/lib/que/sequel/model.rb +52 -0
- data/lib/que/utils/assertions.rb +62 -0
- data/lib/que/utils/constantization.rb +19 -0
- data/lib/que/utils/error_notification.rb +68 -0
- data/lib/que/utils/freeze.rb +20 -0
- data/lib/que/utils/introspection.rb +50 -0
- data/lib/que/utils/json_serialization.rb +21 -0
- data/lib/que/utils/logging.rb +79 -0
- data/lib/que/utils/middleware.rb +46 -0
- data/lib/que/utils/queue_management.rb +18 -0
- data/lib/que/utils/ruby2_keywords.rb +19 -0
- data/lib/que/utils/transactions.rb +34 -0
- data/lib/que/version.rb +5 -1
- data/lib/que/worker.rb +145 -149
- data/lib/que.rb +103 -159
- data/que.gemspec +17 -4
- data/scripts/docker-entrypoint +14 -0
- data/scripts/test +6 -0
- metadata +59 -95
- data/.rspec +0 -2
- data/.travis.yml +0 -17
- data/Gemfile +0 -24
- data/docs/advanced_setup.md +0 -106
- data/docs/customizing_que.md +0 -200
- data/docs/error_handling.md +0 -47
- data/docs/inspecting_the_queue.md +0 -114
- data/docs/logging.md +0 -50
- data/docs/managing_workers.md +0 -80
- data/docs/migrating.md +0 -30
- data/docs/multiple_queues.md +0 -27
- data/docs/shutting_down_safely.md +0 -7
- data/docs/using_plain_connections.md +0 -41
- data/docs/using_sequel.md +0 -31
- data/docs/writing_reliable_jobs.md +0 -117
- data/lib/generators/que/install_generator.rb +0 -24
- data/lib/generators/que/templates/add_que.rb +0 -13
- data/lib/que/adapters/active_record.rb +0 -54
- data/lib/que/adapters/base.rb +0 -127
- data/lib/que/adapters/connection_pool.rb +0 -16
- data/lib/que/adapters/pg.rb +0 -21
- data/lib/que/adapters/pond.rb +0 -16
- data/lib/que/adapters/sequel.rb +0 -20
- data/lib/que/railtie.rb +0 -16
- data/lib/que/rake_tasks.rb +0 -59
- data/lib/que/sql.rb +0 -152
- data/spec/adapters/active_record_spec.rb +0 -152
- data/spec/adapters/connection_pool_spec.rb +0 -22
- data/spec/adapters/pg_spec.rb +0 -41
- data/spec/adapters/pond_spec.rb +0 -22
- data/spec/adapters/sequel_spec.rb +0 -57
- data/spec/gemfiles/Gemfile1 +0 -18
- data/spec/gemfiles/Gemfile2 +0 -18
- data/spec/spec_helper.rb +0 -118
- data/spec/support/helpers.rb +0 -19
- data/spec/support/jobs.rb +0 -35
- data/spec/support/shared_examples/adapter.rb +0 -37
- data/spec/support/shared_examples/multi_threaded_adapter.rb +0 -46
- data/spec/travis.rb +0 -23
- data/spec/unit/connection_spec.rb +0 -14
- data/spec/unit/customization_spec.rb +0 -251
- data/spec/unit/enqueue_spec.rb +0 -245
- data/spec/unit/helper_spec.rb +0 -12
- data/spec/unit/logging_spec.rb +0 -101
- data/spec/unit/migrations_spec.rb +0 -84
- data/spec/unit/pool_spec.rb +0 -365
- data/spec/unit/run_spec.rb +0 -14
- data/spec/unit/states_spec.rb +0 -50
- data/spec/unit/stats_spec.rb +0 -46
- data/spec/unit/transaction_spec.rb +0 -36
- data/spec/unit/work_spec.rb +0 -407
- data/spec/unit/worker_spec.rb +0 -167
- data/tasks/benchmark.rb +0 -3
- data/tasks/rspec.rb +0 -14
- data/tasks/safe_shutdown.rb +0 -67
data/lib/que/locker.rb
ADDED
@@ -0,0 +1,507 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# The Locker class encapsulates a thread that is listening/polling for new
|
4
|
+
# jobs in the DB, locking them, passing their primary keys to workers, then
|
5
|
+
# cleaning up by unlocking them once the workers are done.
|
6
|
+
|
7
|
+
require 'set'
|
8
|
+
|
9
|
+
module Que
|
10
|
+
Listener::MESSAGE_FORMATS[:job_available] =
|
11
|
+
{
|
12
|
+
queue: String,
|
13
|
+
id: Integer,
|
14
|
+
run_at: TIME_REGEX,
|
15
|
+
priority: Integer,
|
16
|
+
}
|
17
|
+
|
18
|
+
SQL[:clean_lockers] =
|
19
|
+
%{
|
20
|
+
DELETE FROM public.que_lockers
|
21
|
+
WHERE pid = pg_backend_pid()
|
22
|
+
OR NOT EXISTS (SELECT 1 FROM pg_stat_activity WHERE pid = public.que_lockers.pid)
|
23
|
+
}
|
24
|
+
|
25
|
+
SQL[:register_locker] =
|
26
|
+
%{
|
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
|
+
}
|
30
|
+
|
31
|
+
class Locker
|
32
|
+
attr_reader :thread, :workers, :job_buffer, :locks, :queues, :poll_interval
|
33
|
+
|
34
|
+
MESSAGE_RESOLVERS = {}
|
35
|
+
RESULT_RESOLVERS = {}
|
36
|
+
|
37
|
+
MESSAGE_RESOLVERS[:job_available] =
|
38
|
+
-> (messages) {
|
39
|
+
metajobs = messages.map { |key| Metajob.new(key) }
|
40
|
+
push_jobs(lock_jobs(job_buffer.accept?(metajobs)))
|
41
|
+
}
|
42
|
+
|
43
|
+
RESULT_RESOLVERS[:job_finished] =
|
44
|
+
-> (messages) { finish_jobs(messages.map{|m| m.fetch(:metajob)}) }
|
45
|
+
|
46
|
+
DEFAULT_POLL_INTERVAL = 5.0
|
47
|
+
DEFAULT_WAIT_PERIOD = 50
|
48
|
+
DEFAULT_MAXIMUM_BUFFER_SIZE = 8
|
49
|
+
DEFAULT_WORKER_PRIORITIES = [10, 30, 50, nil, nil, nil].freeze
|
50
|
+
|
51
|
+
def initialize(
|
52
|
+
queues: [Que.default_queue],
|
53
|
+
connection_url: nil,
|
54
|
+
listen: true,
|
55
|
+
poll: true,
|
56
|
+
poll_interval: DEFAULT_POLL_INTERVAL,
|
57
|
+
wait_period: DEFAULT_WAIT_PERIOD,
|
58
|
+
maximum_buffer_size: DEFAULT_MAXIMUM_BUFFER_SIZE,
|
59
|
+
worker_priorities: DEFAULT_WORKER_PRIORITIES,
|
60
|
+
on_worker_start: nil
|
61
|
+
)
|
62
|
+
|
63
|
+
# Sanity-check all our arguments, since some users may instantiate Locker
|
64
|
+
# directly.
|
65
|
+
Que.assert [TrueClass, FalseClass], listen
|
66
|
+
Que.assert [TrueClass, FalseClass], poll
|
67
|
+
|
68
|
+
Que.assert Numeric, poll_interval
|
69
|
+
Que.assert Numeric, wait_period
|
70
|
+
|
71
|
+
Que.assert Array, worker_priorities
|
72
|
+
worker_priorities.each { |p| Que.assert([Integer, NilClass], p) }
|
73
|
+
|
74
|
+
# We use a JobBuffer to track jobs and pass them to workers, and a
|
75
|
+
# ResultQueue to receive messages from workers.
|
76
|
+
@job_buffer = JobBuffer.new(
|
77
|
+
maximum_size: maximum_buffer_size,
|
78
|
+
priorities: worker_priorities.uniq,
|
79
|
+
)
|
80
|
+
|
81
|
+
@result_queue = ResultQueue.new
|
82
|
+
|
83
|
+
@stop = false
|
84
|
+
|
85
|
+
Que.internal_log :locker_instantiate, self do
|
86
|
+
{
|
87
|
+
queues: queues,
|
88
|
+
listen: listen,
|
89
|
+
poll: poll,
|
90
|
+
poll_interval: poll_interval,
|
91
|
+
wait_period: wait_period,
|
92
|
+
maximum_buffer_size: maximum_buffer_size,
|
93
|
+
worker_priorities: worker_priorities,
|
94
|
+
}
|
95
|
+
end
|
96
|
+
|
97
|
+
# Local cache of which advisory locks are held by this connection.
|
98
|
+
@locks = Set.new
|
99
|
+
|
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
|
+
|
114
|
+
@wait_period = wait_period.to_f / 1000 # Milliseconds to seconds.
|
115
|
+
|
116
|
+
@workers =
|
117
|
+
worker_priorities.map do |priority|
|
118
|
+
Worker.new(
|
119
|
+
priority: priority,
|
120
|
+
job_buffer: @job_buffer,
|
121
|
+
result_queue: @result_queue,
|
122
|
+
start_callback: on_worker_start,
|
123
|
+
)
|
124
|
+
end
|
125
|
+
|
126
|
+
# To prevent race conditions, let every worker get into a ready state
|
127
|
+
# before starting up the locker thread.
|
128
|
+
loop do
|
129
|
+
break if job_buffer.waiting_count == workers.count
|
130
|
+
sleep 0.001
|
131
|
+
end
|
132
|
+
|
133
|
+
# If we weren't passed a specific connection_url, borrow a connection from
|
134
|
+
# the pool and derive the connection string from it.
|
135
|
+
connection_args =
|
136
|
+
if connection_url
|
137
|
+
uri = URI.parse(connection_url)
|
138
|
+
|
139
|
+
opts =
|
140
|
+
{
|
141
|
+
host: uri.host,
|
142
|
+
user: uri.user,
|
143
|
+
password: uri.password,
|
144
|
+
port: uri.port || 5432,
|
145
|
+
dbname: uri.path[1..-1],
|
146
|
+
}
|
147
|
+
|
148
|
+
if uri.query
|
149
|
+
opts.merge!(Hash[uri.query.split("&").map{|s| s.split('=')}.map{|a,b| [a.to_sym, b]}])
|
150
|
+
end
|
151
|
+
|
152
|
+
opts
|
153
|
+
else
|
154
|
+
Que.pool.checkout do |conn|
|
155
|
+
c = conn.wrapped_connection
|
156
|
+
|
157
|
+
{
|
158
|
+
host: c.host,
|
159
|
+
user: c.user,
|
160
|
+
password: c.pass,
|
161
|
+
port: c.port,
|
162
|
+
dbname: c.db,
|
163
|
+
}
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
@connection = Que::Connection.wrap(PG::Connection.open(connection_args))
|
168
|
+
|
169
|
+
@thread =
|
170
|
+
Thread.new do
|
171
|
+
# An error causing this thread to exit is a bug in Que, which we want
|
172
|
+
# to know about ASAP, so propagate the error if it happens.
|
173
|
+
Thread.current.abort_on_exception = true
|
174
|
+
|
175
|
+
# Give this thread priority, so it can promptly respond to NOTIFYs.
|
176
|
+
Thread.current.priority = 1
|
177
|
+
|
178
|
+
begin
|
179
|
+
unless connection_args.has_key?(:application_name)
|
180
|
+
@connection.execute(
|
181
|
+
"SELECT set_config('application_name', $1, false)",
|
182
|
+
["Que Locker: #{@connection.backend_pid}"]
|
183
|
+
)
|
184
|
+
end
|
185
|
+
|
186
|
+
Poller.setup(@connection)
|
187
|
+
|
188
|
+
@listener =
|
189
|
+
if listen
|
190
|
+
Listener.new(connection: @connection)
|
191
|
+
end
|
192
|
+
|
193
|
+
@pollers =
|
194
|
+
if poll
|
195
|
+
@queues.map do |queue_name, interval|
|
196
|
+
Poller.new(
|
197
|
+
connection: @connection,
|
198
|
+
queue: queue_name,
|
199
|
+
poll_interval: interval,
|
200
|
+
)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
work_loop
|
205
|
+
ensure
|
206
|
+
@connection.wrapped_connection.close
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def stop!
|
212
|
+
stop; wait_for_stop
|
213
|
+
end
|
214
|
+
|
215
|
+
def stop
|
216
|
+
@job_buffer.stop
|
217
|
+
@stop = true
|
218
|
+
end
|
219
|
+
|
220
|
+
def stopping?
|
221
|
+
@stop
|
222
|
+
end
|
223
|
+
|
224
|
+
def wait_for_stop
|
225
|
+
@thread.join
|
226
|
+
end
|
227
|
+
|
228
|
+
private
|
229
|
+
|
230
|
+
attr_reader :connection, :pollers
|
231
|
+
|
232
|
+
def work_loop
|
233
|
+
Que.log(
|
234
|
+
level: :debug,
|
235
|
+
event: :locker_start,
|
236
|
+
queues: @queue_names,
|
237
|
+
)
|
238
|
+
|
239
|
+
Que.internal_log :locker_start, self do
|
240
|
+
{
|
241
|
+
backend_pid: connection.backend_pid,
|
242
|
+
worker_priorities: workers.map(&:priority),
|
243
|
+
pollers: pollers && pollers.map { |p| [p.queue, p.poll_interval] }
|
244
|
+
}
|
245
|
+
end
|
246
|
+
|
247
|
+
begin
|
248
|
+
@listener.listen if @listener
|
249
|
+
|
250
|
+
startup
|
251
|
+
|
252
|
+
{} while cycle
|
253
|
+
|
254
|
+
Que.log(
|
255
|
+
level: :debug,
|
256
|
+
event: :locker_stop,
|
257
|
+
)
|
258
|
+
|
259
|
+
shutdown
|
260
|
+
ensure
|
261
|
+
connection.execute :clean_lockers
|
262
|
+
|
263
|
+
@listener.unlisten if @listener
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
def startup
|
268
|
+
# A previous locker that didn't exit cleanly may have left behind
|
269
|
+
# a bad locker record, so clean up before registering.
|
270
|
+
connection.execute :clean_lockers
|
271
|
+
connection.execute :register_locker, [
|
272
|
+
@workers.count,
|
273
|
+
"{#{@workers.map(&:priority).map{|p| p || 'NULL'}.join(',')}}",
|
274
|
+
Process.pid,
|
275
|
+
CURRENT_HOSTNAME,
|
276
|
+
!!@listener,
|
277
|
+
"{\"#{@queue_names.join('","')}\"}",
|
278
|
+
Que.job_schema_version,
|
279
|
+
]
|
280
|
+
end
|
281
|
+
|
282
|
+
def cycle
|
283
|
+
# Poll at the start of a cycle, so that when the worker starts up we can
|
284
|
+
# load up the queue with jobs immediately.
|
285
|
+
poll
|
286
|
+
|
287
|
+
# If we got the stop call while we were polling, break before going to
|
288
|
+
# sleep.
|
289
|
+
return if @stop
|
290
|
+
|
291
|
+
# The main sleeping part of the cycle. If this is a listening locker, this
|
292
|
+
# is where we wait for notifications.
|
293
|
+
wait
|
294
|
+
|
295
|
+
# Manage any job output we got while we were sleeping.
|
296
|
+
handle_results
|
297
|
+
|
298
|
+
# If we haven't gotten the stop signal, cycle again.
|
299
|
+
!@stop
|
300
|
+
end
|
301
|
+
|
302
|
+
def shutdown
|
303
|
+
unlock_jobs(@job_buffer.clear)
|
304
|
+
wait_for_shutdown
|
305
|
+
handle_results
|
306
|
+
end
|
307
|
+
|
308
|
+
def wait_for_shutdown
|
309
|
+
@workers.each(&:wait_until_stopped)
|
310
|
+
end
|
311
|
+
|
312
|
+
def poll
|
313
|
+
# Only poll when there are pollers to use (that is, when polling is
|
314
|
+
# enabled).
|
315
|
+
return unless pollers
|
316
|
+
|
317
|
+
# Figure out what job priorities we have to fill.
|
318
|
+
priorities = job_buffer.available_priorities
|
319
|
+
|
320
|
+
# Only poll when there are workers ready for jobs.
|
321
|
+
return if priorities.empty?
|
322
|
+
|
323
|
+
all_metajobs = []
|
324
|
+
|
325
|
+
pollers.each do |poller|
|
326
|
+
Que.internal_log(:locker_polling, self) {
|
327
|
+
{
|
328
|
+
priorities: priorities,
|
329
|
+
held_locks: @locks.to_a,
|
330
|
+
queue: poller.queue,
|
331
|
+
}
|
332
|
+
}
|
333
|
+
|
334
|
+
if metajobs = poller.poll(priorities: priorities, held_locks: @locks)
|
335
|
+
metajobs.sort!
|
336
|
+
all_metajobs.concat(metajobs)
|
337
|
+
|
338
|
+
# Update the desired priorities list to take the priorities that we
|
339
|
+
# just retrieved into account.
|
340
|
+
metajobs.each do |metajob|
|
341
|
+
job_priority = metajob.job.fetch(:priority)
|
342
|
+
|
343
|
+
priorities.each do |priority, count|
|
344
|
+
if job_priority <= priority
|
345
|
+
new_priority = count - 1
|
346
|
+
|
347
|
+
if new_priority <= 0
|
348
|
+
priorities.delete(priority)
|
349
|
+
else
|
350
|
+
priorities[priority] = new_priority
|
351
|
+
end
|
352
|
+
|
353
|
+
break
|
354
|
+
end
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
break if priorities.empty?
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
all_metajobs.each { |metajob| mark_id_as_locked(metajob.id) }
|
363
|
+
push_jobs(all_metajobs)
|
364
|
+
end
|
365
|
+
|
366
|
+
def wait
|
367
|
+
if l = @listener
|
368
|
+
l.wait_for_grouped_messages(@wait_period).each do |type, messages|
|
369
|
+
if resolver = MESSAGE_RESOLVERS[type]
|
370
|
+
instance_exec messages, &resolver
|
371
|
+
else
|
372
|
+
raise Error, "Unexpected message type: #{type.inspect}"
|
373
|
+
end
|
374
|
+
end
|
375
|
+
else
|
376
|
+
sleep(@wait_period)
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
def handle_results
|
381
|
+
messages_by_type =
|
382
|
+
@result_queue.clear.group_by{|r| r.fetch(:message_type)}
|
383
|
+
|
384
|
+
messages_by_type.each do |type, messages|
|
385
|
+
if resolver = RESULT_RESOLVERS[type]
|
386
|
+
instance_exec messages, &resolver
|
387
|
+
else
|
388
|
+
raise Error, "Unexpected result type: #{type.inspect}"
|
389
|
+
end
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
def lock_jobs(metajobs)
|
394
|
+
metajobs.reject! { |m| @locks.include?(m.id) }
|
395
|
+
return metajobs if metajobs.empty?
|
396
|
+
|
397
|
+
ids = metajobs.map { |m| m.id.to_i }
|
398
|
+
|
399
|
+
Que.internal_log :locker_locking, self do
|
400
|
+
{
|
401
|
+
backend_pid: connection.backend_pid,
|
402
|
+
ids: ids,
|
403
|
+
}
|
404
|
+
end
|
405
|
+
|
406
|
+
materalize_cte = connection.server_version >= 12_00_00
|
407
|
+
|
408
|
+
jobs =
|
409
|
+
connection.execute \
|
410
|
+
<<-SQL
|
411
|
+
WITH jobs AS #{materalize_cte ? 'MATERIALIZED' : ''} (SELECT * FROM que_jobs WHERE id IN (#{ids.join(', ')}))
|
412
|
+
SELECT * FROM jobs WHERE pg_try_advisory_lock(id)
|
413
|
+
SQL
|
414
|
+
|
415
|
+
jobs_by_id = {}
|
416
|
+
|
417
|
+
jobs.each do |job|
|
418
|
+
id = job.fetch(:id)
|
419
|
+
mark_id_as_locked(id)
|
420
|
+
jobs_by_id[id] = job
|
421
|
+
end
|
422
|
+
|
423
|
+
metajobs.keep_if do |metajob|
|
424
|
+
if job = jobs_by_id[metajob.id]
|
425
|
+
metajob.set_job(job)
|
426
|
+
true
|
427
|
+
else
|
428
|
+
false
|
429
|
+
end
|
430
|
+
end
|
431
|
+
end
|
432
|
+
|
433
|
+
def push_jobs(metajobs)
|
434
|
+
return if metajobs.empty?
|
435
|
+
|
436
|
+
# First check that the jobs are all still visible/available in the DB.
|
437
|
+
ids = metajobs.map(&:id)
|
438
|
+
|
439
|
+
verified_ids =
|
440
|
+
connection.execute(
|
441
|
+
<<-SQL
|
442
|
+
SELECT id
|
443
|
+
FROM public.que_jobs
|
444
|
+
WHERE finished_at IS NULL
|
445
|
+
AND expired_at IS NULL
|
446
|
+
AND id IN (#{ids.join(', ')})
|
447
|
+
SQL
|
448
|
+
).map{|h| h[:id]}.to_set
|
449
|
+
|
450
|
+
good, bad = metajobs.partition{|mj| verified_ids.include?(mj.id)}
|
451
|
+
|
452
|
+
# Need to unlock any low-importance jobs the new ones may displace.
|
453
|
+
if displaced = @job_buffer.push(*good)
|
454
|
+
bad.concat(displaced)
|
455
|
+
end
|
456
|
+
|
457
|
+
unlock_jobs(bad)
|
458
|
+
end
|
459
|
+
|
460
|
+
def finish_jobs(metajobs)
|
461
|
+
unlock_jobs(metajobs)
|
462
|
+
end
|
463
|
+
|
464
|
+
def unlock_jobs(metajobs)
|
465
|
+
return if metajobs.empty?
|
466
|
+
|
467
|
+
# Unclear how untrusted input would get passed to this method, but since
|
468
|
+
# we need string interpolation here, make sure we only have integers.
|
469
|
+
ids = metajobs.map { |job| job.id.to_i }
|
470
|
+
|
471
|
+
Que.internal_log :locker_unlocking, self do
|
472
|
+
{
|
473
|
+
backend_pid: connection.backend_pid,
|
474
|
+
ids: ids,
|
475
|
+
}
|
476
|
+
end
|
477
|
+
|
478
|
+
values = ids.join('), (')
|
479
|
+
|
480
|
+
results =
|
481
|
+
connection.execute \
|
482
|
+
"SELECT pg_advisory_unlock(v.i) FROM (VALUES (#{values})) v (i)"
|
483
|
+
|
484
|
+
results.each do |result|
|
485
|
+
Que.assert(result.fetch(:pg_advisory_unlock)) do
|
486
|
+
[
|
487
|
+
"Tried to unlock a job we hadn't locked!",
|
488
|
+
results.inspect,
|
489
|
+
ids.inspect,
|
490
|
+
].join(' ')
|
491
|
+
end
|
492
|
+
end
|
493
|
+
|
494
|
+
ids.each do |id|
|
495
|
+
Que.assert(@locks.delete?(id)) do
|
496
|
+
"Tried to remove a local lock that didn't exist!: #{id}"
|
497
|
+
end
|
498
|
+
end
|
499
|
+
end
|
500
|
+
|
501
|
+
def mark_id_as_locked(id)
|
502
|
+
Que.assert(@locks.add?(id)) do
|
503
|
+
"Tried to lock a job that was already locked: #{id}"
|
504
|
+
end
|
505
|
+
end
|
506
|
+
end
|
507
|
+
end
|
data/lib/que/metajob.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# A thin wrapper around a job's data that lets us do things like sort easily and
|
4
|
+
# make sure that run_at is in the format we want.
|
5
|
+
|
6
|
+
module Que
|
7
|
+
class Metajob
|
8
|
+
SORT_KEYS = [:priority, :run_at, :id].freeze
|
9
|
+
|
10
|
+
attr_reader :job
|
11
|
+
|
12
|
+
def initialize(job)
|
13
|
+
set_job(job)
|
14
|
+
end
|
15
|
+
|
16
|
+
def set_job(job)
|
17
|
+
if (run_at = job.fetch(:run_at)).is_a?(Time)
|
18
|
+
job[:run_at] = run_at.utc.iso8601(6)
|
19
|
+
end
|
20
|
+
|
21
|
+
@job = job
|
22
|
+
end
|
23
|
+
|
24
|
+
def id
|
25
|
+
job.fetch(:id)
|
26
|
+
end
|
27
|
+
|
28
|
+
def <=>(other)
|
29
|
+
k1 = job
|
30
|
+
k2 = other.job
|
31
|
+
|
32
|
+
SORT_KEYS.each do |key|
|
33
|
+
value1 = k1.fetch(key)
|
34
|
+
value2 = k2.fetch(key)
|
35
|
+
|
36
|
+
return -1 if value1 < value2
|
37
|
+
return 1 if value1 > value2
|
38
|
+
end
|
39
|
+
|
40
|
+
0
|
41
|
+
end
|
42
|
+
|
43
|
+
def priority_sufficient?(threshold)
|
44
|
+
threshold.nil? || job.fetch(:priority) <= threshold
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
ALTER TABLE que_jobs RESET (fillfactor);
|
2
|
+
|
3
|
+
ALTER TABLE que_jobs DROP CONSTRAINT que_jobs_pkey;
|
4
|
+
DROP INDEX que_poll_idx;
|
5
|
+
DROP INDEX que_jobs_data_gin_idx;
|
6
|
+
|
7
|
+
DROP TRIGGER que_job_notify ON que_jobs;
|
8
|
+
DROP FUNCTION que_job_notify();
|
9
|
+
DROP TRIGGER que_state_notify ON que_jobs;
|
10
|
+
DROP FUNCTION que_state_notify();
|
11
|
+
DROP FUNCTION que_determine_job_state(que_jobs);
|
12
|
+
DROP TABLE que_lockers;
|
13
|
+
|
14
|
+
DROP TABLE que_values;
|
15
|
+
DROP INDEX que_jobs_args_gin_idx;
|
16
|
+
|
17
|
+
ALTER TABLE que_jobs RENAME COLUMN id TO job_id;
|
18
|
+
ALTER SEQUENCE que_jobs_id_seq RENAME TO que_jobs_job_id_seq;
|
19
|
+
|
20
|
+
ALTER TABLE que_jobs RENAME COLUMN last_error_message TO last_error;
|
21
|
+
|
22
|
+
DELETE FROM que_jobs WHERE (finished_at IS NOT NULL OR expired_at IS NOT NULL);
|
23
|
+
|
24
|
+
ALTER TABLE que_jobs
|
25
|
+
DROP CONSTRAINT error_length,
|
26
|
+
DROP CONSTRAINT queue_length,
|
27
|
+
DROP CONSTRAINT job_class_length,
|
28
|
+
DROP CONSTRAINT valid_args,
|
29
|
+
DROP COLUMN finished_at,
|
30
|
+
DROP COLUMN expired_at,
|
31
|
+
ALTER args TYPE JSON using args::json;
|
32
|
+
|
33
|
+
UPDATE que_jobs
|
34
|
+
SET
|
35
|
+
queue = CASE queue WHEN 'default' THEN '' ELSE queue END,
|
36
|
+
last_error = last_error || coalesce(E'\n' || last_error_backtrace, '');
|
37
|
+
|
38
|
+
ALTER TABLE que_jobs
|
39
|
+
DROP COLUMN data,
|
40
|
+
DROP COLUMN last_error_backtrace,
|
41
|
+
ALTER COLUMN args SET NOT NULL,
|
42
|
+
ALTER COLUMN args SET DEFAULT '[]',
|
43
|
+
ALTER COLUMN queue SET DEFAULT '';
|
44
|
+
|
45
|
+
ALTER TABLE que_jobs
|
46
|
+
ADD PRIMARY KEY (queue, priority, run_at, job_id);
|
47
|
+
|
48
|
+
DROP FUNCTION que_validate_tags(jsonb);
|