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
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Logic for middleware to wrap jobs.
|
4
|
+
|
5
|
+
module Que
|
6
|
+
module Utils
|
7
|
+
module Middleware
|
8
|
+
TYPES = [
|
9
|
+
:job,
|
10
|
+
:sql,
|
11
|
+
].freeze
|
12
|
+
|
13
|
+
TYPES.each do |type|
|
14
|
+
module_eval <<-CODE
|
15
|
+
def #{type}_middleware
|
16
|
+
@#{type}_middleware ||= []
|
17
|
+
end
|
18
|
+
|
19
|
+
def run_#{type}_middleware(*args)
|
20
|
+
m = #{type}_middleware
|
21
|
+
|
22
|
+
if m.empty?
|
23
|
+
yield
|
24
|
+
else
|
25
|
+
invoke_middleware(middleware: m.dup, args: args) { yield }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
CODE
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def invoke_middleware(middleware:, args:, &block)
|
34
|
+
if m = middleware.shift
|
35
|
+
r = nil
|
36
|
+
m.call(*args) do
|
37
|
+
r = invoke_middleware(middleware: middleware, args: args, &block)
|
38
|
+
end
|
39
|
+
r
|
40
|
+
else
|
41
|
+
yield
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Tools for managing the contents/state of the queue.
|
4
|
+
|
5
|
+
module Que
|
6
|
+
module Utils
|
7
|
+
module QueueManagement
|
8
|
+
def clear!
|
9
|
+
execute "DELETE FROM que_jobs"
|
10
|
+
end
|
11
|
+
|
12
|
+
# Very old migrations may use Que.create! and Que.drop!, which just
|
13
|
+
# created and dropped the initial version of the jobs table.
|
14
|
+
def create!; migrate!(version: 1); end
|
15
|
+
def drop!; migrate!(version: 0); end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Temporary module allowing ruby2 keyword args to be extracted from an *args splat
|
4
|
+
# Allows us to ensure consistent behaviour when running on ruby 2 vs ruby 3
|
5
|
+
# We can remove this if/when we drop support for ruby 2
|
6
|
+
|
7
|
+
require 'json'
|
8
|
+
|
9
|
+
module Que
|
10
|
+
module Utils
|
11
|
+
module Ruby2Keywords
|
12
|
+
def split_out_ruby2_keywords(args)
|
13
|
+
return [args, {}] unless args.last&.is_a?(Hash) && Hash.ruby2_keywords_hash?(args.last)
|
14
|
+
|
15
|
+
[args[0..-2], args.last]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# A helper method to manage transactions, used mainly by the migration system.
|
4
|
+
# It's available for general use, but if you're using an ORM that provides its
|
5
|
+
# own transaction helper, be sure to use that instead, or the two may interfere
|
6
|
+
# with one another.
|
7
|
+
|
8
|
+
module Que
|
9
|
+
module Utils
|
10
|
+
module Transactions
|
11
|
+
def transaction
|
12
|
+
pool.checkout do
|
13
|
+
if pool.in_transaction?
|
14
|
+
yield
|
15
|
+
else
|
16
|
+
begin
|
17
|
+
execute "BEGIN"
|
18
|
+
yield
|
19
|
+
rescue => error
|
20
|
+
raise
|
21
|
+
ensure
|
22
|
+
# Handle a raised error or a killed thread.
|
23
|
+
if error || Thread.current.status == 'aborting'
|
24
|
+
execute "ROLLBACK"
|
25
|
+
else
|
26
|
+
execute "COMMIT"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/que/version.rb
CHANGED
data/lib/que/worker.rb
CHANGED
@@ -1,184 +1,180 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
# Workers wrap threads which continuously pull job pks from JobBuffer objects,
|
4
|
+
# fetch and work those jobs, and export relevant data to ResultQueues.
|
5
|
+
|
6
|
+
require 'set'
|
4
7
|
|
5
8
|
module Que
|
6
9
|
class Worker
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
def wake!
|
36
|
-
synchronize do
|
37
|
-
if sleeping?
|
38
|
-
# Have to set the state here so that another thread checking
|
39
|
-
# immediately after this won't see the worker as asleep.
|
40
|
-
@state = :working
|
41
|
-
@thread.wakeup
|
42
|
-
true
|
43
|
-
end
|
10
|
+
attr_reader :thread, :priority
|
11
|
+
|
12
|
+
VALID_LOG_LEVELS = [:debug, :info, :warn, :error, :fatal, :unknown].to_set.freeze
|
13
|
+
|
14
|
+
SQL[:check_job] =
|
15
|
+
%{
|
16
|
+
SELECT 1 AS one
|
17
|
+
FROM public.que_jobs
|
18
|
+
WHERE id = $1::bigint
|
19
|
+
}
|
20
|
+
|
21
|
+
def initialize(
|
22
|
+
job_buffer:,
|
23
|
+
result_queue:,
|
24
|
+
priority: nil,
|
25
|
+
start_callback: nil
|
26
|
+
)
|
27
|
+
|
28
|
+
@priority = Que.assert([NilClass, Integer], priority)
|
29
|
+
@job_buffer = Que.assert(JobBuffer, job_buffer)
|
30
|
+
@result_queue = Que.assert(ResultQueue, result_queue)
|
31
|
+
|
32
|
+
Que.internal_log(:worker_instantiate, self) do
|
33
|
+
{
|
34
|
+
priority: priority,
|
35
|
+
job_buffer: job_buffer.object_id,
|
36
|
+
result_queue: result_queue.object_id,
|
37
|
+
}
|
44
38
|
end
|
45
|
-
end
|
46
39
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
40
|
+
@thread =
|
41
|
+
Thread.new do
|
42
|
+
# An error causing this thread to exit is a bug in Que, which we want
|
43
|
+
# to know about ASAP, so propagate the error if it happens.
|
44
|
+
Thread.current.abort_on_exception = true
|
45
|
+
start_callback.call(self) if start_callback.respond_to?(:call)
|
46
|
+
work_loop
|
47
|
+
end
|
51
48
|
end
|
52
49
|
|
53
50
|
def wait_until_stopped
|
54
|
-
|
51
|
+
@thread.join
|
55
52
|
end
|
56
53
|
|
57
54
|
private
|
58
55
|
|
59
|
-
# Sleep very briefly while waiting for a thread to get somewhere.
|
60
|
-
def wait
|
61
|
-
sleep 0.0001
|
62
|
-
end
|
63
|
-
|
64
|
-
def _sleeping?
|
65
|
-
if @state == :sleeping
|
66
|
-
# There's a very small period of time between when the Worker marks
|
67
|
-
# itself as sleeping and when it actually goes to sleep. Only report
|
68
|
-
# true when we're certain the thread is sleeping.
|
69
|
-
wait until @thread.status == 'sleep'
|
70
|
-
true
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
56
|
def work_loop
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
Que.log(result)
|
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
|
60
|
+
# lets the thread finish.
|
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?
|
68
|
+
id = metajob.id
|
69
|
+
|
70
|
+
Que.internal_log(:worker_received_job, self) { {id: id} }
|
71
|
+
|
72
|
+
if Que.execute(:check_job, [id]).first
|
73
|
+
Que.recursively_freeze(metajob.job)
|
74
|
+
Que.internal_log(:worker_fetched_job, self) { {id: id} }
|
75
|
+
|
76
|
+
work_job(metajob)
|
77
|
+
else
|
78
|
+
# The job was locked but doesn't exist anymore, due to a race
|
79
|
+
# condition that exists because advisory locks don't obey MVCC. Not
|
80
|
+
# necessarily a problem, but if it happens a lot it may be meaningful.
|
81
|
+
Que.internal_log(:worker_job_lock_race_condition, self) { {id: id} }
|
102
82
|
end
|
103
83
|
|
104
|
-
|
105
|
-
|
106
|
-
|
84
|
+
Que.internal_log(:worker_pushing_finished_job, self) { {id: id} }
|
85
|
+
|
86
|
+
@result_queue.push(
|
87
|
+
metajob: metajob,
|
88
|
+
message_type: :job_finished,
|
89
|
+
)
|
107
90
|
end
|
108
|
-
ensure
|
109
|
-
@state = :stopped
|
110
91
|
end
|
111
92
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
# a worker, and make sure to wake up the wrangler when @wake_interval is
|
116
|
-
# changed in Que.wake_interval= below.
|
117
|
-
@wake_interval = 5
|
118
|
-
|
119
|
-
# Four workers is a sensible default for most use cases.
|
120
|
-
@worker_count = 4
|
93
|
+
def fetch_next_metajob
|
94
|
+
@job_buffer.shift(*priority)
|
95
|
+
end
|
121
96
|
|
122
|
-
|
123
|
-
|
124
|
-
|
97
|
+
def work_job(metajob)
|
98
|
+
job = metajob.job
|
99
|
+
start = Time.now
|
100
|
+
klass = Que.constantize(job.fetch(:job_class))
|
101
|
+
instance = klass.new(job)
|
125
102
|
|
126
|
-
|
127
|
-
# worker_count and wake_interval settings without actually instantiating
|
128
|
-
# the relevant threads until the mode is actually set to :async in a
|
129
|
-
# post-fork hook (since forking will kill any running background threads).
|
103
|
+
Que.run_job_middleware(instance) { instance.tap(&:_run) }
|
130
104
|
|
131
|
-
|
132
|
-
Que.log :event => 'mode_change', :value => mode.to_s
|
133
|
-
@mode = mode
|
105
|
+
elapsed = Time.now - start
|
134
106
|
|
135
|
-
|
136
|
-
|
137
|
-
|
107
|
+
log_level =
|
108
|
+
if instance.que_error
|
109
|
+
:error
|
110
|
+
else
|
111
|
+
instance.log_level(elapsed)
|
138
112
|
end
|
139
|
-
end
|
140
|
-
|
141
|
-
def worker_count=(count)
|
142
|
-
Que.log :event => 'worker_count_change', :value => count.to_s
|
143
|
-
@worker_count = count
|
144
|
-
set_up_workers if mode == :async
|
145
|
-
end
|
146
|
-
|
147
|
-
def workers
|
148
|
-
@workers ||= []
|
149
|
-
end
|
150
|
-
|
151
|
-
def wake_interval=(interval)
|
152
|
-
@wake_interval = interval
|
153
|
-
wrangler.wakeup if mode == :async
|
154
|
-
end
|
155
113
|
|
156
|
-
|
157
|
-
|
158
|
-
|
114
|
+
if VALID_LOG_LEVELS.include?(log_level)
|
115
|
+
log_message = {
|
116
|
+
level: log_level,
|
117
|
+
job_id: metajob.id,
|
118
|
+
elapsed: elapsed,
|
119
|
+
}
|
120
|
+
|
121
|
+
if error = instance.que_error
|
122
|
+
log_message[:event] = :job_errored
|
123
|
+
log_message[:error] = "#{error.class}: #{error.message}".slice(0, 500)
|
124
|
+
else
|
125
|
+
log_message[:event] = :job_worked
|
126
|
+
end
|
159
127
|
|
160
|
-
|
161
|
-
workers.each(&:wake!)
|
128
|
+
Que.log(**log_message)
|
162
129
|
end
|
163
130
|
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
131
|
+
instance
|
132
|
+
rescue => error
|
133
|
+
Que.log(
|
134
|
+
level: :debug,
|
135
|
+
event: :job_errored,
|
136
|
+
job_id: metajob.id,
|
137
|
+
error: {
|
138
|
+
class: error.class.to_s,
|
139
|
+
message: error.message,
|
140
|
+
backtrace: (error.backtrace || []).join("\n").slice(0, 10000),
|
141
|
+
},
|
142
|
+
)
|
143
|
+
|
144
|
+
Que.notify_error(error)
|
145
|
+
|
146
|
+
begin
|
147
|
+
# If the Job class couldn't be resolved, use the default retry
|
148
|
+
# backoff logic in Que::Job.
|
149
|
+
job_class = (klass && klass <= Job) ? klass : Job
|
150
|
+
|
151
|
+
error_count = job.fetch(:error_count) + 1
|
152
|
+
|
153
|
+
max_retry_count = job_class.resolve_que_setting(:maximum_retry_count)
|
154
|
+
|
155
|
+
if max_retry_count && error_count > max_retry_count
|
156
|
+
Que.execute :expire_job, [job.fetch(:id)]
|
157
|
+
else
|
158
|
+
delay =
|
159
|
+
job_class.
|
160
|
+
resolve_que_setting(
|
161
|
+
:retry_interval,
|
162
|
+
error_count,
|
163
|
+
)
|
164
|
+
|
165
|
+
Que.execute :set_error, [
|
166
|
+
delay,
|
167
|
+
"#{error.class}: #{error.message}".slice(0, 500),
|
168
|
+
(error.backtrace || []).join("\n").slice(0, 10000),
|
169
|
+
job.fetch(:id),
|
170
|
+
]
|
171
171
|
end
|
172
|
+
rescue
|
173
|
+
# If we can't reach the database for some reason, too bad, but
|
174
|
+
# don't let it crash the work loop.
|
172
175
|
end
|
173
176
|
|
174
|
-
|
175
|
-
@wrangler ||= Thread.new do
|
176
|
-
loop do
|
177
|
-
sleep(*@wake_interval)
|
178
|
-
wake! if @wake_interval && mode == :async
|
179
|
-
end
|
180
|
-
end
|
181
|
-
end
|
177
|
+
error
|
182
178
|
end
|
183
179
|
end
|
184
180
|
end
|