shoryuken 6.2.1 → 7.0.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.
Files changed (205) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/push.yml +36 -0
  3. data/.github/workflows/specs.yml +49 -44
  4. data/.github/workflows/verify-action-pins.yml +16 -0
  5. data/.gitignore +4 -1
  6. data/.rspec +3 -1
  7. data/.rubocop.yml +6 -1
  8. data/.ruby-version +1 -0
  9. data/.yard-lint.yml +279 -0
  10. data/CHANGELOG.md +308 -139
  11. data/Gemfile +1 -8
  12. data/Gemfile.lint +9 -0
  13. data/Gemfile.lint.lock +69 -0
  14. data/README.md +16 -33
  15. data/Rakefile +6 -10
  16. data/bin/clean_sqs +52 -0
  17. data/bin/cli/base.rb +22 -2
  18. data/bin/cli/sqs.rb +74 -7
  19. data/bin/integrations +275 -0
  20. data/bin/scenario +154 -0
  21. data/bin/shoryuken +3 -2
  22. data/docker-compose.yml +6 -0
  23. data/lib/{shoryuken/extensions/active_job_extensions.rb → active_job/extensions.rb} +20 -6
  24. data/lib/active_job/queue_adapters/shoryuken_adapter.rb +208 -0
  25. data/lib/active_job/queue_adapters/shoryuken_concurrent_send_adapter.rb +78 -0
  26. data/lib/shoryuken/active_job/current_attributes.rb +139 -0
  27. data/lib/shoryuken/active_job/job_wrapper.rb +28 -0
  28. data/lib/shoryuken/body_parser.rb +11 -1
  29. data/lib/shoryuken/client.rb +16 -0
  30. data/lib/shoryuken/default_exception_handler.rb +11 -0
  31. data/lib/shoryuken/default_worker_registry.rb +39 -11
  32. data/lib/shoryuken/environment_loader.rb +85 -15
  33. data/lib/shoryuken/errors.rb +36 -0
  34. data/lib/shoryuken/fetcher.rb +41 -3
  35. data/lib/shoryuken/helpers/atomic_boolean.rb +58 -0
  36. data/lib/shoryuken/helpers/atomic_counter.rb +104 -0
  37. data/lib/shoryuken/helpers/atomic_hash.rb +182 -0
  38. data/lib/shoryuken/helpers/hash_utils.rb +56 -0
  39. data/lib/shoryuken/helpers/string_utils.rb +65 -0
  40. data/lib/shoryuken/helpers/timer_task.rb +80 -0
  41. data/lib/shoryuken/inline_message.rb +22 -0
  42. data/lib/shoryuken/launcher.rb +55 -0
  43. data/lib/shoryuken/logging/base.rb +26 -0
  44. data/lib/shoryuken/logging/pretty.rb +25 -0
  45. data/lib/shoryuken/logging/without_timestamp.rb +25 -0
  46. data/lib/shoryuken/logging.rb +43 -15
  47. data/lib/shoryuken/manager.rb +84 -5
  48. data/lib/shoryuken/message.rb +116 -1
  49. data/lib/shoryuken/middleware/chain.rb +141 -43
  50. data/lib/shoryuken/middleware/entry.rb +30 -0
  51. data/lib/shoryuken/middleware/server/active_record.rb +10 -0
  52. data/lib/shoryuken/middleware/server/auto_delete.rb +12 -0
  53. data/lib/shoryuken/middleware/server/auto_extend_visibility.rb +37 -11
  54. data/lib/shoryuken/middleware/server/exponential_backoff_retry.rb +34 -3
  55. data/lib/shoryuken/middleware/server/non_retryable_exception.rb +95 -0
  56. data/lib/shoryuken/middleware/server/timing.rb +13 -0
  57. data/lib/shoryuken/options.rb +154 -13
  58. data/lib/shoryuken/polling/base_strategy.rb +127 -0
  59. data/lib/shoryuken/polling/queue_configuration.rb +103 -0
  60. data/lib/shoryuken/polling/strict_priority.rb +41 -0
  61. data/lib/shoryuken/polling/weighted_round_robin.rb +44 -0
  62. data/lib/shoryuken/processor.rb +37 -3
  63. data/lib/shoryuken/queue.rb +99 -8
  64. data/lib/shoryuken/runner.rb +54 -16
  65. data/lib/shoryuken/util.rb +32 -7
  66. data/lib/shoryuken/version.rb +4 -1
  67. data/lib/shoryuken/worker/default_executor.rb +23 -1
  68. data/lib/shoryuken/worker/inline_executor.rb +33 -2
  69. data/lib/shoryuken/worker.rb +224 -0
  70. data/lib/shoryuken/worker_registry.rb +35 -0
  71. data/lib/shoryuken.rb +27 -38
  72. data/renovate.json +62 -0
  73. data/shoryuken.gemspec +8 -4
  74. data/spec/integration/.rspec +1 -0
  75. data/spec/integration/active_job/adapter_configuration/configuration_spec.rb +26 -0
  76. data/spec/integration/active_job/bulk_enqueue/bulk_enqueue_spec.rb +53 -0
  77. data/spec/integration/active_job/current_attributes/bulk_enqueue_spec.rb +50 -0
  78. data/spec/integration/active_job/current_attributes/complex_types_spec.rb +55 -0
  79. data/spec/integration/active_job/current_attributes/empty_context_spec.rb +41 -0
  80. data/spec/integration/active_job/current_attributes/full_context_spec.rb +63 -0
  81. data/spec/integration/active_job/current_attributes/partial_context_spec.rb +57 -0
  82. data/spec/integration/active_job/custom_attributes/number_attributes_spec.rb +37 -0
  83. data/spec/integration/active_job/custom_attributes/string_attributes_spec.rb +39 -0
  84. data/spec/integration/active_job/error_handling/job_wrapper_spec.rb +53 -0
  85. data/spec/integration/active_job/fifo_and_attributes/deduplication_spec.rb +86 -0
  86. data/spec/integration/active_job/keyword_arguments/keyword_arguments_spec.rb +63 -0
  87. data/spec/integration/active_job/retry/discard_on_spec.rb +43 -0
  88. data/spec/integration/active_job/retry/retry_on_spec.rb +36 -0
  89. data/spec/integration/active_job/roundtrip/roundtrip_spec.rb +52 -0
  90. data/spec/integration/active_job/scheduled/scheduled_spec.rb +76 -0
  91. data/spec/integration/active_record_middleware/active_record_middleware_spec.rb +84 -0
  92. data/spec/integration/auto_delete/auto_delete_spec.rb +53 -0
  93. data/spec/integration/auto_extend_visibility/auto_extend_visibility_spec.rb +57 -0
  94. data/spec/integration/aws_config/aws_config_spec.rb +59 -0
  95. data/spec/integration/batch_processing/batch_processing_spec.rb +37 -0
  96. data/spec/integration/body_parser/json_parser_spec.rb +45 -0
  97. data/spec/integration/body_parser/proc_parser_spec.rb +54 -0
  98. data/spec/integration/body_parser/text_parser_spec.rb +43 -0
  99. data/spec/integration/concurrent_processing/concurrent_processing_spec.rb +45 -0
  100. data/spec/integration/custom_group_polling_strategy/custom_group_polling_strategy_spec.rb +87 -0
  101. data/spec/integration/dead_letter_queue/dead_letter_queue_spec.rb +91 -0
  102. data/spec/integration/exception_handlers/exception_handlers_spec.rb +69 -0
  103. data/spec/integration/exponential_backoff/exponential_backoff_spec.rb +67 -0
  104. data/spec/integration/fifo_ordering/fifo_ordering_spec.rb +44 -0
  105. data/spec/integration/large_payloads/large_payloads_spec.rb +30 -0
  106. data/spec/integration/launcher/launcher_spec.rb +40 -0
  107. data/spec/integration/message_attributes/message_attributes_spec.rb +54 -0
  108. data/spec/integration/message_operations/message_operations_spec.rb +59 -0
  109. data/spec/integration/middleware_chain/empty_chain_spec.rb +11 -0
  110. data/spec/integration/middleware_chain/execution_order_spec.rb +33 -0
  111. data/spec/integration/middleware_chain/removal_spec.rb +31 -0
  112. data/spec/integration/middleware_chain/short_circuit_spec.rb +40 -0
  113. data/spec/integration/non_retryable_exception/non_retryable_exception_spec.rb +149 -0
  114. data/spec/integration/polling_strategies/polling_strategies_spec.rb +46 -0
  115. data/spec/integration/queue_operations/queue_operations_spec.rb +84 -0
  116. data/spec/integration/rails/rails_72/Gemfile +6 -0
  117. data/spec/integration/rails/rails_72/activejob_adapter_spec.rb +98 -0
  118. data/spec/integration/rails/rails_80/Gemfile +6 -0
  119. data/spec/integration/rails/rails_80/activejob_adapter_spec.rb +98 -0
  120. data/spec/integration/rails/rails_80/continuation_spec.rb +79 -0
  121. data/spec/integration/rails/rails_81/Gemfile +6 -0
  122. data/spec/integration/rails/rails_81/activejob_adapter_spec.rb +98 -0
  123. data/spec/integration/rails/rails_81/continuation_spec.rb +79 -0
  124. data/spec/integration/retry_behavior/retry_behavior_spec.rb +45 -0
  125. data/spec/integration/spec_helper.rb +7 -0
  126. data/spec/integration/strict_priority_polling/strict_priority_polling_spec.rb +58 -0
  127. data/spec/integration/visibility_timeout/visibility_timeout_spec.rb +37 -0
  128. data/spec/integration/worker_enqueueing/worker_enqueueing_spec.rb +60 -0
  129. data/spec/integration/worker_groups/worker_groups_spec.rb +79 -0
  130. data/spec/integration/worker_lifecycle/worker_lifecycle_spec.rb +33 -0
  131. data/spec/integrations_helper.rb +243 -0
  132. data/spec/lib/active_job/extensions_spec.rb +225 -0
  133. data/spec/lib/active_job/queue_adapters/shoryuken_adapter_spec.rb +29 -0
  134. data/spec/{shoryuken/extensions/active_job_concurrent_send_adapter_spec.rb → lib/active_job/queue_adapters/shoryuken_concurrent_send_adapter_spec.rb} +5 -4
  135. data/spec/{shoryuken/extensions/active_job_wrapper_spec.rb → lib/shoryuken/active_job/job_wrapper_spec.rb} +6 -5
  136. data/spec/{shoryuken → lib/shoryuken}/body_parser_spec.rb +2 -4
  137. data/spec/{shoryuken → lib/shoryuken}/client_spec.rb +1 -1
  138. data/spec/{shoryuken → lib/shoryuken}/default_exception_handler_spec.rb +9 -10
  139. data/spec/{shoryuken → lib/shoryuken}/default_worker_registry_spec.rb +1 -2
  140. data/spec/{shoryuken → lib/shoryuken}/environment_loader_spec.rb +10 -9
  141. data/spec/{shoryuken → lib/shoryuken}/fetcher_spec.rb +23 -26
  142. data/spec/lib/shoryuken/helpers/atomic_boolean_spec.rb +196 -0
  143. data/spec/lib/shoryuken/helpers/atomic_counter_spec.rb +177 -0
  144. data/spec/lib/shoryuken/helpers/atomic_hash_spec.rb +307 -0
  145. data/spec/lib/shoryuken/helpers/hash_utils_spec.rb +145 -0
  146. data/spec/lib/shoryuken/helpers/string_utils_spec.rb +124 -0
  147. data/spec/lib/shoryuken/helpers/timer_task_spec.rb +298 -0
  148. data/spec/lib/shoryuken/helpers_integration_spec.rb +96 -0
  149. data/spec/lib/shoryuken/inline_message_spec.rb +196 -0
  150. data/spec/{shoryuken → lib/shoryuken}/launcher_spec.rb +23 -2
  151. data/spec/lib/shoryuken/logging_spec.rb +242 -0
  152. data/spec/{shoryuken → lib/shoryuken}/manager_spec.rb +1 -2
  153. data/spec/lib/shoryuken/message_spec.rb +109 -0
  154. data/spec/{shoryuken → lib/shoryuken}/middleware/chain_spec.rb +1 -1
  155. data/spec/lib/shoryuken/middleware/entry_spec.rb +68 -0
  156. data/spec/lib/shoryuken/middleware/server/active_record_spec.rb +133 -0
  157. data/spec/{shoryuken → lib/shoryuken}/middleware/server/auto_delete_spec.rb +1 -1
  158. data/spec/{shoryuken → lib/shoryuken}/middleware/server/auto_extend_visibility_spec.rb +51 -1
  159. data/spec/{shoryuken → lib/shoryuken}/middleware/server/exponential_backoff_retry_spec.rb +1 -1
  160. data/spec/lib/shoryuken/middleware/server/non_retryable_exception_spec.rb +214 -0
  161. data/spec/{shoryuken → lib/shoryuken}/middleware/server/timing_spec.rb +1 -1
  162. data/spec/{shoryuken → lib/shoryuken}/options_spec.rb +49 -6
  163. data/spec/lib/shoryuken/polling/base_strategy_spec.rb +280 -0
  164. data/spec/lib/shoryuken/polling/queue_configuration_spec.rb +195 -0
  165. data/spec/{shoryuken → lib/shoryuken}/polling/strict_priority_spec.rb +1 -1
  166. data/spec/{shoryuken → lib/shoryuken}/polling/weighted_round_robin_spec.rb +1 -1
  167. data/spec/{shoryuken → lib/shoryuken}/processor_spec.rb +1 -1
  168. data/spec/{shoryuken → lib/shoryuken}/queue_spec.rb +2 -3
  169. data/spec/{shoryuken → lib/shoryuken}/runner_spec.rb +1 -3
  170. data/spec/{shoryuken → lib/shoryuken}/util_spec.rb +2 -2
  171. data/spec/lib/shoryuken/version_spec.rb +17 -0
  172. data/spec/{shoryuken → lib/shoryuken}/worker/default_executor_spec.rb +1 -1
  173. data/spec/lib/shoryuken/worker/inline_executor_spec.rb +105 -0
  174. data/spec/lib/shoryuken/worker_registry_spec.rb +63 -0
  175. data/spec/{shoryuken → lib/shoryuken}/worker_spec.rb +15 -11
  176. data/spec/{shoryuken_spec.rb → lib/shoryuken_spec.rb} +1 -1
  177. data/spec/shared_examples_for_active_job.rb +40 -15
  178. data/spec/spec_helper.rb +48 -2
  179. metadata +295 -101
  180. data/.codeclimate.yml +0 -20
  181. data/.devcontainer/Dockerfile +0 -17
  182. data/.devcontainer/base.Dockerfile +0 -43
  183. data/.devcontainer/devcontainer.json +0 -35
  184. data/.github/FUNDING.yml +0 -12
  185. data/.github/dependabot.yml +0 -6
  186. data/.github/workflows/stale.yml +0 -20
  187. data/.reek.yml +0 -5
  188. data/Appraisals +0 -42
  189. data/gemfiles/.gitignore +0 -1
  190. data/gemfiles/aws_sdk_core_2.gemfile +0 -21
  191. data/gemfiles/rails_4_2.gemfile +0 -20
  192. data/gemfiles/rails_5_2.gemfile +0 -21
  193. data/gemfiles/rails_6_0.gemfile +0 -21
  194. data/gemfiles/rails_6_1.gemfile +0 -21
  195. data/gemfiles/rails_7_0.gemfile +0 -22
  196. data/lib/shoryuken/core_ext.rb +0 -69
  197. data/lib/shoryuken/extensions/active_job_adapter.rb +0 -103
  198. data/lib/shoryuken/extensions/active_job_concurrent_send_adapter.rb +0 -50
  199. data/lib/shoryuken/polling/base.rb +0 -67
  200. data/shoryuken.jpg +0 -0
  201. data/spec/integration/launcher_spec.rb +0 -128
  202. data/spec/shoryuken/core_ext_spec.rb +0 -40
  203. data/spec/shoryuken/extensions/active_job_adapter_spec.rb +0 -7
  204. data/spec/shoryuken/extensions/active_job_base_spec.rb +0 -84
  205. data/spec/shoryuken/worker/inline_executor_spec.rb +0 -49
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoryuken
4
+ module Middleware
5
+ # Represents an entry in a middleware chain, storing the middleware class
6
+ # and any arguments needed for its instantiation.
7
+ #
8
+ # @api private
9
+ class Entry
10
+ # @return [Class] The middleware class this entry represents
11
+ attr_reader :klass
12
+
13
+ # Creates a new middleware entry.
14
+ #
15
+ # @param klass [Class] The middleware class
16
+ # @param args [Array] Arguments to pass to the middleware constructor
17
+ def initialize(klass, *args)
18
+ @klass = klass
19
+ @args = args
20
+ end
21
+
22
+ # Creates a new instance of the middleware class with the stored arguments.
23
+ #
24
+ # @return [Object] A new instance of the middleware class
25
+ def make_new
26
+ @klass.new(*@args)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,7 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Shoryuken
2
4
  module Middleware
