que 0.11.3 → 2.2.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 +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/poller.rb
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Que
|
|
4
|
+
class Poller
|
|
5
|
+
# The following SQL statement locks a batch of jobs using a Postgres
|
|
6
|
+
# recursive CTE [1].
|
|
7
|
+
#
|
|
8
|
+
# As noted by the Postgres documentation, it may be slightly easier to
|
|
9
|
+
# think about this expression as iteration rather than recursion, despite
|
|
10
|
+
# the `RECURSION` nomenclature defined by the SQL standards committee.
|
|
11
|
+
# Recursion is used here so that jobs in the table can be iterated one-by-
|
|
12
|
+
# one until a lock can be acquired, where a non-recursive `SELECT` would
|
|
13
|
+
# have the undesirable side-effect of locking jobs unnecessarily. For
|
|
14
|
+
# example, the following might lock more than five jobs during execution:
|
|
15
|
+
#
|
|
16
|
+
# SELECT (j).*, pg_try_advisory_lock((j).id) AS locked
|
|
17
|
+
# FROM public.que_jobs AS j
|
|
18
|
+
# LIMIT 5;
|
|
19
|
+
#
|
|
20
|
+
# The CTE will initially produce an "anchor" from the non-recursive term
|
|
21
|
+
# (i.e. before the `UNION`), and then use it as the contents of the
|
|
22
|
+
# working table as it continues to iterate through `que_jobs` looking for
|
|
23
|
+
# locks. The jobs table has an index on (priority, run_at, id) which
|
|
24
|
+
# allows it to walk the jobs table in a stable manner. As noted above, the
|
|
25
|
+
# recursion examines/locks one job at a time. Every time the recursive
|
|
26
|
+
# entry runs, it's output becomes the new contents of the working table,
|
|
27
|
+
# and what was previously in the working table is appended to the final
|
|
28
|
+
# result set. For more information on the basic workings of recursive
|
|
29
|
+
# CTEs, see http://www.postgresql.org/docs/devel/static/queries-with.html
|
|
30
|
+
#
|
|
31
|
+
# The polling query is provided a JSONB hash of desired job priorities.
|
|
32
|
+
# For example, if the locker has three workers free that can work a
|
|
33
|
+
# priority less than 5, and two workers free that can work a priority less
|
|
34
|
+
# than 10, the provided priority document is `{"5":3,"10":2}`. The query
|
|
35
|
+
# uses this information to decide what jobs to lock - if only high-
|
|
36
|
+
# priority workers were available, it wouldn't make sense to retrieve low-
|
|
37
|
+
# priority jobs.
|
|
38
|
+
#
|
|
39
|
+
# As each job is retrieved from the table, it is passed to
|
|
40
|
+
# lock_and_update_priorities() (which, for future flexibility, we define
|
|
41
|
+
# as a temporary function attached to the connection rather than embedding
|
|
42
|
+
# permanently into the DB schema). lock_and_update_priorities() attempts
|
|
43
|
+
# to lock the given job and, if it is able to, updates the priorities
|
|
44
|
+
# document to reflect that a job was available for that given priority.
|
|
45
|
+
# When the priorities document is emptied (all the counts of desired jobs
|
|
46
|
+
# for the various priorities have reached zero and been removed), the
|
|
47
|
+
# recursive query returns an empty result and the recursion breaks. This
|
|
48
|
+
# also happens if there aren't enough appropriate jobs in the jobs table.
|
|
49
|
+
#
|
|
50
|
+
# Also note the use of JOIN LATERAL to combine the job data with the
|
|
51
|
+
# output of lock_and_update_priorities(). The naive approach would be to
|
|
52
|
+
# write the SELECT as `SELECT (j).*, (lock_and_update_priorities(..., j)).*`,
|
|
53
|
+
# but the asterisk-expansion of the latter composite row causes the function
|
|
54
|
+
# to be evaluated twice, and to thereby take the advisory lock twice,
|
|
55
|
+
# which complicates the later unlocking step.
|
|
56
|
+
#
|
|
57
|
+
# Thanks to RhodiumToad in #postgresql for help with the original
|
|
58
|
+
# (simpler) version of the recursive job lock CTE.
|
|
59
|
+
|
|
60
|
+
SQL[:poll_jobs] =
|
|
61
|
+
%{
|
|
62
|
+
WITH RECURSIVE jobs AS (
|
|
63
|
+
SELECT
|
|
64
|
+
(j).*,
|
|
65
|
+
l.locked,
|
|
66
|
+
l.remaining_priorities
|
|
67
|
+
FROM (
|
|
68
|
+
SELECT j
|
|
69
|
+
FROM public.que_jobs AS j
|
|
70
|
+
WHERE queue = $1::text
|
|
71
|
+
AND job_schema_version = #{Que.job_schema_version}
|
|
72
|
+
AND NOT id = ANY($2::bigint[])
|
|
73
|
+
AND priority <= pg_temp.que_highest_remaining_priority($3::jsonb)
|
|
74
|
+
AND run_at <= now()
|
|
75
|
+
AND finished_at IS NULL AND expired_at IS NULL
|
|
76
|
+
ORDER BY priority, run_at, id
|
|
77
|
+
LIMIT 1
|
|
78
|
+
) AS t1
|
|
79
|
+
JOIN LATERAL (SELECT * FROM pg_temp.lock_and_update_priorities($3::jsonb, j)) AS l ON true
|
|
80
|
+
UNION ALL (
|
|
81
|
+
SELECT
|
|
82
|
+
(j).*,
|
|
83
|
+
l.locked,
|
|
84
|
+
l.remaining_priorities
|
|
85
|
+
FROM (
|
|
86
|
+
SELECT
|
|
87
|
+
remaining_priorities,
|
|
88
|
+
(
|
|
89
|
+
SELECT j
|
|
90
|
+
FROM public.que_jobs AS j
|
|
91
|
+
WHERE queue = $1::text
|
|
92
|
+
AND job_schema_version = #{Que.job_schema_version}
|
|
93
|
+
AND NOT id = ANY($2::bigint[])
|
|
94
|
+
AND priority <= pg_temp.que_highest_remaining_priority(jobs.remaining_priorities)
|
|
95
|
+
AND run_at <= now()
|
|
96
|
+
AND finished_at IS NULL AND expired_at IS NULL
|
|
97
|
+
AND (priority, run_at, id) >
|
|
98
|
+
(jobs.priority, jobs.run_at, jobs.id)
|
|
99
|
+
ORDER BY priority, run_at, id
|
|
100
|
+
LIMIT 1
|
|
101
|
+
) AS j
|
|
102
|
+
|
|
103
|
+
FROM jobs
|
|
104
|
+
WHERE jobs.id IS NOT NULL AND jobs.remaining_priorities != '{}'::jsonb
|
|
105
|
+
LIMIT 1
|
|
106
|
+
) AS t1
|
|
107
|
+
JOIN LATERAL (SELECT * FROM pg_temp.lock_and_update_priorities(remaining_priorities, j)) AS l ON true
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
SELECT *
|
|
111
|
+
FROM jobs
|
|
112
|
+
WHERE locked
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
attr_reader \
|
|
116
|
+
:connection,
|
|
117
|
+
:queue,
|
|
118
|
+
:poll_interval,
|
|
119
|
+
:last_polled_at,
|
|
120
|
+
:last_poll_satisfied
|
|
121
|
+
|
|
122
|
+
def initialize(
|
|
123
|
+
connection:,
|
|
124
|
+
queue:,
|
|
125
|
+
poll_interval:
|
|
126
|
+
)
|
|
127
|
+
@connection = connection
|
|
128
|
+
@queue = queue
|
|
129
|
+
@poll_interval = poll_interval
|
|
130
|
+
@last_polled_at = nil
|
|
131
|
+
@last_poll_satisfied = nil
|
|
132
|
+
|
|
133
|
+
Que.internal_log :poller_instantiate, self do
|
|
134
|
+
{
|
|
135
|
+
backend_pid: connection.backend_pid,
|
|
136
|
+
queue: queue,
|
|
137
|
+
poll_interval: poll_interval,
|
|
138
|
+
}
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def poll(
|
|
143
|
+
priorities:,
|
|
144
|
+
held_locks:
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
return unless should_poll?
|
|
148
|
+
|
|
149
|
+
jobs =
|
|
150
|
+
connection.execute_prepared(
|
|
151
|
+
:poll_jobs,
|
|
152
|
+
[
|
|
153
|
+
@queue,
|
|
154
|
+
"{#{held_locks.to_a.join(',')}}",
|
|
155
|
+
JSON.dump(priorities),
|
|
156
|
+
]
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
@last_polled_at = Time.now
|
|
160
|
+
@last_poll_satisfied = poll_satisfied?(priorities, jobs)
|
|
161
|
+
|
|
162
|
+
Que.internal_log :poller_polled, self do
|
|
163
|
+
{
|
|
164
|
+
queue: @queue,
|
|
165
|
+
locked: jobs.count,
|
|
166
|
+
priorities: priorities,
|
|
167
|
+
held_locks: held_locks.to_a,
|
|
168
|
+
newly_locked: jobs.map { |key| key.fetch(:id) },
|
|
169
|
+
}
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
jobs.map! { |job| Metajob.new(job) }
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def should_poll?
|
|
176
|
+
# Never polled before?
|
|
177
|
+
last_poll_satisfied.nil? ||
|
|
178
|
+
# Plenty of jobs were available last time?
|
|
179
|
+
last_poll_satisfied == true ||
|
|
180
|
+
poll_interval_elapsed?
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def poll_interval_elapsed?
|
|
184
|
+
return unless interval = poll_interval
|
|
185
|
+
(Time.now - last_polled_at) > interval
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
class << self
|
|
189
|
+
# Manage some temporary infrastructure (specific to the connection) that
|
|
190
|
+
# we'll use for polling. These could easily be created permanently in a
|
|
191
|
+
# migration, but that'd require another migration if we wanted to tweak
|
|
192
|
+
# them later.
|
|
193
|
+
|
|
194
|
+
def setup(connection)
|
|
195
|
+
connection.execute <<-SQL
|
|
196
|
+
-- Temporary composite type we need for our queries to work.
|
|
197
|
+
CREATE TYPE pg_temp.que_query_result AS (
|
|
198
|
+
locked boolean,
|
|
199
|
+
remaining_priorities jsonb
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
CREATE FUNCTION pg_temp.lock_and_update_priorities(priorities jsonb, job que_jobs)
|
|
203
|
+
RETURNS pg_temp.que_query_result
|
|
204
|
+
AS $$
|
|
205
|
+
WITH
|
|
206
|
+
-- Take the lock in a CTE because we want to use the result
|
|
207
|
+
-- multiple times while only taking the lock once.
|
|
208
|
+
lock_taken AS (
|
|
209
|
+
SELECT pg_try_advisory_lock((job).id) AS taken
|
|
210
|
+
),
|
|
211
|
+
relevant AS (
|
|
212
|
+
SELECT priority, count
|
|
213
|
+
FROM (
|
|
214
|
+
SELECT
|
|
215
|
+
key::smallint AS priority,
|
|
216
|
+
value::text::integer AS count
|
|
217
|
+
FROM jsonb_each(priorities)
|
|
218
|
+
) t1
|
|
219
|
+
WHERE priority >= (job).priority
|
|
220
|
+
ORDER BY priority ASC
|
|
221
|
+
LIMIT 1
|
|
222
|
+
)
|
|
223
|
+
SELECT
|
|
224
|
+
(SELECT taken FROM lock_taken), -- R
|
|
225
|
+
CASE (SELECT taken FROM lock_taken)
|
|
226
|
+
WHEN false THEN
|
|
227
|
+
-- Simple case - we couldn't lock the job, so don't update the
|
|
228
|
+
-- priorities hash.
|
|
229
|
+
priorities
|
|
230
|
+
WHEN true THEN
|
|
231
|
+
CASE count
|
|
232
|
+
WHEN 1 THEN
|
|
233
|
+
-- Remove the priority from the JSONB doc entirely, rather
|
|
234
|
+
-- than leaving a zero entry in it.
|
|
235
|
+
priorities - priority::text
|
|
236
|
+
ELSE
|
|
237
|
+
-- Decrement the value in the JSONB doc.
|
|
238
|
+
jsonb_set(
|
|
239
|
+
priorities,
|
|
240
|
+
ARRAY[priority::text],
|
|
241
|
+
to_jsonb(count - 1)
|
|
242
|
+
)
|
|
243
|
+
END
|
|
244
|
+
END
|
|
245
|
+
FROM relevant
|
|
246
|
+
$$
|
|
247
|
+
STABLE
|
|
248
|
+
LANGUAGE SQL;
|
|
249
|
+
|
|
250
|
+
CREATE FUNCTION pg_temp.que_highest_remaining_priority(priorities jsonb) RETURNS smallint AS $$
|
|
251
|
+
SELECT max(key::smallint) FROM jsonb_each(priorities)
|
|
252
|
+
$$
|
|
253
|
+
STABLE
|
|
254
|
+
LANGUAGE SQL;
|
|
255
|
+
SQL
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def cleanup(connection)
|
|
259
|
+
connection.execute <<-SQL
|
|
260
|
+
DROP FUNCTION pg_temp.que_highest_remaining_priority(jsonb);
|
|
261
|
+
DROP FUNCTION pg_temp.lock_and_update_priorities(jsonb, que_jobs);
|
|
262
|
+
DROP TYPE pg_temp.que_query_result;
|
|
263
|
+
SQL
|
|
264
|
+
end
|
|
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
|
|
273
|
+
end
|
|
274
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# A thread-safe queue that holds ids for jobs that have been worked. Allows
|
|
4
|
+
# appending single/retrieving all ids in a thread-safe fashion.
|
|
5
|
+
|
|
6
|
+
module Que
|
|
7
|
+
class ResultQueue
|
|
8
|
+
def initialize
|
|
9
|
+
@array = []
|
|
10
|
+
@mutex = Mutex.new
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def push(item)
|
|
14
|
+
sync { @array.push(item) }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def clear
|
|
18
|
+
sync { @array.pop(@array.size) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_a
|
|
22
|
+
sync { @array.dup }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def length
|
|
26
|
+
sync { @array.length }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def sync(&block)
|
|
32
|
+
@mutex.synchronize(&block)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
::Sequel.extension :pg_json_ops
|
|
4
|
+
|
|
5
|
+
module Que
|
|
6
|
+
module Sequel
|
|
7
|
+
QUALIFIED_TABLE = ::Sequel.qualify(:public, :que_jobs)
|
|
8
|
+
|
|
9
|
+
class Model < ::Sequel::Model(QUALIFIED_TABLE)
|
|
10
|
+
dataset_module do
|
|
11
|
+
conditions = {
|
|
12
|
+
errored: QUALIFIED_TABLE[:error_count] > 0,
|
|
13
|
+
expired: QUALIFIED_TABLE[:expired_at] !~ nil,
|
|
14
|
+
finished: QUALIFIED_TABLE[:finished_at] !~ nil,
|
|
15
|
+
scheduled: QUALIFIED_TABLE[:run_at] > ::Sequel::CURRENT_TIMESTAMP,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
conditions.each do |name, condition|
|
|
19
|
+
subset name, condition
|
|
20
|
+
subset :"not_#{name}", ~condition
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
subset :ready, conditions.values.map(&:~).inject(:&)
|
|
24
|
+
subset :not_ready, conditions.values. inject(:|)
|
|
25
|
+
|
|
26
|
+
def by_job_class(job_class)
|
|
27
|
+
job_class = job_class.name if job_class.is_a?(Class)
|
|
28
|
+
where(
|
|
29
|
+
(QUALIFIED_TABLE[:job_class] =~ job_class) |
|
|
30
|
+
(QUALIFIED_TABLE[:job_class] =~ "ActiveJob::QueueAdapters::QueAdapter::JobWrapper") &
|
|
31
|
+
(QUALIFIED_TABLE[:args].pg_jsonb[0].get_text("job_class") =~ job_class)
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def by_queue(queue)
|
|
36
|
+
where(QUALIFIED_TABLE[:queue] => queue)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def by_tag(tag)
|
|
40
|
+
where(QUALIFIED_TABLE[:data].pg_jsonb.contains(JSON.dump(tags: [tag])))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def by_args(*args, **kwargs)
|
|
44
|
+
where(
|
|
45
|
+
QUALIFIED_TABLE[:args].pg_jsonb.contains(JSON.dump(args)) &
|
|
46
|
+
QUALIFIED_TABLE[:kwargs].pg_jsonb.contains(JSON.dump(kwargs))
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Assertion helpers. Que has a fair amount of internal state, and there's no
|
|
4
|
+
# telling what users will try to throw at it, so for ease of debugging issues it
|
|
5
|
+
# makes sense to sanity-check frequently.
|
|
6
|
+
|
|
7
|
+
module Que
|
|
8
|
+
module Utils
|
|
9
|
+
module Assertions
|
|
10
|
+
class AssertionFailed < Error; end
|
|
11
|
+
|
|
12
|
+
def assert(*args)
|
|
13
|
+
comparison, object, pass = _check_assertion_args(*args)
|
|
14
|
+
return object if pass
|
|
15
|
+
|
|
16
|
+
message =
|
|
17
|
+
if block_given?
|
|
18
|
+
yield.to_s
|
|
19
|
+
elsif comparison
|
|
20
|
+
"Expected #{comparison.inspect}, got #{object.inspect}!"
|
|
21
|
+
else
|
|
22
|
+
"Assertion failed!"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Remove this method from the backtrace, to make errors clearer.
|
|
26
|
+
raise AssertionFailed, message, caller
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def assert?(*args)
|
|
30
|
+
_, _, pass = _check_assertion_args(*args)
|
|
31
|
+
!!pass
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
# Want to support:
|
|
37
|
+
# assert(x) # Truthiness.
|
|
38
|
+
# assert(thing, other) # Trip-equals.
|
|
39
|
+
# assert([thing1, thing2], other) # Multiple Trip-equals.
|
|
40
|
+
def _check_assertion_args(first, second = (second_omitted = true; nil))
|
|
41
|
+
if second_omitted
|
|
42
|
+
comparison = nil
|
|
43
|
+
object = first
|
|
44
|
+
else
|
|
45
|
+
comparison = first
|
|
46
|
+
object = second
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
pass =
|
|
50
|
+
if second_omitted
|
|
51
|
+
object
|
|
52
|
+
elsif comparison.is_a?(Array)
|
|
53
|
+
comparison.any? { |k| k === object }
|
|
54
|
+
else
|
|
55
|
+
comparison === object
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
[comparison, object, pass]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Que
|
|
4
|
+
module Utils
|
|
5
|
+
module Constantization
|
|
6
|
+
def constantize(string)
|
|
7
|
+
Que.assert String, string
|
|
8
|
+
|
|
9
|
+
if string.respond_to?(:constantize)
|
|
10
|
+
string.constantize
|
|
11
|
+
else
|
|
12
|
+
names = string.split('::')
|
|
13
|
+
names.reject!(&:empty?)
|
|
14
|
+
names.inject(Object, &:const_get)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Que
|
|
4
|
+
module Utils
|
|
5
|
+
module ErrorNotification
|
|
6
|
+
attr_accessor :error_notifier
|
|
7
|
+
|
|
8
|
+
def notify_error(*args)
|
|
9
|
+
Que.internal_log(:error_notification_attempted) do
|
|
10
|
+
{args: args.inspect}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
if notifier = error_notifier
|
|
14
|
+
arity = notifier.arity
|
|
15
|
+
args = args.first(arity) if arity >= 0
|
|
16
|
+
|
|
17
|
+
notifier.call(*args)
|
|
18
|
+
end
|
|
19
|
+
rescue => error
|
|
20
|
+
Que.log(
|
|
21
|
+
event: :error_notifier_failed,
|
|
22
|
+
level: :error,
|
|
23
|
+
message: "error_notifier callable raised an error",
|
|
24
|
+
|
|
25
|
+
error_class: error.class.name,
|
|
26
|
+
error_message: error.message,
|
|
27
|
+
error_backtrace: error.backtrace,
|
|
28
|
+
)
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
ASYNC_QUEUE = Queue.new
|
|
33
|
+
MAX_QUEUE_SIZE = 5
|
|
34
|
+
|
|
35
|
+
# Helper method to notify errors asynchronously. For use in high-priority
|
|
36
|
+
# code, where we don't want to be held up by whatever I/O the error
|
|
37
|
+
# notification proc contains.
|
|
38
|
+
def notify_error_async(*args)
|
|
39
|
+
# We don't synchronize around the size check and the push, so there's a
|
|
40
|
+
# race condition where the queue could grow to more than the maximum
|
|
41
|
+
# number of errors, but no big deal if it does. The size check is mainly
|
|
42
|
+
# here to ensure that the error queue doesn't grow unboundedly large in
|
|
43
|
+
# pathological cases.
|
|
44
|
+
|
|
45
|
+
if ASYNC_QUEUE.size < MAX_QUEUE_SIZE
|
|
46
|
+
ASYNC_QUEUE.push(args)
|
|
47
|
+
# Puma raises some ugly warnings if you start up a new thread in the
|
|
48
|
+
# background during initialization, so start the async error-reporting
|
|
49
|
+
# thread lazily.
|
|
50
|
+
async_error_thread
|
|
51
|
+
true
|
|
52
|
+
else
|
|
53
|
+
false
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def async_error_thread
|
|
58
|
+
CONFIG_MUTEX.synchronize do
|
|
59
|
+
@async_error_thread ||=
|
|
60
|
+
Thread.new do
|
|
61
|
+
Thread.current.abort_on_exception = true
|
|
62
|
+
loop { Que.notify_error(*ASYNC_QUEUE.pop) }
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Helper method for recursively freezing a data structure.
|
|
4
|
+
|
|
5
|
+
module Que
|
|
6
|
+
module Utils
|
|
7
|
+
module Freeze
|
|
8
|
+
def recursively_freeze(thing)
|
|
9
|
+
case thing
|
|
10
|
+
when Array
|
|
11
|
+
thing.each { |e| recursively_freeze(e) }
|
|
12
|
+
when Hash
|
|
13
|
+
thing.each { |k, v| recursively_freeze(k); recursively_freeze(v) }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
thing.freeze
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Tools for introspecting the state of the job queue.
|
|
4
|
+
|
|
5
|
+
module Que
|
|
6
|
+
module Utils
|
|
7
|
+
module Introspection
|
|
8
|
+
SQL[:job_stats] =
|
|
9
|
+
%{
|
|
10
|
+
SELECT job_class,
|
|
11
|
+
count(*) AS count,
|
|
12
|
+
count(locks.id) AS count_working,
|
|
13
|
+
sum((error_count > 0)::int) AS count_errored,
|
|
14
|
+
max(error_count) AS highest_error_count,
|
|
15
|
+
min(run_at) AS oldest_run_at
|
|
16
|
+
FROM public.que_jobs
|
|
17
|
+
LEFT JOIN (
|
|
18
|
+
SELECT (classid::bigint << 32) + objid::bigint AS id
|
|
19
|
+
FROM pg_locks
|
|
20
|
+
WHERE locktype = 'advisory'
|
|
21
|
+
) locks USING (id)
|
|
22
|
+
WHERE finished_at IS NULL AND expired_at IS NULL
|
|
23
|
+
GROUP BY job_class
|
|
24
|
+
ORDER BY count(*) DESC
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
def job_stats
|
|
28
|
+
execute :job_stats
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
SQL[:job_states] =
|
|
32
|
+
%{
|
|
33
|
+
SELECT que_jobs.*,
|
|
34
|
+
pg.ruby_hostname,
|
|
35
|
+
pg.ruby_pid
|
|
36
|
+
FROM public.que_jobs
|
|
37
|
+
JOIN (
|
|
38
|
+
SELECT (classid::bigint << 32) + objid::bigint AS id, que_lockers.*
|
|
39
|
+
FROM pg_locks
|
|
40
|
+
JOIN public.que_lockers USING (pid)
|
|
41
|
+
WHERE locktype = 'advisory'
|
|
42
|
+
) pg USING (id)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
def job_states
|
|
46
|
+
execute :job_states
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Logic for serializing to/from JSON. We assume that the standard library's JSON
|
|
4
|
+
# module is good enough for our purposes.
|
|
5
|
+
|
|
6
|
+
require 'json'
|
|
7
|
+
|
|
8
|
+
module Que
|
|
9
|
+
module Utils
|
|
10
|
+
module JSONSerialization
|
|
11
|
+
def serialize_json(object)
|
|
12
|
+
JSON.dump(object)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def deserialize_json(json)
|
|
16
|
+
# Allowing `create_additions` would be a security vulnerability.
|
|
17
|
+
JSON.parse(json, symbolize_names: true, create_additions: false)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Tools for logging from Que.
|
|
4
|
+
|
|
5
|
+
module Que
|
|
6
|
+
module Utils
|
|
7
|
+
module Logging
|
|
8
|
+
attr_accessor :logger, :internal_logger
|
|
9
|
+
attr_writer :log_formatter
|
|
10
|
+
|
|
11
|
+
def log(event:, level: :info, **extra)
|
|
12
|
+
data = _default_log_data
|
|
13
|
+
data[:event] = Que.assert(Symbol, event)
|
|
14
|
+
data.merge!(extra)
|
|
15
|
+
|
|
16
|
+
if l = get_logger
|
|
17
|
+
begin
|
|
18
|
+
if output = log_formatter.call(data)
|
|
19
|
+
l.send level, output
|
|
20
|
+
end
|
|
21
|
+
rescue => e
|
|
22
|
+
msg =
|
|
23
|
+
"Error raised from Que.log_formatter proc:" +
|
|
24
|
+
" #{e.class}: #{e.message}\n#{e.backtrace}"
|
|
25
|
+
|
|
26
|
+
l.error(msg)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Logging method used specifically to instrument Que's internals. There's
|
|
32
|
+
# usually not an internal logger set up, so this method is generally a no-
|
|
33
|
+
# op unless the specs are running or someone turns on internal logging so
|
|
34
|
+
# we can debug an issue.
|
|
35
|
+
def internal_log(event, object = nil)
|
|
36
|
+
if l = get_logger(internal: true)
|
|
37
|
+
data = _default_log_data
|
|
38
|
+
|
|
39
|
+
data[:internal_event] = Que.assert(Symbol, event)
|
|
40
|
+
data[:object_id] = object.object_id if object
|
|
41
|
+
data[:t] = Time.now.utc.iso8601(6)
|
|
42
|
+
|
|
43
|
+
additional = Que.assert(Hash, yield)
|
|
44
|
+
|
|
45
|
+
# Make sure that none of our log contents accidentally overwrite our
|
|
46
|
+
# default data contents.
|
|
47
|
+
expected_length = data.length + additional.length
|
|
48
|
+
data.merge!(additional)
|
|
49
|
+
Que.assert(expected_length == data.length) do
|
|
50
|
+
"Bad internal logging keys in: #{additional.keys.inspect}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
l.info(JSON.dump(data))
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def get_logger(internal: false)
|
|
58
|
+
if l = internal ? internal_logger : logger
|
|
59
|
+
l.respond_to?(:call) ? l.call : l
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def log_formatter
|
|
64
|
+
@log_formatter ||= JSON.method(:dump)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def _default_log_data
|
|
70
|
+
{
|
|
71
|
+
lib: :que,
|
|
72
|
+
hostname: CURRENT_HOSTNAME,
|
|
73
|
+
pid: Process.pid,
|
|
74
|
+
thread: Thread.current.object_id,
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|