sidekiq_sqs_processor 0.1.1 → 0.1.2
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/README.md +26 -12
- data/lib/sidekiq_sqs_processor/base_worker.rb +40 -44
- data/lib/sidekiq_sqs_processor/configuration.rb +38 -164
- data/lib/sidekiq_sqs_processor/continuous_poller.rb +69 -59
- data/lib/sidekiq_sqs_processor/railtie.rb +61 -67
- data/lib/sidekiq_sqs_processor/version.rb +1 -1
- data/lib/sidekiq_sqs_processor.rb +35 -45
- 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: a20dc1ecd4771415945cbfa13409b8a532c3ce16fbd8e5a67d2d85dcc0c65cb5
|
4
|
+
data.tar.gz: 9593831edbcb55c0c0b6d7b625e2c8c536fa90105c6bb084e6cf0f27d2c98d01
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 99c6d8311c5a273edae3727d989d6684487986278fc74e252d093fbb4c95ab6c890c71862cbde65bb90652e4c77e0b2c1e938f0296ae8381b028f85e37392033
|
7
|
+
data.tar.gz: 112ee1e6d29af7c2e4c1b40ff0fc864fbd169d388c7e04de2d287112da7b63960b4cd9fc98c9337264cc0c22993ceb070b61ff52c8ba1c4dccb86f2c2415d55d
|
data/README.md
CHANGED
@@ -21,7 +21,7 @@ A Ruby gem that seamlessly integrates Amazon SQS with Sidekiq for efficient and
|
|
21
21
|
Add this line to your application's Gemfile:
|
22
22
|
|
23
23
|
```ruby
|
24
|
-
gem '
|
24
|
+
gem 'sidekiq-sqs-processor'
|
25
25
|
```
|
26
26
|
|
27
27
|
And then execute:
|
@@ -67,9 +67,24 @@ SidekiqSqsProcessor.configure do |config|
|
|
67
67
|
config.queue_urls = [
|
68
68
|
'https://sqs.us-east-1.amazonaws.com/123456789012/my-queue'
|
69
69
|
]
|
70
|
-
|
70
|
+
|
71
71
|
# Polling configuration
|
72
72
|
config.polling_type = :continuous
|
73
|
+
|
74
|
+
# Map queue URLs to worker classes
|
75
|
+
config.queue_workers = {
|
76
|
+
'https://sqs.us-east-1.amazonaws.com/123456789012/queue1' => 'Queue1Worker',
|
77
|
+
'https://sqs.us-east-1.amazonaws.com/123456789012/queue2' => 'Queue2Worker'
|
78
|
+
}
|
79
|
+
|
80
|
+
# Optional: Set custom logger
|
81
|
+
config.logger = Rails.logger
|
82
|
+
|
83
|
+
# Optional: Set custom error handler
|
84
|
+
config.error_handler = ->(error, context) do
|
85
|
+
# Handle errors (e.g., report to monitoring service)
|
86
|
+
Rails.logger.error("SQS Error: #{error.message}\nContext: #{context.inspect}")
|
87
|
+
end
|
73
88
|
end
|
74
89
|
```
|
75
90
|
|
@@ -88,13 +103,13 @@ SidekiqSqsProcessor.configure do |config|
|
|
88
103
|
config.queue_urls = [
|
89
104
|
'https://sqs.us-east-1.amazonaws.com/123456789012/my-queue'
|
90
105
|
]
|
91
|
-
|
106
|
+
|
92
107
|
# Start polling when Sidekiq starts
|
93
108
|
Sidekiq.configure_server do |sidekiq_config|
|
94
109
|
sidekiq_config.on(:startup) do
|
95
110
|
SidekiqSqsProcessor.start_continuous_poller
|
96
111
|
end
|
97
|
-
|
112
|
+
|
98
113
|
sidekiq_config.on(:shutdown) do
|
99
114
|
SidekiqSqsProcessor.stop_continuous_poller
|
100
115
|
end
|
@@ -124,10 +139,10 @@ class MyWorker < SidekiqSqsProcessor::BaseWorker
|
|
124
139
|
# Override the process_message method to handle your SQS messages
|
125
140
|
def process_message(message_body)
|
126
141
|
# message_body is already parsed from JSON if it was a JSON string
|
127
|
-
|
142
|
+
|
128
143
|
# Your processing logic here
|
129
144
|
User.find_by(id: message_body['user_id'])&.notify(message_body['message'])
|
130
|
-
|
145
|
+
|
131
146
|
# Return a result (optional)
|
132
147
|
{ status: 'success' }
|
133
148
|
end
|
@@ -148,9 +163,9 @@ class OrderProcessor < SidekiqSqsProcessor::BaseWorker
|
|
148
163
|
logger.error("Invalid order message format: #{message_body.inspect}")
|
149
164
|
end
|
150
165
|
end
|
151
|
-
|
166
|
+
|
152
167
|
private
|
153
|
-
|
168
|
+
|
154
169
|
def process_order(order_id, items)
|
155
170
|
# Your order processing logic
|
156
171
|
Order.process(order_id, items)
|
@@ -174,7 +189,7 @@ class EventProcessor < SidekiqSqsProcessor::BaseWorker
|
|
174
189
|
logger.warn("Unknown event type: #{message_body['event_type']}")
|
175
190
|
end
|
176
191
|
end
|
177
|
-
|
192
|
+
|
178
193
|
# ... processing methods ...
|
179
194
|
end
|
180
195
|
```
|
@@ -188,7 +203,7 @@ class NotificationProcessor < SidekiqSqsProcessor::BaseWorker
|
|
188
203
|
def process_message(message_body)
|
189
204
|
# For an SNS message, the SNS envelope has been removed
|
190
205
|
# and message_body is the parsed content of the SNS Message field
|
191
|
-
|
206
|
+
|
192
207
|
logger.info("Processing notification: #{message_body.inspect}")
|
193
208
|
# ... your processing logic ...
|
194
209
|
end
|
@@ -202,7 +217,7 @@ You can also enqueue messages directly to a worker without going through SQS:
|
|
202
217
|
```ruby
|
203
218
|
# Enqueue a message to a specific worker
|
204
219
|
SidekiqSqsProcessor.enqueue_message(
|
205
|
-
MyWorker,
|
220
|
+
MyWorker,
|
206
221
|
{ order_id: 123, items: ['item1', 'item2'] }
|
207
222
|
)
|
208
223
|
```
|
@@ -312,4 +327,3 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/yourus
|
|
312
327
|
## License
|
313
328
|
|
314
329
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
315
|
-
|
@@ -4,23 +4,6 @@ module SidekiqSqsProcessor
|
|
4
4
|
class BaseWorker
|
5
5
|
include Sidekiq::Worker
|
6
6
|
|
7
|
-
class << self
|
8
|
-
def process_message_automatically
|
9
|
-
return @process_message_automatically if defined?(@process_message_automatically)
|
10
|
-
@process_message_automatically = true
|
11
|
-
end
|
12
|
-
|
13
|
-
def process_message_automatically=(value)
|
14
|
-
@process_message_automatically = value
|
15
|
-
end
|
16
|
-
|
17
|
-
def inherited(subclass)
|
18
|
-
super
|
19
|
-
# Copy the process_message_automatically value to subclasses
|
20
|
-
subclass.process_message_automatically = process_message_automatically
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
7
|
# Configure Sidekiq options with defaults
|
25
8
|
sidekiq_options(
|
26
9
|
retry: 25, # Default retry count
|
@@ -28,55 +11,68 @@ module SidekiqSqsProcessor
|
|
28
11
|
)
|
29
12
|
|
30
13
|
def perform(message_data)
|
31
|
-
process_message_data(message_data) if self.class.process_message_automatically
|
32
|
-
end
|
33
|
-
|
34
|
-
# Override this method in your worker
|
35
|
-
def process_message(body)
|
36
|
-
raise NotImplementedError, "You must implement process_message in your worker"
|
37
|
-
end
|
38
|
-
|
39
|
-
private
|
40
|
-
|
41
|
-
def process_message_data(message_data)
|
42
14
|
begin
|
43
|
-
# Parse
|
15
|
+
# Parse the SQS message body
|
44
16
|
body = JSON.parse(message_data["body"])
|
45
|
-
|
17
|
+
|
18
|
+
# Extract the actual message from the SQS message
|
19
|
+
# SNS messages are wrapped in a JSON structure with a "Message" field
|
20
|
+
actual_message = if body["Message"]
|
21
|
+
JSON.parse(body["Message"])
|
22
|
+
else
|
23
|
+
body
|
24
|
+
end
|
25
|
+
|
26
|
+
# Process the message
|
27
|
+
process_message(actual_message)
|
46
28
|
|
47
29
|
# Delete the message from SQS after successful processing
|
48
30
|
delete_sqs_message(message_data)
|
49
31
|
rescue JSON::ParserError => e
|
50
|
-
handle_error(e,
|
32
|
+
SidekiqSqsProcessor.handle_error(e, {
|
33
|
+
worker: self.class.name,
|
34
|
+
message: message_data,
|
35
|
+
description: "Failed to parse message body"
|
36
|
+
})
|
37
|
+
raise # Re-raise to trigger Sidekiq retry
|
51
38
|
rescue Aws::SQS::Errors::ServiceError => e
|
52
|
-
handle_error(e,
|
39
|
+
SidekiqSqsProcessor.handle_error(e, {
|
40
|
+
worker: self.class.name,
|
41
|
+
message: message_data,
|
42
|
+
description: "SQS service error"
|
43
|
+
})
|
44
|
+
raise # Re-raise to trigger Sidekiq retry
|
53
45
|
rescue StandardError => e
|
54
|
-
handle_error(e,
|
46
|
+
SidekiqSqsProcessor.handle_error(e, {
|
47
|
+
worker: self.class.name,
|
48
|
+
message: message_data,
|
49
|
+
description: "Error processing message"
|
50
|
+
})
|
51
|
+
raise # Re-raise to trigger Sidekiq retry
|
55
52
|
end
|
56
53
|
end
|
57
54
|
|
55
|
+
# Override this method in your worker
|
56
|
+
def process_message(body)
|
57
|
+
raise NotImplementedError, "You must implement process_message in your worker"
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
58
62
|
def delete_sqs_message(message_data)
|
59
63
|
return unless message_data["queue_url"] && message_data["receipt_handle"]
|
60
|
-
|
64
|
+
|
61
65
|
SidekiqSqsProcessor.sqs_client.delete_message(
|
62
66
|
queue_url: message_data["queue_url"],
|
63
67
|
receipt_handle: message_data["receipt_handle"]
|
64
68
|
)
|
65
69
|
rescue Aws::SQS::Errors::ServiceError => e
|
66
|
-
handle_error(e,
|
67
|
-
end
|
68
|
-
|
69
|
-
def handle_error(error, message_data, description)
|
70
|
-
SidekiqSqsProcessor.handle_error(error, {
|
70
|
+
SidekiqSqsProcessor.handle_error(e, {
|
71
71
|
worker: self.class.name,
|
72
72
|
message: message_data,
|
73
|
-
description:
|
73
|
+
description: "Failed to delete message from SQS"
|
74
74
|
})
|
75
75
|
raise # Re-raise to trigger Sidekiq retry
|
76
76
|
end
|
77
|
-
|
78
|
-
def logger
|
79
|
-
SidekiqSqsProcessor.configuration.logger || Sidekiq.logger
|
80
|
-
end
|
81
77
|
end
|
82
78
|
end
|
@@ -1,182 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module SidekiqSqsProcessor
|
2
4
|
class Configuration
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
# Worker configuration
|
11
|
-
attr_accessor :worker_retry_count, :worker_queue_name, :logger
|
5
|
+
attr_accessor :aws_region,
|
6
|
+
:aws_access_key_id,
|
7
|
+
:aws_secret_access_key,
|
8
|
+
:queue_workers,
|
9
|
+
:logger,
|
10
|
+
:error_handler
|
12
11
|
|
13
12
|
def initialize
|
14
|
-
|
15
|
-
@
|
16
|
-
@
|
17
|
-
@
|
18
|
-
@
|
19
|
-
@poller_thread_count = 1
|
20
|
-
@worker_retry_count = 25
|
21
|
-
@worker_queue_name = "default"
|
22
|
-
@error_handler = default_error_handler
|
23
|
-
@polling_enabled = true
|
24
|
-
@polling_type = :continuous
|
25
|
-
@poll_on_startup = true
|
26
|
-
@polling_frequency = 60 # seconds
|
27
|
-
@log_level = Logger::INFO
|
28
|
-
end
|
29
|
-
|
30
|
-
def aws_region=(region)
|
31
|
-
@aws_region = region&.strip
|
13
|
+
puts "[SidekiqSqsProcessor] Initializing configuration..."
|
14
|
+
@aws_region = 'us-east-1'
|
15
|
+
@queue_workers = {} # Hash of queue_url => worker_class_name
|
16
|
+
@logger = nil
|
17
|
+
@error_handler = nil
|
32
18
|
end
|
33
19
|
|
34
|
-
def
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
@queue_urls << url.strip
|
40
|
-
@queue_urls.uniq!
|
41
|
-
end
|
42
|
-
|
43
|
-
def max_number_of_messages=(value)
|
44
|
-
@max_number_of_messages = value.to_i
|
45
|
-
end
|
20
|
+
def validate!(strict: false)
|
21
|
+
puts "[SidekiqSqsProcessor] Validating configuration..."
|
22
|
+
puts "[SidekiqSqsProcessor] Current queue workers: #{@queue_workers.inspect}"
|
23
|
+
|
24
|
+
raise ArgumentError, "AWS region is required" if @aws_region.nil?
|
46
25
|
|
47
|
-
|
48
|
-
|
26
|
+
# Only enforce queue workers in strict mode
|
27
|
+
if strict
|
28
|
+
raise ArgumentError, "At least one queue worker mapping is required" if @queue_workers.empty?
|
29
|
+
raise ArgumentError, "All queue URLs must have a corresponding worker class name" if @queue_workers.values.any?(&:nil?)
|
30
|
+
elsif @queue_workers.empty?
|
31
|
+
puts "[SidekiqSqsProcessor] WARNING: No queue workers configured yet"
|
32
|
+
return false
|
33
|
+
end
|
34
|
+
|
35
|
+
puts "[SidekiqSqsProcessor] Configuration validation successful"
|
36
|
+
true
|
49
37
|
end
|
50
38
|
|
51
|
-
def
|
52
|
-
|
39
|
+
def ready_for_polling?
|
40
|
+
!@queue_workers.empty? && validate!(strict: false)
|
53
41
|
end
|
54
42
|
|
55
|
-
def
|
56
|
-
@
|
43
|
+
def queue_urls
|
44
|
+
@queue_workers.keys
|
57
45
|
end
|
58
46
|
|
59
|
-
def
|
60
|
-
if
|
61
|
-
|
62
|
-
end
|
63
|
-
@error_handler = handler
|
64
|
-
end
|
65
|
-
|
66
|
-
def polling_enabled=(value)
|
67
|
-
@polling_enabled = !!value
|
68
|
-
end
|
69
|
-
|
70
|
-
def polling_type=(value)
|
71
|
-
value = value.to_sym if value.respond_to?(:to_sym)
|
72
|
-
unless [:continuous, :scheduled].include?(value)
|
73
|
-
raise ArgumentError, "polling_type must be :continuous or :scheduled"
|
74
|
-
end
|
75
|
-
@polling_type = value
|
76
|
-
end
|
77
|
-
|
78
|
-
def poll_on_startup=(value)
|
79
|
-
@poll_on_startup = !!value
|
80
|
-
end
|
81
|
-
|
82
|
-
def polling_frequency=(value)
|
83
|
-
@polling_frequency = [value.to_i, 1].max
|
84
|
-
end
|
85
|
-
|
86
|
-
def log_level=(level)
|
87
|
-
if level.is_a?(Integer) && (0..5).include?(level)
|
88
|
-
@log_level = level
|
89
|
-
elsif level.is_a?(Symbol) || level.is_a?(String)
|
90
|
-
level_map = {
|
91
|
-
debug: Logger::DEBUG,
|
92
|
-
info: Logger::INFO,
|
93
|
-
warn: Logger::WARN,
|
94
|
-
error: Logger::ERROR,
|
95
|
-
fatal: Logger::FATAL,
|
96
|
-
unknown: Logger::UNKNOWN
|
97
|
-
}
|
98
|
-
sym_level = level.to_sym.downcase
|
99
|
-
if level_map.key?(sym_level)
|
100
|
-
@log_level = level_map[sym_level]
|
101
|
-
else
|
102
|
-
raise ArgumentError, "Invalid log level: #{level}. Valid levels are: #{level_map.keys.join(', ')}"
|
103
|
-
end
|
47
|
+
def worker_class_for_queue(queue_url)
|
48
|
+
if worker = @queue_workers[queue_url]
|
49
|
+
puts "[SidekiqSqsProcessor] Found worker #{worker} for queue #{queue_url}"
|
104
50
|
else
|
105
|
-
|
106
|
-
end
|
107
|
-
end
|
108
|
-
def handle_error(error, context = {})
|
109
|
-
if @error_handler
|
110
|
-
@error_handler.call(error, context)
|
111
|
-
else
|
112
|
-
default_error_handler.call(error, context)
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
def validate!
|
117
|
-
errors = []
|
118
|
-
|
119
|
-
# AWS Config
|
120
|
-
if aws_region.nil? || aws_region.empty?
|
121
|
-
errors << "aws_region is not configured"
|
122
|
-
end
|
123
|
-
|
124
|
-
# Validate thread and message settings before queue URLs
|
125
|
-
if !poller_thread_count.positive?
|
126
|
-
errors << "poller_thread_count must be positive"
|
127
|
-
end
|
128
|
-
|
129
|
-
if !max_number_of_messages.between?(1, 10)
|
130
|
-
errors << "max_number_of_messages must be between 1 and 10"
|
131
|
-
end
|
132
|
-
|
133
|
-
if !wait_time_seconds.between?(0, 20)
|
134
|
-
errors << "wait_time_seconds must be between 0 and 20"
|
135
|
-
end
|
136
|
-
|
137
|
-
if !visibility_timeout.positive?
|
138
|
-
errors << "visibility_timeout must be positive"
|
139
|
-
end
|
140
|
-
|
141
|
-
if ![:continuous, :scheduled].include?(polling_type)
|
142
|
-
errors << "polling_type must be :continuous or :scheduled"
|
143
|
-
end
|
144
|
-
|
145
|
-
if !polling_frequency.positive?
|
146
|
-
errors << "polling_frequency must be positive"
|
147
|
-
end
|
148
|
-
|
149
|
-
# Queue URLs should be checked last
|
150
|
-
if queue_urls.empty?
|
151
|
-
errors << "queue_urls is empty"
|
152
|
-
end
|
153
|
-
|
154
|
-
# Raise first specific error for single-error tests
|
155
|
-
if errors.length == 1
|
156
|
-
raise ArgumentError, errors.first
|
157
|
-
end
|
158
|
-
|
159
|
-
# Raise all errors together
|
160
|
-
if errors.any?
|
161
|
-
raise ArgumentError, "Invalid configuration: #{errors.join(', ')}"
|
162
|
-
end
|
163
|
-
|
164
|
-
true
|
165
|
-
end
|
166
|
-
|
167
|
-
private
|
168
|
-
|
169
|
-
def default_error_handler
|
170
|
-
->(error, context = {}) do
|
171
|
-
logger = self.logger || Sidekiq.logger
|
172
|
-
if error.is_a?(Exception)
|
173
|
-
logger.error(error.message)
|
174
|
-
logger.error(error.backtrace.join("\n")) if error.backtrace
|
175
|
-
else
|
176
|
-
logger.error(error.to_s)
|
177
|
-
end
|
178
|
-
logger.error("Context: #{context.inspect}") unless context.empty?
|
51
|
+
puts "[SidekiqSqsProcessor] No worker found for queue #{queue_url}"
|
179
52
|
end
|
53
|
+
worker
|
180
54
|
end
|
181
55
|
end
|
182
56
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'singleton'
|
2
4
|
|
3
5
|
module SidekiqSqsProcessor
|
@@ -5,89 +7,103 @@ module SidekiqSqsProcessor
|
|
5
7
|
include Singleton
|
6
8
|
|
7
9
|
def initialize
|
10
|
+
puts "[SidekiqSqsProcessor] Initializing ContinuousPoller"
|
8
11
|
@running = false
|
9
12
|
@threads = []
|
10
13
|
@mutex = Mutex.new
|
11
14
|
end
|
12
15
|
|
13
16
|
def start
|
17
|
+
puts "[SidekiqSqsProcessor] Starting ContinuousPoller"
|
18
|
+
puts "[SidekiqSqsProcessor] Current state: running=#{@running}, threads=#{@threads.count}"
|
19
|
+
|
14
20
|
return false if running?
|
15
21
|
|
16
22
|
@mutex.synchronize do
|
17
|
-
|
18
|
-
|
23
|
+
puts "[SidekiqSqsProcessor] Inside start mutex"
|
24
|
+
begin
|
25
|
+
@running = true
|
26
|
+
start_polling_threads
|
27
|
+
puts "[SidekiqSqsProcessor] Polling threads started successfully"
|
28
|
+
true
|
29
|
+
rescue => e
|
30
|
+
puts "[SidekiqSqsProcessor] ERROR starting polling threads: #{e.class} - #{e.message}"
|
31
|
+
puts "[SidekiqSqsProcessor] #{e.backtrace.join("\n")}"
|
32
|
+
@running = false
|
33
|
+
false
|
34
|
+
end
|
19
35
|
end
|
20
|
-
|
21
|
-
true
|
22
36
|
end
|
23
37
|
|
24
38
|
def stop
|
39
|
+
puts "[SidekiqSqsProcessor] Stopping ContinuousPoller"
|
25
40
|
return false unless running?
|
26
41
|
|
27
42
|
@mutex.synchronize do
|
28
|
-
|
29
|
-
|
43
|
+
begin
|
44
|
+
@running = false
|
45
|
+
stop_polling_threads
|
46
|
+
puts "[SidekiqSqsProcessor] Polling threads stopped successfully"
|
47
|
+
true
|
48
|
+
rescue => e
|
49
|
+
puts "[SidekiqSqsProcessor] ERROR stopping polling threads: #{e.class} - #{e.message}"
|
50
|
+
puts "[SidekiqSqsProcessor] #{e.backtrace.join("\n")}"
|
51
|
+
false
|
52
|
+
end
|
30
53
|
end
|
31
|
-
|
32
|
-
true
|
33
54
|
end
|
34
55
|
|
35
56
|
def running?
|
36
57
|
@running
|
37
58
|
end
|
38
59
|
|
39
|
-
def stats
|
40
|
-
{
|
41
|
-
running: running?,
|
42
|
-
threads: @threads.count,
|
43
|
-
queue_urls: SidekiqSqsProcessor.configuration.queue_urls
|
44
|
-
}
|
45
|
-
end
|
46
|
-
|
47
60
|
private
|
48
61
|
|
49
62
|
def start_polling_threads
|
63
|
+
puts "[SidekiqSqsProcessor] Starting polling threads"
|
64
|
+
puts "[SidekiqSqsProcessor] Queue URLs: #{SidekiqSqsProcessor.configuration.queue_urls.inspect}"
|
65
|
+
|
50
66
|
SidekiqSqsProcessor.configuration.queue_urls.each do |queue_url|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
67
|
+
thread = Thread.new do
|
68
|
+
Thread.current.name = "SQS-Poller-#{queue_url}"
|
69
|
+
puts "[SidekiqSqsProcessor] Started polling thread for queue: #{queue_url}"
|
70
|
+
|
71
|
+
poll_queue(queue_url) while running?
|
56
72
|
end
|
73
|
+
@threads << thread
|
57
74
|
end
|
58
75
|
end
|
59
76
|
|
60
77
|
def stop_polling_threads
|
78
|
+
puts "[SidekiqSqsProcessor] Stopping polling threads"
|
61
79
|
@threads.each(&:exit)
|
62
80
|
@threads.each(&:join)
|
63
81
|
@threads.clear
|
64
82
|
end
|
65
83
|
|
66
84
|
def poll_queue(queue_url)
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
85
|
+
begin
|
86
|
+
response = receive_messages(queue_url)
|
87
|
+
process_messages(response.messages, queue_url)
|
88
|
+
rescue StandardError => e
|
89
|
+
SidekiqSqsProcessor.handle_error(e, { queue_url: queue_url })
|
90
|
+
end
|
72
91
|
end
|
73
92
|
|
74
93
|
def receive_messages(queue_url)
|
75
94
|
SidekiqSqsProcessor.sqs_client.receive_message(
|
76
95
|
queue_url: queue_url,
|
77
|
-
max_number_of_messages:
|
78
|
-
visibility_timeout:
|
79
|
-
wait_time_seconds:
|
96
|
+
max_number_of_messages: 10,
|
97
|
+
visibility_timeout: 30,
|
98
|
+
wait_time_seconds: 1,
|
80
99
|
attribute_names: ["All"],
|
81
100
|
message_attribute_names: ["All"]
|
82
101
|
)
|
83
102
|
end
|
103
|
+
|
84
104
|
def process_messages(messages, queue_url)
|
85
105
|
messages.each do |message|
|
86
|
-
message_data = nil
|
87
|
-
worker_class = nil
|
88
|
-
|
89
106
|
begin
|
90
|
-
# Convert to a hash for passing to the worker
|
91
107
|
message_data = {
|
92
108
|
"message_id" => message.message_id,
|
93
109
|
"receipt_handle" => message.receipt_handle,
|
@@ -97,41 +113,35 @@ module SidekiqSqsProcessor
|
|
97
113
|
"md5_of_body" => message.md5_of_body,
|
98
114
|
"queue_url" => queue_url
|
99
115
|
}
|
100
|
-
|
101
|
-
worker_class = find_worker_for_message(message)
|
116
|
+
|
117
|
+
worker_class = find_worker_for_message(message, queue_url)
|
102
118
|
if worker_class
|
103
|
-
# Simply call perform_async, which will be handled appropriately in test vs prod
|
104
119
|
worker_class.perform_async(message_data)
|
120
|
+
else
|
121
|
+
SidekiqSqsProcessor.handle_error(
|
122
|
+
StandardError.new("No worker found for message"),
|
123
|
+
{ message: message_data, queue_url: queue_url }
|
124
|
+
)
|
105
125
|
end
|
106
126
|
rescue StandardError => e
|
107
|
-
|
108
|
-
data_to_pass = message_data || message
|
109
|
-
handle_worker_error(e, worker_class&.name, data_to_pass, queue_url)
|
127
|
+
SidekiqSqsProcessor.handle_error(e, { message: message, queue_url: queue_url })
|
110
128
|
end
|
111
129
|
end
|
112
130
|
end
|
113
|
-
|
114
|
-
def
|
115
|
-
|
116
|
-
queue_url: queue_url,
|
117
|
-
worker: worker_name || "Unknown",
|
118
|
-
message: message
|
119
|
-
}
|
120
|
-
SidekiqSqsProcessor.handle_error(error, context)
|
121
|
-
end
|
122
|
-
def find_worker_for_message(message)
|
123
|
-
# Default to using the queue name as the worker class name
|
124
|
-
# This can be overridden in subclasses for custom routing logic
|
131
|
+
|
132
|
+
def find_worker_for_message(message, queue_url)
|
133
|
+
# First try to get worker class from message attributes
|
125
134
|
worker_name = message.message_attributes&.dig("worker_class", "string_value")
|
126
|
-
worker_name ||= queue_name_to_worker_name(message.queue_url)
|
127
|
-
|
128
|
-
SidekiqSqsProcessor.find_worker_class(worker_name)
|
129
|
-
end
|
130
135
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
136
|
+
# If not found in message, use the configured mapping
|
137
|
+
worker_name ||= SidekiqSqsProcessor.configuration.worker_class_for_queue(queue_url)
|
138
|
+
|
139
|
+
# Convert string to class if needed
|
140
|
+
if worker_name.is_a?(String)
|
141
|
+
worker_name.constantize
|
142
|
+
else
|
143
|
+
worker_name
|
144
|
+
end
|
135
145
|
end
|
136
146
|
end
|
137
147
|
end
|
@@ -5,98 +5,92 @@ module SidekiqSqsProcessor
|
|
5
5
|
# Rails integration for SidekiqSqsProcessor
|
6
6
|
# Handles initialization, configuration, and lifecycle management
|
7
7
|
class Railtie < Rails::Railtie
|
8
|
-
|
8
|
+
config.before_initialize do |app|
|
9
|
+
puts "[SidekiqSqsProcessor] Loading Railtie..."
|
10
|
+
end
|
11
|
+
|
12
|
+
# Move initialization to after Rails has fully loaded to ensure
|
13
|
+
# all gems have had a chance to configure themselves
|
14
|
+
config.after_initialize do |app|
|
15
|
+
puts "[SidekiqSqsProcessor] Starting Rails initialization..."
|
16
|
+
|
9
17
|
# Set default logger to Rails logger if not specified
|
10
18
|
SidekiqSqsProcessor.configuration.logger ||= Rails.logger
|
11
|
-
|
12
|
-
# Disable polling in test environment by default
|
13
|
-
if Rails.env.test? && !ENV['ENABLE_SQS_POLLING_IN_TEST']
|
14
|
-
SidekiqSqsProcessor.configuration.polling_enabled = false
|
15
|
-
end
|
16
|
-
|
17
|
-
# Disable polling in development by default unless explicitly enabled
|
18
|
-
if Rails.env.development? && !ENV['ENABLE_SQS_POLLING_IN_DEV']
|
19
|
-
SidekiqSqsProcessor.configuration.polling_enabled = false
|
20
|
-
end
|
21
|
-
|
19
|
+
|
22
20
|
# Configure Sidekiq server middleware and lifecycle hooks
|
23
21
|
if defined?(Sidekiq)
|
22
|
+
puts "[SidekiqSqsProcessor] Configuring Sidekiq server..."
|
23
|
+
|
24
24
|
Sidekiq.configure_server do |config|
|
25
|
+
puts "[SidekiqSqsProcessor] Inside Sidekiq server configuration..."
|
26
|
+
|
25
27
|
# Start continuous poller when Sidekiq server starts
|
26
|
-
# Only if polling is enabled and type is continuous
|
27
28
|
config.on(:startup) do
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
#
|
34
|
-
|
35
|
-
|
36
|
-
|
29
|
+
puts "[SidekiqSqsProcessor] Sidekiq server starting up..."
|
30
|
+
|
31
|
+
# Only run the poller in the scheduler process if using Sidekiq Enterprise
|
32
|
+
# For regular Sidekiq, this will run in every process
|
33
|
+
if !defined?(Sidekiq::Enterprise) || Sidekiq.schedule?
|
34
|
+
# Initialize poller in a non-blocking way
|
35
|
+
Thread.new do
|
36
|
+
# Name the thread for easier debugging
|
37
|
+
Thread.current.name = "SQSPollerInit"
|
38
|
+
begin
|
39
|
+
puts "[SidekiqSqsProcessor] Initializing SQS poller in background thread..."
|
40
|
+
|
41
|
+
# Wait for up to 30 seconds for configuration to be ready
|
42
|
+
30.times do |i|
|
43
|
+
if SidekiqSqsProcessor.configuration.ready_for_polling?
|
44
|
+
puts "[SidekiqSqsProcessor] Configuration is ready, starting poller..."
|
45
|
+
SidekiqSqsProcessor.start_continuous_poller
|
46
|
+
puts "[SidekiqSqsProcessor] Poller started successfully"
|
47
|
+
break
|
48
|
+
else
|
49
|
+
puts "[SidekiqSqsProcessor] Waiting for configuration to be ready... (#{i+1}/30)" if i % 5 == 0
|
50
|
+
sleep 1
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Final check after timeout
|
55
|
+
unless SidekiqSqsProcessor.configuration.ready_for_polling?
|
56
|
+
puts "[SidekiqSqsProcessor] WARNING: Configuration not ready after 30 seconds"
|
57
|
+
puts "[SidekiqSqsProcessor] WARNING: SQS polling will not start"
|
58
|
+
puts "[SidekiqSqsProcessor] WARNING: Current configuration state:"
|
59
|
+
puts "[SidekiqSqsProcessor] Queue workers: #{SidekiqSqsProcessor.configuration.queue_workers.inspect}"
|
60
|
+
end
|
61
|
+
rescue => e
|
62
|
+
puts "[SidekiqSqsProcessor] ERROR: Failed to start poller: #{e.class} - #{e.message}"
|
63
|
+
puts "[SidekiqSqsProcessor] ERROR: Backtrace: #{e.backtrace.join("\n")}"
|
64
|
+
# Log the error but don't crash the thread
|
65
|
+
end
|
37
66
|
end
|
67
|
+
else
|
68
|
+
puts "[SidekiqSqsProcessor] Skipping poller in non-scheduler process"
|
38
69
|
end
|
39
70
|
end
|
40
|
-
|
71
|
+
|
41
72
|
# Stop continuous poller when Sidekiq server shuts down
|
42
73
|
config.on(:shutdown) do
|
43
74
|
if SidekiqSqsProcessor.continuous_poller_running?
|
44
|
-
|
75
|
+
puts "[SidekiqSqsProcessor] Stopping continuous poller..."
|
45
76
|
SidekiqSqsProcessor.stop_continuous_poller
|
46
77
|
end
|
47
78
|
end
|
48
79
|
end
|
49
|
-
|
50
|
-
|
51
|
-
# Set up scheduled polling if enabled
|
52
|
-
if defined?(Sidekiq::Cron) &&
|
53
|
-
SidekiqSqsProcessor.configuration.polling_type == :scheduled &&
|
54
|
-
SidekiqSqsProcessor.configuration.polling_enabled
|
55
|
-
|
56
|
-
# Only set up in server mode
|
57
|
-
if Sidekiq.server?
|
58
|
-
frequency = SidekiqSqsProcessor.configuration.polling_frequency
|
59
|
-
|
60
|
-
# Convert seconds to cron expression
|
61
|
-
# Minimum 1 minute for cron
|
62
|
-
minutes = [frequency / 60, 1].max
|
63
|
-
cron_expression = minutes == 1 ? "* * * * *" : "*/#{minutes} * * * *"
|
64
|
-
|
65
|
-
# Create the cron job
|
66
|
-
Sidekiq::Cron::Job.create(
|
67
|
-
name: 'SQS Polling Job',
|
68
|
-
cron: cron_expression,
|
69
|
-
class: 'SidekiqSqsProcessor::ScheduledPoller',
|
70
|
-
queue: 'critical'
|
71
|
-
)
|
72
|
-
|
73
|
-
Rails.logger.info("Registered SidekiqSqsProcessor scheduled poller with cron: #{cron_expression}")
|
74
|
-
end
|
80
|
+
else
|
81
|
+
puts "[SidekiqSqsProcessor] WARNING: Sidekiq is not defined!"
|
75
82
|
end
|
76
83
|
end
|
77
|
-
|
84
|
+
|
78
85
|
# Expose rake tasks if available
|
79
86
|
rake_tasks do
|
80
87
|
load "tasks/sidekiq_sqs_processor_tasks.rake" if File.exist?(File.join(File.dirname(__FILE__), "../tasks/sidekiq_sqs_processor_tasks.rake"))
|
81
88
|
end
|
82
|
-
|
89
|
+
|
83
90
|
# Register Rails generators
|
84
91
|
generators do
|
85
|
-
require_relative "../generators/sidekiq_sqs_processor/install_generator"
|
86
|
-
require_relative "../generators/sidekiq_sqs_processor/worker_generator"
|
87
|
-
end
|
88
|
-
|
89
|
-
# Add local configuration options to Rails application configuration
|
90
|
-
config.after_initialize do |app|
|
91
|
-
# Validate configuration if in production
|
92
|
-
if Rails.env.production? && SidekiqSqsProcessor.configuration.polling_enabled
|
93
|
-
# Ensure configuration is valid
|
94
|
-
unless SidekiqSqsProcessor.configuration.valid?
|
95
|
-
Rails.logger.error("Invalid SidekiqSqsProcessor configuration")
|
96
|
-
SidekiqSqsProcessor.configuration.validate! # This will raise an error with details
|
97
|
-
end
|
98
|
-
end
|
92
|
+
require_relative "../generators/sidekiq_sqs_processor/install_generator" if File.exist?(File.join(File.dirname(__FILE__), "../generators/sidekiq_sqs_processor/install_generator.rb"))
|
93
|
+
require_relative "../generators/sidekiq_sqs_processor/worker_generator" if File.exist?(File.join(File.dirname(__FILE__), "../generators/sidekiq_sqs_processor/worker_generator.rb"))
|
99
94
|
end
|
100
95
|
end
|
101
96
|
end
|
102
|
-
|
@@ -9,7 +9,6 @@ require_relative 'sidekiq_sqs_processor/version'
|
|
9
9
|
require_relative 'sidekiq_sqs_processor/configuration'
|
10
10
|
require_relative 'sidekiq_sqs_processor/base_worker'
|
11
11
|
require_relative 'sidekiq_sqs_processor/continuous_poller'
|
12
|
-
require_relative 'sidekiq_sqs_processor/continuous_poller'
|
13
12
|
require_relative 'sidekiq_sqs_processor/railtie' if defined?(Rails)
|
14
13
|
|
15
14
|
# Main module for the SidekiqSqsProcessor gem
|
@@ -17,13 +16,13 @@ require_relative 'sidekiq_sqs_processor/railtie' if defined?(Rails)
|
|
17
16
|
module SidekiqSqsProcessor
|
18
17
|
class << self
|
19
18
|
attr_writer :configuration
|
20
|
-
|
19
|
+
|
21
20
|
# Get the current configuration
|
22
21
|
# @return [SidekiqSqsProcessor::Configuration]
|
23
22
|
def configuration
|
24
23
|
@configuration ||= Configuration.new
|
25
24
|
end
|
26
|
-
|
25
|
+
|
27
26
|
# Configure the gem
|
28
27
|
# @yield [config] Gives the configuration object to the block
|
29
28
|
# @example
|
@@ -33,93 +32,79 @@ module SidekiqSqsProcessor
|
|
33
32
|
# end
|
34
33
|
def configure
|
35
34
|
yield(configuration)
|
35
|
+
configuration.validate!
|
36
36
|
end
|
37
|
-
|
37
|
+
|
38
38
|
# Start the continuous poller
|
39
39
|
# @return [Boolean] Whether the poller was started
|
40
40
|
def start_continuous_poller
|
41
41
|
ContinuousPoller.instance.start
|
42
42
|
end
|
43
|
-
|
43
|
+
|
44
44
|
# Stop the continuous poller
|
45
45
|
# @return [Boolean] Whether the poller was stopped
|
46
46
|
def stop_continuous_poller
|
47
47
|
ContinuousPoller.instance.stop
|
48
48
|
end
|
49
|
-
|
49
|
+
|
50
50
|
# Check if the continuous poller is running
|
51
51
|
# @return [Boolean] Whether the poller is running
|
52
52
|
def continuous_poller_running?
|
53
53
|
ContinuousPoller.instance.running?
|
54
54
|
end
|
55
|
-
|
55
|
+
|
56
56
|
# Get statistics about the continuous poller
|
57
57
|
# @return [Hash] Statistics about the poller threads
|
58
58
|
def continuous_poller_stats
|
59
59
|
ContinuousPoller.instance.stats
|
60
60
|
end
|
61
|
-
|
61
|
+
|
62
62
|
# Get the AWS SQS client
|
63
63
|
# @return [Aws::SQS::Client]
|
64
64
|
def sqs_client
|
65
|
-
@sqs_client ||=
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
def sqs_client=(client)
|
71
|
-
@sqs_client = client
|
72
|
-
end
|
73
|
-
|
74
|
-
# Create a new SQS client based on configuration
|
75
|
-
# @return [Aws::SQS::Client] The AWS SQS client
|
76
|
-
def create_sqs_client
|
77
|
-
options = { region: configuration.aws_region }
|
78
|
-
|
79
|
-
# Use custom credentials if provided
|
80
|
-
if configuration.aws_credentials
|
81
|
-
options[:credentials] = configuration.aws_credentials
|
82
|
-
elsif configuration.aws_access_key_id && configuration.aws_secret_access_key
|
83
|
-
options[:access_key_id] = configuration.aws_access_key_id
|
84
|
-
options[:secret_access_key] = configuration.aws_secret_access_key
|
85
|
-
end
|
86
|
-
|
87
|
-
Aws::SQS::Client.new(options)
|
65
|
+
@sqs_client ||= Aws::SQS::Client.new(
|
66
|
+
region: configuration.aws_region,
|
67
|
+
access_key_id: configuration.aws_access_key_id,
|
68
|
+
secret_access_key: configuration.aws_secret_access_key
|
69
|
+
)
|
88
70
|
end
|
89
|
-
|
71
|
+
|
90
72
|
# Get the logger
|
91
73
|
# @return [Logger] The logger
|
92
74
|
def logger
|
93
75
|
configuration.logger || Sidekiq.logger
|
94
76
|
end
|
95
|
-
|
77
|
+
|
96
78
|
# Reset the configuration and clients
|
97
79
|
# Used primarily for testing
|
98
80
|
def reset!
|
99
81
|
@configuration = nil
|
100
82
|
@sqs_client = nil
|
101
83
|
end
|
102
|
-
|
84
|
+
|
103
85
|
# Validate the current configuration
|
104
86
|
# @return [Boolean] Whether the configuration is valid
|
105
87
|
# @raise [ArgumentError] If the configuration is invalid
|
106
88
|
def validate_configuration!
|
107
89
|
configuration.validate!
|
108
90
|
end
|
109
|
-
|
91
|
+
|
110
92
|
# Find all worker classes that inherit from SidekiqSqsProcessor::BaseWorker
|
111
93
|
# @return [Array<Class>] Array of worker classes
|
112
94
|
def worker_classes
|
113
95
|
ObjectSpace.each_object(Class).select { |c| c < BaseWorker rescue false }
|
114
96
|
end
|
115
|
-
|
97
|
+
|
116
98
|
# Get a worker class by name
|
117
99
|
# @param name [String] The worker class name
|
118
100
|
# @return [Class, nil] The worker class or nil if not found
|
119
|
-
def find_worker_class(
|
120
|
-
|
101
|
+
def find_worker_class(worker_name)
|
102
|
+
return nil unless worker_name
|
103
|
+
worker_name.constantize
|
104
|
+
rescue NameError
|
105
|
+
nil
|
121
106
|
end
|
122
|
-
|
107
|
+
|
123
108
|
# Enqueue a message directly to a specific worker
|
124
109
|
# @param worker_class [Class, String] The worker class or name
|
125
110
|
# @param message_body [Hash, String] The message body
|
@@ -128,12 +113,12 @@ module SidekiqSqsProcessor
|
|
128
113
|
def enqueue_message(worker_class, message_body, options = {})
|
129
114
|
# If worker_class is a string, convert to actual class
|
130
115
|
worker_class = Object.const_get(worker_class) if worker_class.is_a?(String)
|
131
|
-
|
116
|
+
|
132
117
|
# Ensure the worker is a SidekiqSqsProcessor::BaseWorker
|
133
118
|
unless worker_class < BaseWorker
|
134
119
|
raise ArgumentError, "Worker class must inherit from SidekiqSqsProcessor::BaseWorker"
|
135
120
|
end
|
136
|
-
|
121
|
+
|
137
122
|
# Create a simulated SQS message
|
138
123
|
message_data = {
|
139
124
|
'message_id' => SecureRandom.uuid,
|
@@ -142,20 +127,25 @@ module SidekiqSqsProcessor
|
|
142
127
|
'message_attributes' => options[:message_attributes] || {},
|
143
128
|
'enqueued_at' => Time.now.to_f
|
144
129
|
}
|
145
|
-
|
130
|
+
|
146
131
|
# Special handling for receipt_handle and queue_url if used for testing
|
147
132
|
message_data['receipt_handle'] = options[:receipt_handle] if options[:receipt_handle]
|
148
133
|
message_data['queue_url'] = options[:queue_url] if options[:queue_url]
|
149
|
-
|
134
|
+
|
150
135
|
# Enqueue to Sidekiq
|
151
136
|
worker_class.perform_async(message_data)
|
152
137
|
end
|
153
|
-
|
138
|
+
|
154
139
|
# Handle an error using the configured error handler
|
155
140
|
# @param error [Exception] The error to handle
|
156
141
|
# @param context [Hash] Additional context for the error
|
157
142
|
def handle_error(error, context = {})
|
158
|
-
configuration.
|
143
|
+
if configuration.error_handler
|
144
|
+
configuration.error_handler.call(error, context)
|
145
|
+
else
|
146
|
+
logger = configuration.logger || Sidekiq.logger
|
147
|
+
logger.error("SQS Error: #{error.message}\nContext: #{context.inspect}")
|
148
|
+
end
|
159
149
|
end
|
160
150
|
end
|
161
151
|
end
|