5
+ # Server-side middleware that runs around message processing
3
6
  module Server
7
+ # Middleware that clears ActiveRecord connections after message processing.
8
+ # Ensures database connections are returned to the pool after each job.
4
9
  class ActiveRecord
10
+ # Processes a message and clears database connections afterwards
11
+ #
12
+ # @param _args [Array<Object>] middleware call arguments (unused)
13
+ # @yield continues to the next middleware in the chain
14
+ # @return [Object] return value from the next middleware or worker in the chain
5
15
  def call(*_args)
6
16
  yield
7
17
  ensure
@@ -1,7 +1,19 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Shoryuken
2
4
  module Middleware
3
5
  module Server
6
+ # Middleware that automatically deletes messages after successful processing.
7
+ # Only deletes messages when the worker has auto_delete enabled.
4
8
  class AutoDelete
9
+ # Processes a message and deletes it if auto_delete is enabled
10
+ #
11
+ # @param worker [Object] the worker instance
12
+ # @param queue [String] the queue name
13
+ # @param sqs_msg [Shoryuken::Message, Array<Shoryuken::Message>] the message or batch
14
+ # @param _body [Object] the parsed message body (unused)
15
+ # @yield continues to the next middleware in the chain
16
+ # @return [void]
5
17
  def call(worker, queue, sqs_msg, _body)
