aws-activejob-sqs 1.1.1 → 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 +7 -0
- data/VERSION +1 -1
- data/lib/active_job/queue_adapters/sqs_adapter.rb +7 -5
- data/lib/aws/active_job/sqs/configuration.rb +17 -2
- data/lib/aws/active_job/sqs/executor.rb +45 -8
- data/lib/aws/active_job/sqs/job_runner.rb +15 -3
- 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,10 @@
|
|
|
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
|
+
|
|
1
8
|
1.1.1 (2026-08-12)
|
|
2
9
|
------------------
|
|
3
10
|
|
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
|
#
|
|
@@ -177,7 +187,7 @@ module Aws
|
|
|
177
187
|
|
|
178
188
|
# @api private
|
|
179
189
|
attr_writer :max_messages, :message_group_id, :visibility_timeout,
|
|
180
|
-
:poller_error_handler, :client
|
|
190
|
+
:poller_error_handler, :permanent_failure_handler, :client
|
|
181
191
|
|
|
182
192
|
def excluded_deduplication_keys=(keys)
|
|
183
193
|
@excluded_deduplication_keys = keys.map(&:to_s) | ['job_id']
|
|
@@ -188,6 +198,11 @@ module Aws
|
|
|
188
198
|
@poller_error_handler
|
|
189
199
|
end
|
|
190
200
|
|
|
201
|
+
def permanent_failure_handler(&block)
|
|
202
|
+
@permanent_failure_handler = block if block_given?
|
|
203
|
+
@permanent_failure_handler
|
|
204
|
+
end
|
|
205
|
+
|
|
191
206
|
def client
|
|
192
207
|
@client ||= begin
|
|
193
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),
|