sidekiq_sqs_processor 0.1.1 → 0.1.3

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: d0ff0052092b07dd5382e15eeb24602e1bdb03702d588a91fa0f5e374f705df4
4
- data.tar.gz: 24e0fa2d29c6d3714ed747671bba3c09f712d9ad57fa0295a84db092af07404f
3
+ metadata.gz: fdadc44e3df0c0ff5d8a09102f83afedb6f0c571c988b228138e8134947e6338
4
+ data.tar.gz: 74101646e239ec7bbd8c57ef224f957c1c046be760600e6291e7573feae4c818
5
5
  SHA512:
6
- metadata.gz: 7a455adcec4f646834b089ec75ab934b6be6bc238d2b570bd25499736fdb7ef7b75c68f6ad58aabd9f9f053a64222da4efb4388dddb031c4411141fca876fd79
7
- data.tar.gz: 5f50146da3c8569ea44008a670446736139fdeace823fff3eb715c59b3748aa10c209cad8d433dc7a29c93fe52a1eca95f6a7f451e1f784235a24dd0666e1ca0
6
+ metadata.gz: 99da0bcadb9f8b16ad61fde81dc03623230e1f72904b62440b70bfc9f85e32cd7027774d747dcaf38596619a0d6d93ec39879da20bad0a18aae90e9fca6feb0c
7
+ data.tar.gz: e1c9755983ea3a5cadeae2e5ae82c29a0ee4fbba877005bc45a59695854259391b9031179abdffdae6d2a0379905c7ba7e76403f97a8b1f50d066e53cd585b6f
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 'sidekiq_sqs_processor'
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 and process the message
15
+ # Parse the SQS message body
44
16
  body = JSON.parse(message_data["body"])
45
- process_message(body)
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, message_data, "Failed to parse message body")
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, message_data, "SQS service error")
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, message_data, "Error processing message")
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, message_data, "Failed to delete message from SQS")
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: 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,62 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module SidekiqSqsProcessor
2
4
  class Configuration
3
- # AWS credentials
4
- attr_accessor :aws_access_key_id, :aws_secret_access_key, :aws_credentials
5
- attr_reader :aws_region, :queue_urls, :max_number_of_messages,
6
- :visibility_timeout, :wait_time_seconds, :poller_thread_count,
7
- :error_handler, :polling_enabled, :polling_type, :poll_on_startup,
8
- :polling_frequency, :log_level
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
- @aws_region = "us-east-1"
15
- @queue_urls = []
16
- @max_number_of_messages = 10
17
- @visibility_timeout = 30
18
- @wait_time_seconds = 20
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
32
- end
33
-
34
- def queue_urls=(urls)
35
- @queue_urls = Array(urls).map(&: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
36
18
  end
37
19
 
38
- def add_queue_url(url)
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
- def visibility_timeout=(value)
48
- @visibility_timeout = value.to_i
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 wait_time_seconds=(value)
52
- @wait_time_seconds = value.to_i
39
+ def ready_for_polling?
40
+ !@queue_workers.empty? && validate!(strict: false)
53
41
  end
54
42
 
55
- def poller_thread_count=(value)
56
- @poller_thread_count = value.to_i
43
+ def queue_urls
44
+ @queue_workers.keys
57
45
  end
58
46
 
59
- def error_handler=(handler)
60
- if !handler.respond_to?(:call) && !handler.nil?
61
- raise ArgumentError, "Error handler must be callable (respond to #call)"
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
104
- else
105
- raise ArgumentError, "Log level must be an Integer (0-5) or one of: debug, info, warn, error, fatal, unknown"
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
47
+ def register_worker(queue_url, worker_class)
48
+ @queue_workers[queue_url] = worker_class
49
+ puts "[SidekiqSqsProcessor] Registered worker #{worker_class} for queue #{queue_url}"
114
50
  end
115
51
 
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
52
+ def worker_class_for_queue(queue_url)
53
+ worker = @queue_workers[queue_url]
144
54
 
145
- if !polling_frequency.positive?
146
- errors << "polling_frequency must be positive"
55
+ if worker.nil?
56
+ puts "[SidekiqSqsProcessor] No worker found for queue #{queue_url}"
147
57
  end
148
58
 
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?
179
- end
59
+ worker
180
60
  end
181
61
  end
182
62
  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
- @running = true
18
- start_polling_threads
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
- @running = false
29
- stop_polling_threads
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
- SidekiqSqsProcessor.configuration.poller_thread_count.times do
52
- thread = Thread.new do
53
- poll_queue(queue_url) while running?
54
- end
55
- @threads << thread
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
- response = receive_messages(queue_url)
68
- process_messages(response.messages, queue_url)
69
- rescue StandardError => e
70
- SidekiqSqsProcessor.handle_error(e, { queue_url: queue_url })
71
- sleep(1) # Brief pause before retrying
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: SidekiqSqsProcessor.configuration.max_number_of_messages,
78
- visibility_timeout: SidekiqSqsProcessor.configuration.visibility_timeout,
79
- wait_time_seconds: SidekiqSqsProcessor.configuration.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
- # Handle worker error without re-raising
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 handle_worker_error(error, worker_name, message, queue_url)
115
- context = {
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
- def queue_name_to_worker_name(queue_url)
132
- # Convert 'my-queue-name' to 'MyQueueNameWorker'
133
- queue_name = queue_url.split('/').last
134
- "#{queue_name.split('-').map(&:capitalize).join}Worker"
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
- initializer "sidekiq_sqs_processor.configure_rails_initialization" do |app|
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
- if SidekiqSqsProcessor.configuration.polling_type == :continuous &&
29
- SidekiqSqsProcessor.configuration.polling_enabled &&
30
- SidekiqSqsProcessor.configuration.poll_on_startup
31
-
32
- # Only run the poller in the scheduler process if using Sidekiq Enterprise
33
- # For regular Sidekiq, this will run in every process
34
- if !defined?(Sidekiq::Enterprise) || Sidekiq.schedule?
35
- Rails.logger.info("Starting SidekiqSqsProcessor continuous poller")
36
- SidekiqSqsProcessor.start_continuous_poller
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
- Rails.logger.info("Stopping SidekiqSqsProcessor continuous poller")
75
+ puts "[SidekiqSqsProcessor] Stopping continuous poller..."
45
76
  SidekiqSqsProcessor.stop_continuous_poller
46
77
  end
47
78
  end
48
79
  end
49
- end
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
-
@@ -1,5 +1,5 @@
1
1
 
2
2
  module SidekiqSqsProcessor
3
- VERSION = "0.1.1"
3
+ VERSION = "0.1.3"
4
4
  end
5
5
 
@@ -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 ||= create_sqs_client
66
- end
67
-
68
- # Explicitly set the SQS client (primarily for testing)
69
- # @param client [Aws::SQS::Client] The SQS client to use
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(name)
120
- Object.const_get(name) rescue nil
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.handle_error(error, context)
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sidekiq_sqs_processor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Unnikrishnan KP