6
18
  yield
7
19
 
@@ -1,11 +1,24 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Shoryuken
2
4
  module Middleware
3
5
  module Server
6
+ # Middleware that automatically extends message visibility timeout during processing.
7
+ # Prevents messages from becoming visible to other consumers while still being processed.
4
8
  class AutoExtendVisibility
5
9
  include Util
6
10
 
11
+ # Number of seconds before timeout to extend visibility
7
12
  EXTEND_UPFRONT_SECONDS = 5
8
13
 
14
+ # Processes a message with automatic visibility timeout extension
15
+ #
16
+ # @param worker [Object] the worker instance
17
+ # @param queue [String] the queue name
18
+ # @param sqs_msg [Shoryuken::Message, Array<Shoryuken::Message>] the message or batch
19
+ # @param body [Object] the parsed message body
20
+ # @yield continues to the next middleware in the chain
21
+ # @return [void]
9
22
  def call(worker, queue, sqs_msg, body)
10
23
  return yield unless worker.class.auto_visibility_timeout?
11
24
 
@@ -22,28 +35,41 @@ module Shoryuken
22
35
 
23
36
  private
24
37
 
38
+ # Helper class for extending message visibility
25
39
  class MessageVisibilityExtender
26
40
  include Util
27
41
 
42
+ # Creates a timer task that extends message visibility
43
+ #
44
+ # @param _worker [Object] the worker instance (unused)
45
+ # @param queue [String] the queue name
46
+ # @param sqs_msg [Shoryuken::Message] the message
47
+ # @param _body [Object] the parsed message body (unused)
48
+ # @return [Shoryuken::Helpers::TimerTask] the timer task
28
49
  def auto_extend(_worker, queue, sqs_msg, _body)
