aws-activejob-sqs 1.1.0 → 1.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 +4 -4
- data/CHANGELOG.md +12 -0
- data/VERSION +1 -1
- data/lib/active_job/queue_adapters/sqs_adapter.rb +7 -5
- data/lib/aws/active_job/sqs/configuration.rb +24 -5
- data/lib/aws/active_job/sqs/executor.rb +45 -8
- data/lib/aws/active_job/sqs/job_runner.rb +19 -4
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 87f80b0bcacdf1a7121b918c0e126427f98ff530492ba1b086886e04de4e58e3
|
|
4
|
+
data.tar.gz: 29c3445f74019e920a6b3aa8999f756ea728bc5a2200fd56927e31eca2ed51dc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5f6f6856fbf896f9af13fa6c03e24d0de80f18ef4fe0edf757e8e952b334ed71e14130a6a340025114380049ff5d605678168d207cbf367bee74d250d933542e
|
|
7
|
+
data.tar.gz: d3efaa6e454968548d296ab448ad5e18c0940ce95b2969cafa9ced9703b91dbf040534bcc07591ba52961d4732b7ed3777c35eebdd24cd6835e9a76e9b32e712
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
1.2.0 (2026-08-17)
|
|
2
|
+
------------------
|
|
3
|
+
|
|
4
|
+
* Feature - Report a dropped job to the Rails error reporter and add an optional `permanent_failure_handler` so apps can keep unresolvable jobs on the queue for redelivery instead of losing them (#39).
|
|
5
|
+
|
|
6
|
+
* Issue - `perform_all_later` no longer partially enqueues a batch before raising when one of the jobs has a delay on a FIFO queue (#40).
|
|
7
|
+
|
|
8
|
+
1.1.1 (2026-08-12)
|
|
9
|
+
------------------
|
|
10
|
+
|
|
11
|
+
* Issue - Fixed an empty or blank `job_class_allowlist` causing every job to be rejected and deleted. A blank allowlist is now treated as no allowlist, so all jobs run.
|
|
12
|
+
|
|
1
13
|
1.1.0 (2026-08-11)
|
|
2
14
|
------------------
|
|
3
15
|
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.
|
|
1
|
+
1.2.0
|
|
@@ -26,6 +26,12 @@ module ActiveJob
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def enqueue_all(jobs)
|
|
29
|
+
jobs.each do |job|
|
|
30
|
+
next unless job.scheduled_at
|
|
31
|
+
|
|
32
|
+
validate_fifo_delay!(job, Params.assured_delay_seconds(job.scheduled_at))
|
|
33
|
+
end
|
|
34
|
+
|
|
29
35
|
enqueued_count = 0
|
|
30
36
|
jobs.group_by(&:queue_name).each do |queue_name, same_queue_jobs|
|
|
31
37
|
enqueued_count += enqueue_batches(queue_name, same_queue_jobs)
|
|
@@ -58,11 +64,7 @@ module ActiveJob
|
|
|
58
64
|
def batch_entry(job)
|
|
59
65
|
entry = Params.new(job, nil).entry
|
|
60
66
|
entry[:id] = job.job_id
|
|
61
|
-
if job.scheduled_at
|
|
62
|
-
delay = Params.assured_delay_seconds(job.scheduled_at)
|
|
63
|
-
validate_fifo_delay!(job, delay)
|
|
64
|
-
entry[:delay_seconds] = delay
|
|
65
|
-
end
|
|
67
|
+
entry[:delay_seconds] = Params.assured_delay_seconds(job.scheduled_at) if job.scheduled_at
|
|
66
68
|
entry
|
|
67
69
|
end
|
|
68
70
|
|
|
@@ -48,7 +48,7 @@ module Aws
|
|
|
48
48
|
# {GLOBAL_ENV_CONFIGS}. For supported queue specific ENV configurations
|
|
49
49
|
# see: {QUEUE_ENV_CONFIGS}.
|
|
50
50
|
#
|
|
51
|
-
class Configuration
|
|
51
|
+
class Configuration # rubocop:disable Metrics/ClassLength
|
|
52
52
|
# Default configuration options
|
|
53
53
|
# @api private
|
|
54
54
|
DEFAULTS = {
|
|
@@ -125,6 +125,16 @@ module Aws
|
|
|
125
125
|
# receive settings). Retries provided by this mechanism are
|
|
126
126
|
# after any retries configured on the job with `retry_on`.
|
|
127
127
|
#
|
|
128
|
+
# @option options [Callable] :permanent_failure_handler a handler called
|
|
129
|
+
# as `(error, sqs_message)` just before the poller deletes a
|
|
130
|
+
# permanently-failed message (unparseable body or an
|
|
131
|
+
# {InvalidJobClassError}). Return normally to delete and report the
|
|
132
|
+
# drop; `throw :skip_delete` to keep the message on the queue for
|
|
133
|
+
# redelivery and skip the report. Intended for recovering a
|
|
134
|
+
# {JobClassNotDefinedError} raised mid rolling-deploy; throwing for
|
|
135
|
+
# other causes will pin an unprocessable message on the queue. Poller
|
|
136
|
+
# only; the Lambda handler ignores it. See the README for details.
|
|
137
|
+
#
|
|
128
138
|
# @option options [ActiveSupport::Logger] :logger Logger to use
|
|
129
139
|
# for the poller.
|
|
130
140
|
#
|
|
@@ -151,11 +161,15 @@ module Aws
|
|
|
151
161
|
# Using this option, job_id is implicitly added to the keys.
|
|
152
162
|
#
|
|
153
163
|
# @option options [Array<Class, String>, String, nil] :job_class_allowlist (nil)
|
|
154
|
-
# An optional list of job classes permitted to be executed. When set
|
|
155
|
-
# only classes in
|
|
164
|
+
# An optional list of job classes permitted to be executed. When set
|
|
165
|
+
# to a non-empty list, only classes in it are dispatched and any
|
|
166
|
+
# other class is rejected; when nil, blank, or empty, any class
|
|
156
167
|
# inheriting from ActiveJob::Base is allowed. Entries may be Class
|
|
157
168
|
# objects or class name Strings (in code/YAML), or a comma-separated
|
|
158
|
-
# String of class names (from ENV), and are compared by class name.
|
|
169
|
+
# String of class names (from ENV), and are compared by class name. A
|
|
170
|
+
# blank or empty value (a declared-but-empty environment variable or
|
|
171
|
+
# an empty list) is treated the same as unset, allowing all classes,
|
|
172
|
+
# not as "allow nothing".
|
|
159
173
|
|
|
160
174
|
def initialize(options = {})
|
|
161
175
|
opts = env_options.deep_merge(options)
|
|
@@ -173,7 +187,7 @@ module Aws
|
|
|
173
187
|
|
|
174
188
|
# @api private
|
|
175
189
|
attr_writer :max_messages, :message_group_id, :visibility_timeout,
|
|
176
|
-
:poller_error_handler, :client
|
|
190
|
+
:poller_error_handler, :permanent_failure_handler, :client
|
|
177
191
|
|
|
178
192
|
def excluded_deduplication_keys=(keys)
|
|
179
193
|
@excluded_deduplication_keys = keys.map(&:to_s) | ['job_id']
|
|
@@ -184,6 +198,11 @@ module Aws
|
|
|
184
198
|
@poller_error_handler
|
|
185
199
|
end
|
|
186
200
|
|
|
201
|
+
def permanent_failure_handler(&block)
|
|
202
|
+
@permanent_failure_handler = block if block_given?
|
|
203
|
+
@permanent_failure_handler
|
|
204
|
+
end
|
|
205
|
+
|
|
187
206
|
def client
|
|
188
207
|
@client ||= begin
|
|
189
208
|
client = Aws::SQS::Client.new
|
|
@@ -85,7 +85,7 @@ module Aws
|
|
|
85
85
|
end
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
-
def execute_task(message)
|
|
88
|
+
def execute_task(message) # rubocop:disable Metrics/MethodLength
|
|
89
89
|
job = JobRunner.new(message)
|
|
90
90
|
@logger.info("Running job: #{job.id}[#{job.class_name}]")
|
|
91
91
|
job.run
|
|
@@ -94,25 +94,62 @@ module Aws
|
|
|
94
94
|
# An unparseable body is a permanent failure: the message content is
|
|
95
95
|
# immutable, so re-parsing it on redelivery can never succeed. Log and
|
|
96
96
|
# delete it so it does not redeliver indefinitely.
|
|
97
|
-
|
|
97
|
+
parser_msg = "Unable to parse message body: #{message.data.body}. Error: #{e}."
|
|
98
|
+
drop_permanent_failure(message, e, parser_msg)
|
|
98
99
|
rescue InvalidJobClassError => e
|
|
99
100
|
# A bad job class is a permanent failure: redelivering it can never
|
|
100
101
|
# succeed and, on the default config, would crash-loop the poller.
|
|
101
102
|
# Log the forensic detail (the message can't be inspected in a DLQ
|
|
102
103
|
# once deleted) and delete it so it does not redeliver.
|
|
103
|
-
|
|
104
|
-
|
|
104
|
+
invalid_msg =
|
|
105
|
+
"Rejecting message #{message.message_id}: #{e}. " \
|
|
106
|
+
"Body: #{message.data.body}. Deleting so it does not redeliver."
|
|
107
|
+
drop_permanent_failure(message, e, invalid_msg)
|
|
105
108
|
rescue StandardError => e
|
|
106
109
|
handle_standard_error(e, job, message)
|
|
107
110
|
ensure
|
|
108
111
|
@task_complete.set
|
|
109
112
|
end
|
|
110
113
|
|
|
111
|
-
#
|
|
112
|
-
#
|
|
113
|
-
|
|
114
|
+
# Handle a permanently-failed message (unparseable body or invalid job
|
|
115
|
+
# class). Always logged. A dropped message is also reported to the
|
|
116
|
+
# error tracker so the loss is visible beyond the logs, then deleted so
|
|
117
|
+
# SQS won't redeliver. A configured +permanent_failure_handler+ can
|
|
118
|
+
# +throw :skip_delete+ to keep the message on the queue for redelivery
|
|
119
|
+
# instead, in which case it is neither reported nor deleted.
|
|
120
|
+
def drop_permanent_failure(message, error, log_message)
|
|
114
121
|
@logger.error log_message
|
|
115
|
-
|
|
122
|
+
|
|
123
|
+
handler = Aws::ActiveJob::SQS.config.permanent_failure_handler
|
|
124
|
+
unless handler
|
|
125
|
+
report_permanent_failure(error, message)
|
|
126
|
+
return message.delete
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
catch(:skip_delete) do
|
|
130
|
+
begin
|
|
131
|
+
handler.call(error, message)
|
|
132
|
+
rescue StandardError => e
|
|
133
|
+
@logger.error "permanent_failure_handler raised: #{e}"
|
|
134
|
+
end
|
|
135
|
+
report_permanent_failure(error, message)
|
|
136
|
+
message.delete
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Report a permanently-dropped job to the application's error tracker
|
|
141
|
+
# via Rails' error reporter (Rails 7+), if available, so the loss
|
|
142
|
+
# surfaces in whatever tracker the app uses. A no-op outside Rails.
|
|
143
|
+
# Only called when the message is actually deleted, so a message kept
|
|
144
|
+
# for redelivery via +:skip_delete+ is not reported as dropped.
|
|
145
|
+
def report_permanent_failure(error, message)
|
|
146
|
+
return unless defined?(::Rails) && ::Rails.respond_to?(:error)
|
|
147
|
+
|
|
148
|
+
::Rails.error.report(
|
|
149
|
+
error,
|
|
150
|
+
handled: true,
|
|
151
|
+
context: { message_id: message.message_id, body: message.data.body }
|
|
152
|
+
)
|
|
116
153
|
end
|
|
117
154
|
|
|
118
155
|
def handle_standard_error(error, job, message)
|
|
@@ -11,6 +11,16 @@ module Aws
|
|
|
11
11
|
# deletes it rather than letting it redeliver.
|
|
12
12
|
class InvalidJobClassError < StandardError; end
|
|
13
13
|
|
|
14
|
+
# Raised when a message names a class not defined in this process.
|
|
15
|
+
#
|
|
16
|
+
# Recoverable in a way other {InvalidJobClassError} causes (malformed
|
|
17
|
+
# name, disallowed class, non-job class) aren't: during a rolling deploy,
|
|
18
|
+
# redelivering to a newer worker that has the class may succeed. Subclasses
|
|
19
|
+
# {InvalidJobClassError} so existing rescues still catch it; a
|
|
20
|
+
# +permanent_failure_handler+ can single it out and keep the message on
|
|
21
|
+
# the queue for redelivery.
|
|
22
|
+
class JobClassNotDefinedError < InvalidJobClassError; end
|
|
23
|
+
|
|
14
24
|
# @api private
|
|
15
25
|
class JobRunner
|
|
16
26
|
# A job class name is an (optionally namespaced) constant path and
|
|
@@ -65,12 +75,14 @@ module Aws
|
|
|
65
75
|
# segment must be defined directly on the preceding one: a name such
|
|
66
76
|
# as 'SomeJob::Foo' cannot resolve Foo from SomeJob's ancestors when
|
|
67
77
|
# SomeJob itself does not define it. NameError is treated as a
|
|
68
|
-
# rejection rather than propagated
|
|
69
|
-
#
|
|
78
|
+
# rejection rather than propagated, but as the recoverable
|
|
79
|
+
# {JobClassNotDefinedError} subclass: an undefined class may just be
|
|
80
|
+
# missing from this process (e.g. mid rolling-deploy), unlike a
|
|
81
|
+
# malformed or disallowed name.
|
|
70
82
|
def constantize_job_class(name)
|
|
71
83
|
Object.const_get(name, false)
|
|
72
84
|
rescue NameError
|
|
73
|
-
raise
|
|
85
|
+
raise JobClassNotDefinedError, "Job Class: #{name} is not defined"
|
|
74
86
|
end
|
|
75
87
|
|
|
76
88
|
# The allowlist may be configured as an Array of Class values (in code),
|
|
@@ -78,8 +90,11 @@ module Aws
|
|
|
78
90
|
# (from ENV). Normalize all forms to an Array of class name Strings.
|
|
79
91
|
def normalized_allowlist
|
|
80
92
|
allowlist = Aws::ActiveJob::SQS.config.job_class_allowlist
|
|
93
|
+
return if allowlist.nil?
|
|
94
|
+
|
|
81
95
|
allowlist = allowlist.split(',') if allowlist.is_a?(String)
|
|
82
|
-
allowlist
|
|
96
|
+
normalized = allowlist.map { |entry| entry.to_s.strip }.reject(&:empty?)
|
|
97
|
+
normalized unless normalized.empty?
|
|
83
98
|
end
|
|
84
99
|
end
|
|
85
100
|
end
|