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,176 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Que
|
4
|
+
SQL[:finish_job] =
|
5
|
+
%{
|
6
|
+
UPDATE public.que_jobs
|
7
|
+
SET finished_at = now()
|
8
|
+
WHERE id = $1::bigint
|
9
|
+
}
|
10
|
+
|
11
|
+
SQL[:expire_job] =
|
12
|
+
%{
|
13
|
+
UPDATE public.que_jobs
|
14
|
+
SET error_count = error_count + 1,
|
15
|
+
expired_at = now()
|
16
|
+
WHERE id = $1::bigint
|
17
|
+
}
|
18
|
+
|
19
|
+
SQL[:destroy_job] =
|
20
|
+
%{
|
21
|
+
DELETE FROM public.que_jobs
|
22
|
+
WHERE id = $1::bigint
|
23
|
+
}
|
24
|
+
|
25
|
+
SQL[:set_error] =
|
26
|
+
%{
|
27
|
+
UPDATE public.que_jobs
|
28
|
+
SET error_count = error_count + 1,
|
29
|
+
run_at = now() + $1::float * '1 second'::interval,
|
30
|
+
last_error_message = left($2::text, 500),
|
31
|
+
last_error_backtrace = left($3::text, 10000)
|
32
|
+
WHERE id = $4::bigint
|
33
|
+
}
|
34
|
+
|
35
|
+
module JobMethods
|
36
|
+
# Note that we delegate almost all methods to the result of the que_target
|
37
|
+
# method, which could be one of a few things, depending on the circumstance.
|
38
|
+
|
39
|
+
# Run the job with error handling and cleanup logic. Optionally support
|
40
|
+
# overriding the args, because it's necessary when jobs are invoked from
|
41
|
+
# ActiveJob.
|
42
|
+
def _run(args: nil, kwargs: nil, reraise_errors: false)
|
43
|
+
if args.nil? && que_target
|
44
|
+
args = que_target.que_attrs.fetch(:args)
|
45
|
+
end
|
46
|
+
|
47
|
+
if kwargs.nil? && que_target
|
48
|
+
kwargs = que_target.que_attrs.fetch(:kwargs)
|
49
|
+
end
|
50
|
+
|
51
|
+
run(*args, **kwargs)
|
52
|
+
default_resolve_action if que_target && !que_target.que_resolved
|
53
|
+
rescue => error
|
54
|
+
raise error unless que_target
|
55
|
+
|
56
|
+
que_target.que_error = error
|
57
|
+
|
58
|
+
run_error_notifier =
|
59
|
+
begin
|
60
|
+
handle_error(error)
|
61
|
+
rescue => error_2
|
62
|
+
Que.notify_error(error_2, que_target.que_attrs)
|
63
|
+
true
|
64
|
+
end
|
65
|
+
|
66
|
+
Que.notify_error(error, que_target.que_attrs) if run_error_notifier
|
67
|
+
retry_in_default_interval unless que_target.que_resolved
|
68
|
+
|
69
|
+
raise error if reraise_errors
|
70
|
+
end
|
71
|
+
|
72
|
+
def log_level(elapsed)
|
73
|
+
:debug
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
# This method defines the object on which the various job helper methods are
|
79
|
+
# acting. When using Que in the default configuration this will just be
|
80
|
+
# self, but when using the Que adapter for ActiveJob it'll be the actual
|
81
|
+
# underlying job object. When running an ActiveJob::Base subclass that
|
82
|
+
# includes this module through a separate adapter this will be nil - hence,
|
83
|
+
# the defensive coding in every method that no-ops if que_target is falsy.
|
84
|
+
def que_target
|
85
|
+
raise NotImplementedError
|
86
|
+
end
|
87
|
+
|
88
|
+
def resolve_que_setting(*args)
|
89
|
+
return unless que_target
|
90
|
+
|
91
|
+
que_target.class.resolve_que_setting(*args)
|
92
|
+
end
|
93
|
+
|
94
|
+
def default_resolve_action
|
95
|
+
return unless que_target
|
96
|
+
|
97
|
+
destroy
|
98
|
+
end
|
99
|
+
|
100
|
+
def expire
|
101
|
+
return unless que_target
|
102
|
+
|
103
|
+
if id = que_target.que_attrs[:id]
|
104
|
+
Que.execute :expire_job, [id]
|
105
|
+
end
|
106
|
+
|
107
|
+
que_target.que_resolved = true
|
108
|
+
end
|
109
|
+
|
110
|
+
def finish
|
111
|
+
return unless que_target
|
112
|
+
|
113
|
+
if id = que_target.que_attrs[:id]
|
114
|
+
Que.execute :finish_job, [id]
|
115
|
+
end
|
116
|
+
|
117
|
+
que_target.que_resolved = true
|
118
|
+
end
|
119
|
+
|
120
|
+
def error_count
|
121
|
+
return 0 unless que_target
|
122
|
+
|
123
|
+
count = que_target.que_attrs.fetch(:error_count)
|
124
|
+
que_target.que_error ? count + 1 : count
|
125
|
+
end
|
126
|
+
|
127
|
+
# To be overridden in subclasses.
|
128
|
+
def handle_error(error)
|
129
|
+
return unless que_target
|
130
|
+
|
131
|
+
max = resolve_que_setting(:maximum_retry_count)
|
132
|
+
|
133
|
+
if max && error_count > max
|
134
|
+
expire
|
135
|
+
else
|
136
|
+
retry_in_default_interval
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def retry_in_default_interval
|
141
|
+
return unless que_target
|
142
|
+
|
143
|
+
retry_in(resolve_que_setting(:retry_interval, error_count))
|
144
|
+
end
|
145
|
+
|
146
|
+
# Explicitly check for the job id in these helpers, because it won't exist
|
147
|
+
# if we're running synchronously.
|
148
|
+
def retry_in(period)
|
149
|
+
return unless que_target
|
150
|
+
|
151
|
+
if id = que_target.que_attrs[:id]
|
152
|
+
values = [period]
|
153
|
+
|
154
|
+
if e = que_target.que_error
|
155
|
+
values << "#{e.class}: #{e.message}".slice(0, 500) << e.backtrace.join("\n").slice(0, 10000)
|
156
|
+
else
|
157
|
+
values << nil << nil
|
158
|
+
end
|
159
|
+
|
160
|
+
Que.execute :set_error, values << id
|
161
|
+
end
|
162
|
+
|
163
|
+
que_target.que_resolved = true
|
164
|
+
end
|
165
|
+
|
166
|
+
def destroy
|
167
|
+
return unless que_target
|
168
|
+
|
169
|
+
if id = que_target.que_attrs[:id]
|
170
|
+
Que.execute :destroy_job, [id]
|
171
|
+
end
|
172
|
+
|
173
|
+
que_target.que_resolved = true
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
data/lib/que/listener.rb
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Que
|
4
|
+
class Listener
|
5
|
+
MESSAGE_FORMATS = {}
|
6
|
+
|
7
|
+
attr_reader :connection, :channel
|
8
|
+
|
9
|
+
def initialize(connection:, channel: nil)
|
10
|
+
@connection = connection
|
11
|
+
@channel = channel || "que_listener_#{connection.backend_pid}"
|
12
|
+
|
13
|
+
Que.internal_log :listener_instantiate, self do
|
14
|
+
{
|
15
|
+
backend_pid: connection.backend_pid,
|
16
|
+
}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def listen
|
21
|
+
connection.execute "LISTEN #{channel}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def wait_for_grouped_messages(timeout)
|
25
|
+
messages = wait_for_messages(timeout)
|
26
|
+
|
27
|
+
output = {}
|
28
|
+
|
29
|
+
messages.each do |message|
|
30
|
+
message_type = message.delete(:message_type)
|
31
|
+
|
32
|
+
(output[message_type.to_sym] ||= []) << message.freeze
|
33
|
+
end
|
34
|
+
|
35
|
+
output
|
36
|
+
end
|
37
|
+
|
38
|
+
def wait_for_messages(timeout)
|
39
|
+
# Make sure we never pass nil to this method, so we don't hang the thread.
|
40
|
+
Que.assert(Numeric, timeout)
|
41
|
+
|
42
|
+
Que.internal_log :listener_waiting, self do
|
43
|
+
{
|
44
|
+
backend_pid: connection.backend_pid,
|
45
|
+
channel: channel,
|
46
|
+
timeout: timeout,
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
accumulated_messages = []
|
51
|
+
|
52
|
+
# Notifications often come in batches (especially when a transaction that
|
53
|
+
# inserted many jobs commits), so we want to loop and pick up all the
|
54
|
+
# received notifications before continuing.
|
55
|
+
loop do
|
56
|
+
notification_received =
|
57
|
+
connection.wait_for_notify(timeout) do |channel, pid, payload|
|
58
|
+
# We've received a notification, so zero out the timeout before we
|
59
|
+
# loop again to check for another message. This ensures that we
|
60
|
+
# don't wait an additional `timeout` seconds after processing the
|
61
|
+
# final message before this method returns.
|
62
|
+
timeout = 0
|
63
|
+
|
64
|
+
Que.internal_log(:listener_received_notification, self) do
|
65
|
+
{
|
66
|
+
channel: channel,
|
67
|
+
backend_pid: connection.backend_pid,
|
68
|
+
source_pid: pid,
|
69
|
+
payload: payload,
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
# Be very defensive about the message we receive - it may not be
|
74
|
+
# valid JSON or have the structure we expect.
|
75
|
+
next unless message = parse_payload(payload)
|
76
|
+
|
77
|
+
case message
|
78
|
+
when Array then accumulated_messages.concat(message)
|
79
|
+
when Hash then accumulated_messages << message
|
80
|
+
else raise Error, "Unexpected parse_payload output: #{message.class}"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
break unless notification_received
|
85
|
+
end
|
86
|
+
|
87
|
+
return accumulated_messages if accumulated_messages.empty?
|
88
|
+
|
89
|
+
Que.internal_log(:listener_received_messages, self) do
|
90
|
+
{
|
91
|
+
backend_pid: connection.backend_pid,
|
92
|
+
channel: channel,
|
93
|
+
messages: accumulated_messages,
|
94
|
+
}
|
95
|
+
end
|
96
|
+
|
97
|
+
accumulated_messages.keep_if do |message|
|
98
|
+
next unless message.is_a?(Hash)
|
99
|
+
next unless type = message[:message_type]
|
100
|
+
next unless type.is_a?(String)
|
101
|
+
next unless format = MESSAGE_FORMATS[type.to_sym]
|
102
|
+
|
103
|
+
if message_matches_format?(message, format)
|
104
|
+
true
|
105
|
+
else
|
106
|
+
error_message = [
|
107
|
+
"Message of type '#{type}' doesn't match format!",
|
108
|
+
# Massage message and format a bit to make these errors more readable.
|
109
|
+
"Message: #{Hash[message.reject{|k,v| k == :message_type}.sort_by{|k,v| k}].inspect}",
|
110
|
+
"Format: #{Hash[format.sort_by{|k,v| k}].inspect}",
|
111
|
+
].join("\n")
|
112
|
+
|
113
|
+
Que.notify_error_async(Error.new(error_message))
|
114
|
+
false
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
Que.internal_log(:listener_filtered_messages, self) do
|
119
|
+
{
|
120
|
+
backend_pid: connection.backend_pid,
|
121
|
+
channel: channel,
|
122
|
+
messages: accumulated_messages,
|
123
|
+
}
|
124
|
+
end
|
125
|
+
|
126
|
+
accumulated_messages
|
127
|
+
end
|
128
|
+
|
129
|
+
def unlisten
|
130
|
+
# Be sure to drain all notifications so that any code that uses this
|
131
|
+
# connection later doesn't receive any nasty surprises.
|
132
|
+
connection.execute "UNLISTEN *"
|
133
|
+
connection.drain_notifications
|
134
|
+
|
135
|
+
Que.internal_log :listener_unlisten, self do
|
136
|
+
{
|
137
|
+
backend_pid: connection.backend_pid,
|
138
|
+
channel: channel,
|
139
|
+
}
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
private
|
144
|
+
|
145
|
+
def parse_payload(payload)
|
146
|
+
Que.deserialize_json(payload)
|
147
|
+
rescue JSON::ParserError => e
|
148
|
+
Que.notify_error_async(e)
|
149
|
+
nil
|
150
|
+
end
|
151
|
+
|
152
|
+
def message_matches_format?(message, format)
|
153
|
+
message_has_all_keys?(message, format) &&
|
154
|
+
message_has_no_excess_keys?(message, format) &&
|
155
|
+
message_keys_all_valid?(message, format)
|
156
|
+
end
|
157
|
+
|
158
|
+
def message_has_all_keys?(message, format)
|
159
|
+
format.all? { |k,v| message.has_key?(k) }
|
160
|
+
end
|
161
|
+
|
162
|
+
def message_has_no_excess_keys?(message, format)
|
163
|
+
message.all? { |k,v| format.has_key?(k) || k == :message_type }
|
164
|
+
end
|
165
|
+
|
166
|
+
def message_keys_all_valid?(message, format)
|
167
|
+
message.all? do |key, value|
|
168
|
+
if type = format[key]
|
169
|
+
Que.assert?(type, value)
|
170
|
+
else
|
171
|
+
true
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|