29
50
  queue_visibility_timeout = Shoryuken::Client.queues(queue).visibility_timeout
30
51
 
31
- Concurrent::TimerTask.new(execution_interval: queue_visibility_timeout - EXTEND_UPFRONT_SECONDS) do
32
- begin
33
- logger.debug do
34
- "Extending message #{queue}/#{sqs_msg.message_id} visibility timeout by #{queue_visibility_timeout}s"
35
- end
36
-
37
- sqs_msg.change_visibility(visibility_timeout: queue_visibility_timeout)
38
- rescue => ex
39
- logger.error do
40
- "Could not auto extend the message #{queue}/#{sqs_msg.message_id} visibility timeout. Error: #{ex.message}"
41
- end
52
+ Shoryuken::Helpers::TimerTask.new(execution_interval: queue_visibility_timeout - EXTEND_UPFRONT_SECONDS) do
53
+ logger.debug do
54
+ "Extending message #{queue}/#{sqs_msg.message_id} visibility timeout by #{queue_visibility_timeout}s"
55
+ end
56
+
57
+ sqs_msg.change_visibility(visibility_timeout: queue_visibility_timeout)
58
+ rescue => e
59
+ logger.error do
60
+ "Could not auto extend the message #{queue}/#{sqs_msg.message_id} visibility timeout. Error: #{e.message}"
42
61
  end
43
62
  end
44
63
  end
45
64
  end
46
65
 
66
+ # Creates and starts a visibility extension timer
67
+ #
68
+ # @param worker [Object] the worker instance
69
+ # @param queue [String] the queue name
70
+ # @param sqs_msg [Shoryuken::Message] the message
71
+ # @param body [Object] the parsed message body
72
+ # @return [Shoryuken::Helpers::TimerTask] the started timer
47
73
  def auto_visibility_timer(worker, queue, sqs_msg, body)
