aws-activejob-sqs 1.0.2 → 1.1.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 +27 -8
- data/lib/aws/active_job/sqs/configuration.rb +11 -2
- data/lib/aws/active_job/sqs/errors.rb +10 -0
- data/lib/aws/active_job/sqs/executor.rb +18 -1
- data/lib/aws/active_job/sqs/job_runner.rb +61 -1
- data/lib/aws-activejob-sqs.rb +18 -15
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d1b770d150d33bd540df6ee1c10c1546ccd93590d7ad09706856e60f6b6cae47
|
|
4
|
+
data.tar.gz: 38cb019b97c6b630d396116b105a6c8df30c9f16318cf82cccd25f8f85fef587
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8a4fe7cc814506be5d6b667e44150c5abf6fd7badfaa565fc448bb976380e16ac594abc601333571faebc6ac964c885a98eebb67722476885f9f4d8d2ca01282
|
|
7
|
+
data.tar.gz: b4986768479fb9e0a92db2769a3a19b576184bcefe6491abe4aa501568d9aa8672988847587741cf391e4062d514a5f85a66f45919bdb7a8e13c8ebf026f7066
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
1.1.0 (2026-08-11)
|
|
2
|
+
------------------
|
|
3
|
+
|
|
4
|
+
* Issue - Raise a clear error at enqueue time when a delayed job targets a FIFO queue, which does not support per-message delays (#36).
|
|
5
|
+
* Issue - Only classes inheriting from `ActiveJob::Base` are executed by the poller, preventing arbitrary classes named in an SQS message from being instantiated and run. Job class names are validated as constant paths and resolved without searching the namespace's ancestors, so a message cannot name a constant outside the intended namespace.
|
|
6
|
+
* Feature - Add optional `job_class_allowlist` configuration to restrict which job classes can be dispatched. Configurable in code, the config YAML file, or via the `AWS_ACTIVE_JOB_SQS_JOB_CLASS_ALLOWLIST` environment variable. When set, the allowlist is checked before the class name is resolved, so an excluded class is never loaded.
|
|
7
|
+
|
|
1
8
|
1.0.2 (2025-04-01)
|
|
2
9
|
------------------
|
|
3
10
|
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0
|
|
1
|
+
1.1.0
|
|
@@ -21,6 +21,7 @@ module ActiveJob
|
|
|
21
21
|
|
|
22
22
|
def enqueue_at(job, timestamp)
|
|
23
23
|
delay = Params.assured_delay_seconds(timestamp)
|
|
24
|
+
validate_fifo_delay!(job, delay)
|
|
24
25
|
_enqueue(job, nil, delay_seconds: delay)
|
|
25
26
|
end
|
|
26
27
|
|
|
@@ -45,22 +46,40 @@ module ActiveJob
|
|
|
45
46
|
end
|
|
46
47
|
|
|
47
48
|
def enqueue_batch(queue_url, chunk)
|
|
48
|
-
entries = chunk.map do |job|
|
|
49
|
-
entry = Params.new(job, nil).entry
|
|
50
|
-
entry[:id] = job.job_id
|
|
51
|
-
entry[:delay_seconds] = Params.assured_delay_seconds(job.scheduled_at) if job.scheduled_at
|
|
52
|
-
entry
|
|
53
|
-
end
|
|
54
|
-
|
|
55
49
|
send_message_opts = {
|
|
56
50
|
queue_url: queue_url,
|
|
57
|
-
entries:
|
|
51
|
+
entries: chunk.map { |job| batch_entry(job) }
|
|
58
52
|
}
|
|
59
53
|
|
|
60
54
|
send_message_batch_result = Aws::ActiveJob::SQS.config.client.send_message_batch(send_message_opts)
|
|
61
55
|
send_message_batch_result.successful.count
|
|
62
56
|
end
|
|
63
57
|
|
|
58
|
+
def batch_entry(job)
|
|
59
|
+
entry = Params.new(job, nil).entry
|
|
60
|
+
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
|
|
66
|
+
entry
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# SQS FIFO queues do not support per-message delays
|
|
70
|
+
def validate_fifo_delay!(job, delay)
|
|
71
|
+
return unless delay.positive?
|
|
72
|
+
|
|
73
|
+
queue_url = Aws::ActiveJob::SQS.config.url_for(job.queue_name)
|
|
74
|
+
return unless Aws::ActiveJob::SQS.fifo?(queue_url)
|
|
75
|
+
|
|
76
|
+
err_msg =
|
|
77
|
+
"FIFO queue #{queue_url} does not support per-message delays " \
|
|
78
|
+
'(e.g. `set(wait:)`, `enqueue_at` or `retry_on wait:`). ' \
|
|
79
|
+
'When using `retry_on` with FIFO queues, set `wait: 0`.'
|
|
80
|
+
raise Aws::ActiveJob::SQS::FifoDelayNotSupportedError, err_msg
|
|
81
|
+
end
|
|
82
|
+
|
|
64
83
|
def _enqueue(job, body = nil, send_message_opts = {})
|
|
65
84
|
body ||= job.serialize
|
|
66
85
|
params = Params.new(job, body)
|
|
@@ -69,6 +69,7 @@ module Aws
|
|
|
69
69
|
shutdown_timeout
|
|
70
70
|
visibility_timeout
|
|
71
71
|
message_group_id
|
|
72
|
+
job_class_allowlist
|
|
72
73
|
].freeze
|
|
73
74
|
|
|
74
75
|
QUEUE_ENV_CONFIGS = %i[
|
|
@@ -81,7 +82,7 @@ module Aws
|
|
|
81
82
|
QUEUE_CONFIGS = QUEUE_ENV_CONFIGS + %i[excluded_deduplication_keys]
|
|
82
83
|
|
|
83
84
|
QUEUE_KEY_REGEX =
|
|
84
|
-
/AWS_ACTIVE_JOB_SQS_(
|
|
85
|
+
/AWS_ACTIVE_JOB_SQS_(\w+)_(#{QUEUE_ENV_CONFIGS.map(&:upcase).join('|')})/.freeze
|
|
85
86
|
|
|
86
87
|
# Don't use this method directly: Configuration is a singleton class,
|
|
87
88
|
# use {Aws::ActiveJob::SQS.config Aws::ActiveJob::SQS.config}
|
|
@@ -148,6 +149,13 @@ module Aws
|
|
|
148
149
|
# @option options [Array] :excluded_deduplication_keys (['job_id'])
|
|
149
150
|
# The type of keys stored in the array should be String or Symbol.
|
|
150
151
|
# Using this option, job_id is implicitly added to the keys.
|
|
152
|
+
#
|
|
153
|
+
# @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 this list are dispatched; when nil, any class
|
|
156
|
+
# inheriting from ActiveJob::Base is allowed. Entries may be Class
|
|
157
|
+
# 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.
|
|
151
159
|
|
|
152
160
|
def initialize(options = {})
|
|
153
161
|
opts = env_options.deep_merge(options)
|
|
@@ -160,7 +168,8 @@ module Aws
|
|
|
160
168
|
# @api private
|
|
161
169
|
attr_accessor :queues, :threads, :backpressure,
|
|
162
170
|
:shutdown_timeout, :logger,
|
|
163
|
-
:async_queue_error_handler
|
|
171
|
+
:async_queue_error_handler,
|
|
172
|
+
:job_class_allowlist
|
|
164
173
|
|
|
165
174
|
# @api private
|
|
166
175
|
attr_writer :max_messages, :message_group_id, :visibility_timeout,
|
|
@@ -91,13 +91,30 @@ module Aws
|
|
|
91
91
|
job.run
|
|
92
92
|
message.delete
|
|
93
93
|
rescue JSON::ParserError => e
|
|
94
|
-
|
|
94
|
+
# An unparseable body is a permanent failure: the message content is
|
|
95
|
+
# immutable, so re-parsing it on redelivery can never succeed. Log and
|
|
96
|
+
# delete it so it does not redeliver indefinitely.
|
|
97
|
+
drop_permanent_failure(message, "Unable to parse message body: #{message.data.body}. Error: #{e}.")
|
|
98
|
+
rescue InvalidJobClassError => e
|
|
99
|
+
# A bad job class is a permanent failure: redelivering it can never
|
|
100
|
+
# succeed and, on the default config, would crash-loop the poller.
|
|
101
|
+
# Log the forensic detail (the message can't be inspected in a DLQ
|
|
102
|
+
# 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.")
|
|
95
105
|
rescue StandardError => e
|
|
96
106
|
handle_standard_error(e, job, message)
|
|
97
107
|
ensure
|
|
98
108
|
@task_complete.set
|
|
99
109
|
end
|
|
100
110
|
|
|
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
|
+
@logger.error log_message
|
|
115
|
+
message.delete
|
|
116
|
+
end
|
|
117
|
+
|
|
101
118
|
def handle_standard_error(error, job, message)
|
|
102
119
|
job_msg = job ? "#{job.id}[#{job.class_name}]" : 'unknown job'
|
|
103
120
|
@logger.info "Error processing job #{job_msg}: #{error}"
|
|
@@ -3,13 +3,26 @@
|
|
|
3
3
|
module Aws
|
|
4
4
|
module ActiveJob
|
|
5
5
|
module SQS
|
|
6
|
+
# Raised when a message names a job class that cannot be executed: the
|
|
7
|
+
# name is not a well-formed constant path, it is not in the configured
|
|
8
|
+
# job_class_allowlist, it is undefined, or it does not name a class
|
|
9
|
+
# inheriting from ActiveJob::Base. This is a permanent
|
|
10
|
+
# failure: such a message can never succeed, so the executor logs and
|
|
11
|
+
# deletes it rather than letting it redeliver.
|
|
12
|
+
class InvalidJobClassError < StandardError; end
|
|
13
|
+
|
|
6
14
|
# @api private
|
|
7
15
|
class JobRunner
|
|
16
|
+
# A job class name is an (optionally namespaced) constant path and
|
|
17
|
+
# nothing else. Names that cannot match this can never name a job
|
|
18
|
+
# class, so rejecting them keeps them out of the constant lookup.
|
|
19
|
+
JOB_CLASS_NAME_PATTERN = /\A[A-Z]\w*(::[A-Z]\w*)*\z/.freeze
|
|
20
|
+
|
|
8
21
|
attr_reader :id, :class_name
|
|
9
22
|
|
|
10
23
|
def initialize(message)
|
|
11
24
|
@job_data = ActiveSupport::JSON.load(message.data.body)
|
|
12
|
-
@class_name = @job_data['job_class']
|
|
25
|
+
@class_name = resolve_job_class(@job_data['job_class'])
|
|
13
26
|
@id = @job_data['job_id']
|
|
14
27
|
end
|
|
15
28
|
|
|
@@ -21,6 +34,53 @@ module Aws
|
|
|
21
34
|
@job_data['exception_executions'] &&
|
|
22
35
|
!@job_data['exception_executions'].empty?
|
|
23
36
|
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
# Checks are ordered so that the least trusting one runs first. Merely
|
|
41
|
+
# resolving a constant is not inert: in Rails it triggers autoloading,
|
|
42
|
+
# which runs the body of whichever file defines it. So the name is
|
|
43
|
+
# matched against the allowlist as a String, before any lookup, and an
|
|
44
|
+
# excluded name never reaches the constant resolver at all.
|
|
45
|
+
def resolve_job_class(name)
|
|
46
|
+
name = name.to_s
|
|
47
|
+
unless JOB_CLASS_NAME_PATTERN.match?(name)
|
|
48
|
+
raise InvalidJobClassError, "#{name.inspect} is not a valid job class name"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
allowlist = normalized_allowlist
|
|
52
|
+
if allowlist && !allowlist.include?(name)
|
|
53
|
+
raise InvalidJobClassError, "#{name} is not in the configured job_class_allowlist"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
klass = constantize_job_class(name)
|
|
57
|
+
unless klass.is_a?(Class) && klass < ::ActiveJob::Base
|
|
58
|
+
raise InvalidJobClassError, "#{name} is not a valid job class (must inherit from ActiveJob::Base)"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
klass
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Resolves an already-validated name. +inherit+ is false so that each
|
|
65
|
+
# segment must be defined directly on the preceding one: a name such
|
|
66
|
+
# as 'SomeJob::Foo' cannot resolve Foo from SomeJob's ancestors when
|
|
67
|
+
# 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.
|
|
70
|
+
def constantize_job_class(name)
|
|
71
|
+
Object.const_get(name, false)
|
|
72
|
+
rescue NameError
|
|
73
|
+
raise InvalidJobClassError, "#{name} is not defined"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# The allowlist may be configured as an Array of Class values (in code),
|
|
77
|
+
# an Array of Strings (from the YAML file), or a comma-separated String
|
|
78
|
+
# (from ENV). Normalize all forms to an Array of class name Strings.
|
|
79
|
+
def normalized_allowlist
|
|
80
|
+
allowlist = Aws::ActiveJob::SQS.config.job_class_allowlist
|
|
81
|
+
allowlist = allowlist.split(',') if allowlist.is_a?(String)
|
|
82
|
+
allowlist&.map { |entry| entry.to_s.strip }
|
|
83
|
+
end
|
|
24
84
|
end
|
|
25
85
|
end
|
|
26
86
|
end
|
data/lib/aws-activejob-sqs.rb
CHANGED
|
@@ -4,6 +4,7 @@ require 'active_job'
|
|
|
4
4
|
require_relative 'active_job/queue_adapters/sqs_adapter'
|
|
5
5
|
require_relative 'active_job/queue_adapters/sqs_adapter/params'
|
|
6
6
|
require_relative 'active_job/queue_adapters/sqs_async_adapter'
|
|
7
|
+
require_relative 'aws/active_job/sqs/errors'
|
|
7
8
|
require_relative 'aws/active_job/sqs/configuration'
|
|
8
9
|
require_relative 'aws/active_job/sqs/deduplication'
|
|
9
10
|
require_relative 'aws/active_job/sqs/executor'
|
|
@@ -16,24 +17,26 @@ module Aws
|
|
|
16
17
|
module SQS
|
|
17
18
|
VERSION = File.read(File.expand_path('../VERSION', __dir__)).strip
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
class << self
|
|
21
|
+
# @return [Configuration] the (singleton) Configuration
|
|
22
|
+
def config
|
|
23
|
+
@config ||= Configuration.new
|
|
24
|
+
end
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
# @yield [Configuration] the (singleton) Configuration
|
|
27
|
+
def configure
|
|
28
|
+
yield(config)
|
|
29
|
+
end
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
# @param queue_url [String]
|
|
32
|
+
# @return [Boolean] true if the queue_url is a FIFO queue
|
|
33
|
+
def fifo?(queue_url)
|
|
34
|
+
queue_url.end_with?('.fifo')
|
|
35
|
+
end
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
def on_worker_stop(...)
|
|
38
|
+
Executor.on_stop(...)
|
|
39
|
+
end
|
|
37
40
|
end
|
|
38
41
|
end
|
|
39
42
|
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: aws-activejob-sqs
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Amazon Web Services
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: aws-sdk-sqs
|
|
@@ -76,6 +76,7 @@ files:
|
|
|
76
76
|
- lib/aws/active_job/sqs/cli_options.rb
|
|
77
77
|
- lib/aws/active_job/sqs/configuration.rb
|
|
78
78
|
- lib/aws/active_job/sqs/deduplication.rb
|
|
79
|
+
- lib/aws/active_job/sqs/errors.rb
|
|
79
80
|
- lib/aws/active_job/sqs/executor.rb
|
|
80
81
|
- lib/aws/active_job/sqs/job_runner.rb
|
|
81
82
|
- lib/aws/active_job/sqs/lambda_handler.rb
|
|
@@ -98,7 +99,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
98
99
|
- !ruby/object:Gem::Version
|
|
99
100
|
version: '0'
|
|
100
101
|
requirements: []
|
|
101
|
-
rubygems_version:
|
|
102
|
+
rubygems_version: 4.0.16
|
|
102
103
|
specification_version: 4
|
|
103
104
|
summary: ActiveJob integration with SQS
|
|
104
105
|
test_files: []
|