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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 733e9f1e9e56f0bf7052035f5b983890a9a29ddc67d668b5a83e609411151375
4
- data.tar.gz: cb5c4d63e714b6abbf79c947a39c2f2176fd1be5631baf775383ac2d3f01e7f8
3
+ metadata.gz: 87f80b0bcacdf1a7121b918c0e126427f98ff530492ba1b086886e04de4e58e3
4
+ data.tar.gz: 29c3445f74019e920a6b3aa8999f756ea728bc5a2200fd56927e31eca2ed51dc
5
5
  SHA512:
6
- metadata.gz: 789be54e545ee534377e3e2129911a35a789df7f5b6de0791f075e21beb8f5fcbc804430f2f4b98518ea97559939453594d8f8f47724c0f6f475410456f1d247
7
- data.tar.gz: ebb107e7a92d5ec1643f709b6d913ea8a47ee712e70981760ab87517e28c0c9673a4aa5d79f4d99b229e3799a12825744d846388e6fcddb721b96a57d395c052
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
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
- drop_permanent_failure(message, "Unable to parse message body: #{message.data.body}. Error: #{e}.")
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
- drop_permanent_failure(message, "Rejecting message #{message.message_id}: #{e}. " \
104
- "Body: #{message.data.body}. Deleting so it does not redeliver.")
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
- # Log a permanently-failed message and delete it so SQS does not
112
- # redeliver it (which, on the default config, would crash-loop the poller).
113
- def drop_permanent_failure(message, log_message)
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
- message.delete
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: an unknown class is the same
69
- # permanent failure as a disallowed one.
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 InvalidJobClassError, "#{name} is not defined"
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),
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aws-activejob-sqs
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.1
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Amazon Web Services