48
74
  MessageVisibilityExtender.new.auto_extend(worker, queue, sqs_msg, body).tap(&:execute)
49
75
  end
@@ -1,9 +1,24 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Shoryuken
2
4
  module Middleware
3
5
  module Server
6
+ # Middleware that implements exponential backoff retry for failed messages.
7
+ # When a job fails, the message visibility timeout is adjusted based on
8
+ # configured retry intervals.
4
9
  class ExponentialBackoffRetry
5
10
  include Util
6
11
 
12
+ # Processes a message with exponential backoff retry on failure
13
+ #
14
+ # @param worker [Object] the worker instance
15
+ # @param _queue [String] the queue name (unused)
16
+ # @param sqs_msg [Shoryuken::Message, Array<Shoryuken::Message>] the message or batch
17
+ # @param _body [Object] the parsed message body (unused)
18
+ # @yield continues to the next middleware in the chain
19
+ # @return [void]
20
+ # @raise [StandardError] re-raises the original exception if retry intervals are not configured
21
+ # or if retry limit is exceeded
7
22
  def call(worker, _queue, sqs_msg, _body)
8
23
  return yield unless worker.class.exponential_backoff?
9
24
 
@@ -14,7 +29,7 @@ module Shoryuken
14
29
 
15
30
  started_at = Time.now
16
31
  yield
17
- rescue => ex
32
+ rescue => e
18
33
  retry_intervals = worker.class.get_shoryuken_options['retry_intervals']
19
34
 
20
35
  if retry_intervals.nil? || !handle_failure(sqs_msg, started_at, retry_intervals)
@@ -23,13 +38,18 @@ module Shoryuken
23
38
  raise
24
39
  end
25
40
 
26
- logger.warn { "Message #{sqs_msg.message_id} will attempt retry due to error: #{ex.message}" }
41
+ logger.warn { "Message #{sqs_msg.message_id} will attempt retry due to error: #{e.message}" }
27
42
  # since we didn't raise, lets log the backtrace for debugging purposes.
28
- logger.debug { ex.backtrace.join("\n") } unless ex.backtrace.nil?
43
+ logger.debug { e.backtrace.join("\n") } unless e.backtrace.nil?
29
44
  end
30
45
 
31
46
  private
32
47
 
48
+ # Gets the retry interval for a given attempt number
49
+ #
50
+ # @param retry_intervals [Array<Integer>, #call] the configured intervals or callable
51
+ # @param attempts [Integer] the current attempt number
52
+ # @return [Integer, nil] the interval in seconds or nil
33
53
  def get_interval(retry_intervals, attempts)
34
54
  return retry_intervals.call(attempts) if retry_intervals.respond_to?(:call)
35
55
 
@@ -40,12 +60,23 @@ module Shoryuken
40
60
  end
41
61
  end
42
62
 
63
+ # Calculates the next visibility timeout capped at SQS maximum
64
+ #
65
+ # @param interval [Integer] the desired interval
66
+ # @param started_at [Time] when processing started
67
+ # @return [Integer] the capped visibility timeout
43
68
  def next_visibility_timeout(interval, started_at)
44
69
  max_timeout = 43_200 - (Time.now - started_at).ceil - 1
45
70
  interval = max_timeout if interval > max_timeout
46
71
  interval.to_i
47
72
  end
48
73
 
74
+ # Handles a message failure by adjusting visibility timeout
75
+ #
76
+ # @param sqs_msg [Shoryuken::Message] the failed message
77
+ # @param started_at [Time] when processing started
78
+ # @param retry_intervals [Array<Integer>, #call] the configured intervals
79
+ # @return [Boolean] true if retry was scheduled, false otherwise
49
80
  def handle_failure(sqs_msg, started_at, retry_intervals)
50
81
  receive_count = sqs_msg.attributes['ApproximateReceiveCount'].to_i
51
82
 
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoryuken
4
+ module Middleware
5
+ module Server
6
+ # Middleware that handles non-retryable exceptions by deleting messages immediately.
7
+ # When a configured exception occurs, the message is deleted instead of being retried.
8
+ #
9
+ # Configure non-retryable exceptions per worker:
10
+ #
11
+ # class MyWorker
12
+ # include Shoryuken::Worker
13
+ #
14
+ # # Using exception classes
15
+ # shoryuken_options queue: 'my_queue',
16
+ # non_retryable_exceptions: [InvalidInputError, RecordNotFoundError]
17
+ #
18
+ # # Or using a lambda for dynamic classification
19
+ # shoryuken_options queue: 'my_queue',
20
+ # non_retryable_exceptions: ->(error) {
21
+ # error.is_a?(StandardError) && error.message.include?('permanent')
22
+ # }
23
+ #
24
+ # def perform(sqs_msg, body)
25
+ # # ...
26
+ # end
27
+ # end
28
+ class NonRetryableException
29
+ include Util
30
+
31
+ # Processes a message and handles non-retryable exceptions
32
+ #
33
+ # @param worker [Object] the worker instance
34
+ # @param queue [String] the queue name
35
+ # @param sqs_msg [Shoryuken::Message, Array<Shoryuken::Message>] the message or batch
36
+ # @param _body [Object] the parsed message body (unused)
37
+ # @yield continues to the next middleware in the chain
38
+ # @return [void]
39
+ def call(worker, queue, sqs_msg, _body)
40
+ yield
41
+ rescue => e
42
+ non_retryable_exceptions = worker.class.get_shoryuken_options['non_retryable_exceptions']
43
+
44
+ return raise unless non_retryable_exceptions
45
+
46
+ if non_retryable_exceptions.respond_to?(:call)
47
+ return raise unless non_retryable_exceptions.call(e)
48
+ else
49
+ exception_classes = Array(non_retryable_exceptions)
50
+ return raise unless exception_classes.any? { |klass| e.is_a?(klass) }
51
+ end
52
+
53
+ # Handle batch messages
54
+ messages = sqs_msg.is_a?(Array) ? sqs_msg : [sqs_msg]
55
+
56
+ logger.warn do
57
+ "Non-retryable exception #{e.class} occurred for message(s) #{messages.map(&:message_id).join(', ')}. " \
58
+ "Deleting message(s) immediately. Error: #{e.message}"
59
+ end
60
+
61
+ logger.debug { e.backtrace.join("\n") } if e.backtrace
62
+
63
+ # Delete the message(s) immediately
64
+ entries = messages.map.with_index { |message, i| { id: i.to_s, receipt_handle: message.receipt_handle } }
65
+
66
+ begin
67
+ queue_client = Shoryuken::Client.queues(queue)
68
+ delete_failed = queue_client.delete_messages(entries: entries)
69
+
70
+ # Check if deletion reported failures (returns true if any failed)
71
+ if delete_failed
72
+ logger.warn do
73
+ 'Failed to delete some messages for non-retryable exception on queue ' \
74
+ "'#{queue}'. " \
75
+ "Entries: #{entries.map { |e| { id: e[:id] } }.inspect}. " \
76
+ 'Some messages may remain in the queue and could be reprocessed.'
77
+ end
78
+ end
79
+ rescue => delete_error
80
+ logger.error do
81
+ 'Error deleting messages for non-retryable exception on queue ' \
82
+ "'#{queue}': #{delete_error.class} - #{delete_error.message}. " \
83
+ "Entries: #{entries.map { |e| { id: e[:id] } }.inspect}. " \
84
+ 'Messages may remain in the queue and could be reprocessed.'
85
+ end
86
+ logger.debug { delete_error.backtrace.join("\n") } if delete_error.backtrace
87
+ end
88
+
89
+ # Don't re-raise - the exception has been handled by deleting the message
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+
@@ -1,9 +1,22 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Shoryuken
2
4
  module Middleware
3
5
  module Server
6
+ # Middleware that logs timing information for message processing.
7
+ # Records start time, completion time, and warns if processing
8
+ # exceeds the queue's visibility timeout.
4
9
  class Timing
5
10
  include Util
6
11
 
12
+ # Processes a message while logging timing information
13
+ #
14
+ # @param _worker [Object] the worker instance (unused)
15
+ # @param queue [String] the queue name
16
+ # @param _sqs_msg [Shoryuken::Message] the message being processed (unused)
17
+ # @param _body [Object] the parsed message body (unused)
18
+ # @yield continues to the next middleware in the chain
19
+ # @return [void]
7
20
  def call(_worker, queue, _sqs_msg, _body)
8
21
  started_at = Time.now
9
22
 
@@ -1,6 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Shoryuken
4
+ # Stores and manages all Shoryuken configuration options.
5
+ # This class is used internally to hold settings for workers, queues,
6
+ # middleware, and other runtime configurations.
2
7
  class Options
8
+ # Default configuration values for Shoryuken
3
9
  DEFAULTS = {
10
+ thread_priority: -1,
4
11
  concurrency: 25,
5
12
  queues: [],
6
13
  aws: {},
@@ -16,12 +23,31 @@ module Shoryuken
16
23
  }
17
24
  }.freeze
18
25
 
19
- attr_accessor :active_job_queue_name_prefixing, :cache_visibility_timeout, :groups,
20
- :launcher_executor, :reloader, :enable_reloading,
21
- :start_callback, :stop_callback, :worker_executor, :worker_registry, :exception_handlers
22
- attr_writer :default_worker_options, :sqs_client
26
+ # @return [Boolean] whether to enable ActiveJob queue name prefixing
27
+ # @return [Boolean] whether to cache SQS visibility timeout
28
+ # @return [Hash{String => Hash}] the configured processing groups
29
+ # @return [Object] the executor used to launch workers
30
+ # @return [Proc] the code reloader proc for development environments
31
+ # @return [Boolean] whether code reloading is enabled
32
+ # @return [Proc, nil] callback to execute when server starts
33
+ # @return [Proc, nil] callback to execute when server stops
34
+ # @return [Class] the executor class for running workers
35
+ # @return [Shoryuken::WorkerRegistry] the registry for worker classes
36
+ # @return [Array<#call>] handlers for processing exceptions
37
+ attr_accessor :active_job_queue_name_prefixing, :cache_visibility_timeout,
38
+ :groups, :launcher_executor, :reloader, :enable_reloading,
39
+ :start_callback, :stop_callback, :worker_executor, :worker_registry,
40
+ :exception_handlers
41
+
42
+ # @return [Hash] the default options for workers
43
+ # @return [Aws::SQS::Client] the SQS client instance
44
+ # @return [Logger] the logger instance
45
+ attr_writer :default_worker_options, :sqs_client, :logger
46
+
47
+ # @return [Hash] options passed to SQS receive_message calls
23
48
  attr_reader :sqs_client_receive_message_opts
24
49
 
50
+ # Initializes a new Options instance with default values
25
51
  def initialize
26
52
  self.groups = {}
27
53
  self.worker_registry = DefaultWorkerRegistry.new
@@ -35,33 +61,63 @@ module Shoryuken
35
61
  @sqs_client_receive_message_opts ||= {}
36
62
  end
37
63
 
64
+ # Checks if ActiveJob is available
65
+ #
66
+ # @return [Boolean] true if ActiveJob is defined
38
67
  def active_job?
39
68
  defined?(::ActiveJob)
40
69
  end
41
70
 
42
- def add_group(group, concurrency = nil, delay: nil)
71
+ # Adds a processing group with the specified concurrency and delay
72
+ #
73
+ # @param group [String] the name of the group
74
+ # @param concurrency [Integer, nil] the number of concurrent workers for the group
75
+ # @param delay [Float, nil] the delay between polling cycles
76
+ # @param polling_strategy [Class, String, nil] the polling strategy class for the group
77
+ # @return [Hash] the group configuration
78
+ # @raise [Errors::InvalidPollingStrategyError] if polling_strategy is not a Class, String, or nil
79
+ def add_group(group, concurrency = nil, delay: nil, polling_strategy: nil)
80
+ unless polling_strategy.nil? || polling_strategy.is_a?(Class) || polling_strategy.is_a?(String)
81
+ raise Errors::InvalidPollingStrategyError, "#{polling_strategy} is not a valid polling_strategy"
82
+ end
83
+
43
84
  concurrency ||= options[:concurrency]
44
85
  delay ||= options[:delay]
45
86
 
46
87
  groups[group] ||= {
47
88
  concurrency: concurrency,
48
89
  delay: delay,
90
+ polling_strategy: polling_strategy,
49
91
  queues: []
50
92
  }
51
93
  end
52
94
 
95
+ # Adds a queue to a processing group with the specified weight
96
+ #
97
+ # @param queue [String] the name of the queue
98
+ # @param weight [Integer] the weight (priority) of the queue
99
+ # @param group [String] the name of the group to add the queue to
100
+ # @return [void]
53
101
  def add_queue(queue, weight, group)
54
102
  weight.times do
55
103
  groups[group][:queues] << queue
56
104
  end
57
105
  end
58
106
 
107
+ # Returns all queues from all groups
108
+ #
109
+ # @return [Array<String>] flat array of all queue names
59
110
  def ungrouped_queues
60
111
  groups.values.flat_map { |options| options[:queues] }
61
112
  end
62
113
 
114
+ # Returns the polling strategy class for a group
115
+ #
116
+ # @param group [String] the name of the group
117
+ # @return [Class] the polling strategy class to use
63
118
  def polling_strategy(group)
64
- strategy = (group == 'default' ? options : options[:groups].to_h[group]).to_h[:polling_strategy]
119
+ strategy = groups[group].to_h[:polling_strategy] ||
120
+ (group == 'default' ? options : options[:groups].to_h[group]).to_h[:polling_strategy]
65
121
  case strategy
66
122
  when 'WeightedRoundRobin', nil # Default case
67
123
  Polling::WeightedRoundRobin
@@ -71,57 +127,111 @@ module Shoryuken
71
127
  begin
72
128
  Object.const_get(strategy)
73
129
  rescue NameError
74
- raise ArgumentError, "#{strategy} is not a valid polling_strategy"
130
+ raise Errors::InvalidPollingStrategyError, "#{strategy} is not a valid polling_strategy"
75
131
  end
76
132
  when Class
77
133
  strategy
78
134
  end
79
135
  end
80
136
 
137
+ # Returns the polling delay for a group
138
+ #
139
+ # @param group [String] the name of the group
140
+ # @return [Float] the delay in seconds
81
141
  def delay(group)
82
142
  groups[group].to_h.fetch(:delay, options[:delay]).to_f
83
143
  end
84
144
 
145
+ # Returns the SQS client, initializing a default one if needed.
146
+ # Uses AWS configuration from options[:aws] if available.
147
+ #
148
+ # @return [Aws::SQS::Client] the SQS client
85
149
  def sqs_client
86
- @sqs_client ||= Aws::SQS::Client.new
150
+ @sqs_client ||= Aws::SQS::Client.new(options[:aws])
87
151
  end
88
152
 
153
+ # Sets the SQS client receive message options for the default group
154
+ #
155
+ # @param sqs_client_receive_message_opts [Hash] the options hash
156
+ # @return [Hash] the options hash
89
157
  def sqs_client_receive_message_opts=(sqs_client_receive_message_opts)
90
158
  @sqs_client_receive_message_opts['default'] = sqs_client_receive_message_opts
91
159
  end
92
160
 
161
+ # Returns the global options hash
162
+ #
163
+ # @return [Hash] the options hash
93
164
  def options
94
165
  @options ||= DEFAULTS.dup
95
166
  end
96
167
 
168
+ # Returns the logger instance
169
+ #
170
+ # @return [Logger] the logger
97
171
  def logger
98
- Shoryuken::Logging.logger
172
+ @logger ||= Shoryuken::Logging.logger
99
173
  end
100
174
 
175
+ # Returns the thread priority setting
176
+ #
177
+ # @return [Integer] the thread priority
178
+ def thread_priority
179
+ @thread_priority ||= options[:thread_priority]
180
+ end
181
+
182
+ # Sets the thread priority
183
+ #
184
+ # @param value [Integer] the thread priority value
185
+ # @return [Integer] the thread priority
186
+ attr_writer :thread_priority
187
+
188
+ # Registers a worker class with the worker registry
189
+ #
190
+ # @param args [Array] arguments to pass to the registry
191
+ # @return [void]
101
192
  def register_worker(*args)
102
193
  worker_registry.register_worker(*args)
103
194
  end
104
195
 
196
+ # Yields self if running as a server for server-specific configuration
197
+ #
198
+ # @yield [Shoryuken::Options] the options instance
199
+ # @return [void]
105
200
  def configure_server
106
201
  yield self if server?
107
202
  end
108
203
 
204
+ # Returns the server middleware chain
205
+ #
206
+ # @yield [Shoryuken::Middleware::Chain] the middleware chain for configuration
207
+ # @return [Shoryuken::Middleware::Chain] the server middleware chain
109
208
  def server_middleware
110
209
  @_server_chain ||= default_server_middleware
111
210
  yield @_server_chain if block_given?
112
211
  @_server_chain
113
212
  end
114
213
 
214
+ # Yields self unless running as a server for client-specific configuration
215
+ #
216
+ # @yield [Shoryuken::Options] the options instance
217
+ # @return [void]
115
218
  def configure_client
116
219
  yield self unless server?
117
220
  end
118
221
 
222
+ # Returns the client middleware chain
223
+ #
224
+ # @yield [Shoryuken::Middleware::Chain] the middleware chain for configuration
225
+ # @return [Shoryuken::Middleware::Chain] the client middleware chain
119
226
  def client_middleware
120
227
  @_client_chain ||= default_client_middleware
121
228
  yield @_client_chain if block_given?
122
229
  @_client_chain
123
230
  end
124
231
 
232
+ # Returns the default worker options hash
233
+ #
234
+ # @return [Hash{String => Object}] the default worker options
125
235
  def default_worker_options
126
236
  @default_worker_options ||= {
127
237
  'queue' => 'default',
@@ -133,46 +243,74 @@ module Shoryuken
133
243
  }
134
244
  end
135
245
 
246
+ # Registers a callback to run when the server starts
247
+ #
248
+ # @param block [Proc] the block to execute on start
249
+ # @return [void]
250
+ # @yield the block to execute on start
136
251
  def on_start(&block)
137
252
  self.start_callback = block
138
253
  end
139
254
 
255
+ # Registers a callback to run when the server stops
256
+ #
257
+ # @param block [Proc] the block to execute on stop
258
+ # @return [void]
259
+ # @yield the block to execute on stop
140
260
  def on_stop(&block)
141
261
  self.stop_callback = block
142
262
  end
143
263
 
144
- # Register a block to run at a point in the Shoryuken lifecycle.
145
- # :startup, :quiet, :shutdown or :stopped are valid events.
264
+ # Registers a block to run at a point in the Shoryuken lifecycle.
146
265
  #
266
+ # @param event [Symbol] the lifecycle event (:startup, :quiet, :shutdown, or :stopped)
267
+ # @param block [Proc] the block to execute for the event
268
+ # @return [void]
269
+ # @raise [ArgumentError] if event is not a Symbol or not a valid event name
270
+ # @yield the block to execute for the event
271
+ # @example
147
272
  # Shoryuken.configure_server do |config|
148
273
  # config.on(:shutdown) do
149
274
  # puts "Goodbye cruel world!"
150
275
  # end
151
276
  # end
152
277
  def on(event, &block)
153
- fail ArgumentError, "Symbols only please: #{event}" unless event.is_a?(Symbol)
154
- fail ArgumentError, "Invalid event name: #{event}" unless options[:lifecycle_events].key?(event)
278
+ raise Errors::InvalidEventError, "Symbols only please: #{event}" unless event.is_a?(Symbol)
279
+ raise Errors::InvalidEventError, "Invalid event name: #{event}" unless options[:lifecycle_events].key?(event)
155
280
 
156
281
  options[:lifecycle_events][event] << block
157
282
  end
158
283
 
284
+ # Checks if running as a server (CLI mode)
285
+ #
286
+ # @return [Boolean] true if Shoryuken::CLI is defined
159
287
  def server?
160
288
  defined?(Shoryuken::CLI)
161
289
  end
162
290
 
291
+ # Checks if visibility timeout caching is enabled
292
+ #
293
+ # @return [Boolean] true if caching is enabled
163
294
  def cache_visibility_timeout?
164
295
  @cache_visibility_timeout
165
296
  end
166
297
 
298
+ # Checks if ActiveJob queue name prefixing is enabled
299
+ #
300
+ # @return [Boolean] true if prefixing is enabled
167
301
  def active_job_queue_name_prefixing?
168
302
  @active_job_queue_name_prefixing
169
303
  end
170
304
 
171
305
  private
172
306
 
307
+ # Creates the default server middleware chain
308
+ #
309
+ # @return [Shoryuken::Middleware::Chain] the default middleware chain
173
310
  def default_server_middleware
174
311
  Middleware::Chain.new do |m|
175
312
  m.add Middleware::Server::Timing
313
+ m.add Middleware::Server::NonRetryableException
176
314
  m.add Middleware::Server::ExponentialBackoffRetry
177
315
  m.add Middleware::Server::AutoDelete
178
316
  m.add Middleware::Server::AutoExtendVisibility
@@ -183,6 +321,9 @@ module Shoryuken
183
321
  end
184
322
  end
185
323
 
324
+ # Creates the default client middleware chain
325
+ #
326
+ # @return [Shoryuken::Middleware::Chain] an empty middleware chain
186
327
  def default_client_middleware
187
328
  Middleware::Chain.new
188
329
  end