shoryuken 7.0.0.alpha2 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/push.yml +2 -2
  3. data/.github/workflows/specs.yml +38 -43
  4. data/.github/workflows/verify-action-pins.yml +1 -1
  5. data/.gitignore +3 -0
  6. data/.rspec +1 -0
  7. data/.ruby-version +1 -1
  8. data/.yard-lint.yml +279 -0
  9. data/CHANGELOG.md +69 -1
  10. data/Gemfile +1 -1
  11. data/README.md +2 -7
  12. data/Rakefile +4 -10
  13. data/bin/clean_localstack +52 -0
  14. data/bin/cli/base.rb +21 -0
  15. data/bin/cli/sqs.rb +61 -2
  16. data/bin/integrations +275 -0
  17. data/bin/scenario +154 -0
  18. data/bin/shoryuken +1 -1
  19. data/lib/{shoryuken/extensions/active_job_extensions.rb → active_job/extensions.rb} +15 -4
  20. data/lib/active_job/queue_adapters/shoryuken_adapter.rb +208 -0
  21. data/lib/active_job/queue_adapters/shoryuken_concurrent_send_adapter.rb +78 -0
  22. data/lib/shoryuken/active_job/current_attributes.rb +139 -0
  23. data/lib/shoryuken/active_job/job_wrapper.rb +28 -0
  24. data/lib/shoryuken/body_parser.rb +8 -0
  25. data/lib/shoryuken/client.rb +14 -0
  26. data/lib/shoryuken/default_exception_handler.rb +9 -0
  27. data/lib/shoryuken/default_worker_registry.rb +29 -1
  28. data/lib/shoryuken/environment_loader.rb +78 -8
  29. data/lib/shoryuken/errors.rb +33 -0
  30. data/lib/shoryuken/fetcher.rb +37 -1
  31. data/lib/shoryuken/helpers/atomic_boolean.rb +19 -5
  32. data/lib/shoryuken/helpers/timer_task.rb +80 -0
  33. data/lib/shoryuken/launcher.rb +53 -0
  34. data/lib/shoryuken/logging/base.rb +26 -0
  35. data/lib/shoryuken/logging/pretty.rb +25 -0
  36. data/lib/shoryuken/logging/without_timestamp.rb +25 -0
  37. data/lib/shoryuken/logging.rb +39 -25
  38. data/lib/shoryuken/manager.rb +70 -1
  39. data/lib/shoryuken/message.rb +114 -1
  40. data/lib/shoryuken/middleware/chain.rb +139 -43
  41. data/lib/shoryuken/middleware/entry.rb +30 -0
  42. data/lib/shoryuken/middleware/server/active_record.rb +8 -0
  43. data/lib/shoryuken/middleware/server/auto_delete.rb +10 -0
  44. data/lib/shoryuken/middleware/server/auto_extend_visibility.rb +27 -1
  45. data/lib/shoryuken/middleware/server/exponential_backoff_retry.rb +29 -0
  46. data/lib/shoryuken/middleware/server/timing.rb +11 -0
  47. data/lib/shoryuken/options.rb +129 -6
  48. data/lib/shoryuken/polling/base_strategy.rb +1 -0
  49. data/lib/shoryuken/polling/strict_priority.rb +39 -0
  50. data/lib/shoryuken/polling/weighted_round_robin.rb +42 -0
  51. data/lib/shoryuken/processor.rb +32 -1
  52. data/lib/shoryuken/queue.rb +93 -4
  53. data/lib/shoryuken/runner.rb +45 -4
  54. data/lib/shoryuken/util.rb +26 -1
  55. data/lib/shoryuken/version.rb +2 -1
  56. data/lib/shoryuken/worker/default_executor.rb +21 -1
  57. data/lib/shoryuken/worker/inline_executor.rb +24 -0
  58. data/lib/shoryuken/worker.rb +193 -0
  59. data/lib/shoryuken/worker_registry.rb +33 -0
  60. data/lib/shoryuken.rb +18 -6
  61. data/renovate.json +29 -2
  62. data/shoryuken.gemspec +2 -1
  63. data/spec/integration/.rspec +1 -0
  64. data/spec/integration/active_job/adapter_configuration/configuration_spec.rb +26 -0
  65. data/spec/integration/active_job/bulk_enqueue/bulk_enqueue_spec.rb +53 -0
  66. data/spec/integration/active_job/current_attributes/bulk_enqueue_spec.rb +50 -0
  67. data/spec/integration/active_job/current_attributes/complex_types_spec.rb +55 -0
  68. data/spec/integration/active_job/current_attributes/empty_context_spec.rb +41 -0
  69. data/spec/integration/active_job/current_attributes/full_context_spec.rb +63 -0
  70. data/spec/integration/active_job/current_attributes/partial_context_spec.rb +57 -0
  71. data/spec/integration/active_job/custom_attributes/number_attributes_spec.rb +37 -0
  72. data/spec/integration/active_job/custom_attributes/string_attributes_spec.rb +39 -0
  73. data/spec/integration/active_job/error_handling/job_wrapper_spec.rb +53 -0
  74. data/spec/integration/active_job/fifo_and_attributes/deduplication_spec.rb +86 -0
  75. data/spec/integration/active_job/retry/discard_on_spec.rb +43 -0
  76. data/spec/integration/active_job/retry/retry_on_spec.rb +36 -0
  77. data/spec/integration/active_job/roundtrip/roundtrip_spec.rb +52 -0
  78. data/spec/integration/active_job/scheduled/scheduled_spec.rb +76 -0
  79. data/spec/integration/active_record_middleware/active_record_middleware_spec.rb +84 -0
  80. data/spec/integration/auto_delete/auto_delete_spec.rb +53 -0
  81. data/spec/integration/auto_extend_visibility/auto_extend_visibility_spec.rb +57 -0
  82. data/spec/integration/aws_config/aws_config_spec.rb +59 -0
  83. data/spec/integration/batch_processing/batch_processing_spec.rb +37 -0
  84. data/spec/integration/body_parser/json_parser_spec.rb +45 -0
  85. data/spec/integration/body_parser/proc_parser_spec.rb +54 -0
  86. data/spec/integration/body_parser/text_parser_spec.rb +43 -0
  87. data/spec/integration/concurrent_processing/concurrent_processing_spec.rb +45 -0
  88. data/spec/integration/dead_letter_queue/dead_letter_queue_spec.rb +91 -0
  89. data/spec/integration/exception_handlers/exception_handlers_spec.rb +69 -0
  90. data/spec/integration/exponential_backoff/exponential_backoff_spec.rb +67 -0
  91. data/spec/integration/fifo_ordering/fifo_ordering_spec.rb +44 -0
  92. data/spec/integration/large_payloads/large_payloads_spec.rb +30 -0
  93. data/spec/integration/launcher/launcher_spec.rb +40 -0
  94. data/spec/integration/message_attributes/message_attributes_spec.rb +54 -0
  95. data/spec/integration/message_operations/message_operations_spec.rb +59 -0
  96. data/spec/integration/middleware_chain/empty_chain_spec.rb +11 -0
  97. data/spec/integration/middleware_chain/execution_order_spec.rb +33 -0
  98. data/spec/integration/middleware_chain/removal_spec.rb +31 -0
  99. data/spec/integration/middleware_chain/short_circuit_spec.rb +40 -0
  100. data/spec/integration/polling_strategies/polling_strategies_spec.rb +46 -0
  101. data/spec/integration/queue_operations/queue_operations_spec.rb +84 -0
  102. data/spec/integration/rails/rails_72/Gemfile +6 -0
  103. data/spec/integration/rails/rails_72/activejob_adapter_spec.rb +98 -0
  104. data/spec/integration/rails/rails_80/Gemfile +6 -0
  105. data/spec/integration/rails/rails_80/activejob_adapter_spec.rb +98 -0
  106. data/spec/integration/rails/rails_80/continuation_spec.rb +79 -0
  107. data/spec/integration/rails/rails_81/Gemfile +6 -0
  108. data/spec/integration/rails/rails_81/activejob_adapter_spec.rb +98 -0
  109. data/spec/integration/rails/rails_81/continuation_spec.rb +79 -0
  110. data/spec/integration/retry_behavior/retry_behavior_spec.rb +45 -0
  111. data/spec/integration/spec_helper.rb +7 -0
  112. data/spec/integration/strict_priority_polling/strict_priority_polling_spec.rb +58 -0
  113. data/spec/integration/visibility_timeout/visibility_timeout_spec.rb +37 -0
  114. data/spec/integration/worker_enqueueing/worker_enqueueing_spec.rb +60 -0
  115. data/spec/integration/worker_groups/worker_groups_spec.rb +79 -0
  116. data/spec/integration/worker_lifecycle/worker_lifecycle_spec.rb +33 -0
  117. data/spec/integrations_helper.rb +243 -0
  118. data/spec/lib/active_job/extensions_spec.rb +149 -0
  119. data/spec/lib/active_job/queue_adapters/shoryuken_adapter_spec.rb +29 -0
  120. data/spec/{shoryuken/extensions/active_job_concurrent_send_adapter_spec.rb → lib/active_job/queue_adapters/shoryuken_concurrent_send_adapter_spec.rb} +3 -3
  121. data/spec/{shoryuken/extensions/active_job_wrapper_spec.rb → lib/shoryuken/active_job/job_wrapper_spec.rb} +4 -4
  122. data/spec/{shoryuken → lib/shoryuken}/environment_loader_spec.rb +1 -1
  123. data/spec/{shoryuken → lib/shoryuken}/helpers/hash_utils_spec.rb +14 -14
  124. data/spec/{shoryuken → lib/shoryuken}/helpers/string_utils_spec.rb +3 -3
  125. data/spec/lib/shoryuken/helpers/timer_task_spec.rb +298 -0
  126. data/spec/{shoryuken → lib/shoryuken}/helpers_integration_spec.rb +9 -9
  127. data/spec/{shoryuken → lib/shoryuken}/launcher_spec.rb +22 -0
  128. data/spec/lib/shoryuken/logging_spec.rb +242 -0
  129. data/spec/lib/shoryuken/message_spec.rb +109 -0
  130. data/spec/lib/shoryuken/middleware/entry_spec.rb +68 -0
  131. data/spec/lib/shoryuken/middleware/server/active_record_spec.rb +133 -0
  132. data/spec/{shoryuken → lib/shoryuken}/middleware/server/auto_extend_visibility_spec.rb +50 -0
  133. data/spec/{shoryuken → lib/shoryuken}/options_spec.rb +2 -2
  134. data/spec/{shoryuken → lib/shoryuken}/util_spec.rb +1 -1
  135. data/spec/lib/shoryuken/version_spec.rb +17 -0
  136. data/spec/lib/shoryuken/worker_registry_spec.rb +63 -0
  137. data/spec/shared_examples_for_active_job.rb +29 -9
  138. data/spec/spec_helper.rb +34 -3
  139. metadata +230 -91
  140. data/.devcontainer/Dockerfile +0 -17
  141. data/.devcontainer/base.Dockerfile +0 -43
  142. data/.devcontainer/devcontainer.json +0 -35
  143. data/Appraisals +0 -23
  144. data/gemfiles/.gitignore +0 -1
  145. data/gemfiles/rails_7_0.gemfile +0 -19
  146. data/gemfiles/rails_7_1.gemfile +0 -19
  147. data/gemfiles/rails_7_2.gemfile +0 -19
  148. data/gemfiles/rails_8_0.gemfile +0 -19
  149. data/lib/shoryuken/extensions/active_job_adapter.rb +0 -110
  150. data/lib/shoryuken/extensions/active_job_concurrent_send_adapter.rb +0 -50
  151. data/spec/integration/launcher_spec.rb +0 -127
  152. data/spec/shoryuken/extensions/active_job_adapter_spec.rb +0 -8
  153. data/spec/shoryuken/extensions/active_job_base_spec.rb +0 -85
  154. /data/spec/{shoryuken → lib/shoryuken}/body_parser_spec.rb +0 -0
  155. /data/spec/{shoryuken → lib/shoryuken}/client_spec.rb +0 -0
  156. /data/spec/{shoryuken → lib/shoryuken}/default_exception_handler_spec.rb +0 -0
  157. /data/spec/{shoryuken → lib/shoryuken}/default_worker_registry_spec.rb +0 -0
  158. /data/spec/{shoryuken → lib/shoryuken}/fetcher_spec.rb +0 -0
  159. /data/spec/{shoryuken → lib/shoryuken}/helpers/atomic_boolean_spec.rb +0 -0
  160. /data/spec/{shoryuken → lib/shoryuken}/helpers/atomic_counter_spec.rb +0 -0
  161. /data/spec/{shoryuken → lib/shoryuken}/helpers/atomic_hash_spec.rb +0 -0
  162. /data/spec/{shoryuken → lib/shoryuken}/inline_message_spec.rb +0 -0
  163. /data/spec/{shoryuken → lib/shoryuken}/manager_spec.rb +0 -0
  164. /data/spec/{shoryuken → lib/shoryuken}/middleware/chain_spec.rb +0 -0
  165. /data/spec/{shoryuken → lib/shoryuken}/middleware/server/auto_delete_spec.rb +0 -0
  166. /data/spec/{shoryuken → lib/shoryuken}/middleware/server/exponential_backoff_retry_spec.rb +0 -0
  167. /data/spec/{shoryuken → lib/shoryuken}/middleware/server/timing_spec.rb +0 -0
  168. /data/spec/{shoryuken → lib/shoryuken}/polling/base_strategy_spec.rb +0 -0
  169. /data/spec/{shoryuken → lib/shoryuken}/polling/queue_configuration_spec.rb +0 -0
  170. /data/spec/{shoryuken → lib/shoryuken}/polling/strict_priority_spec.rb +0 -0
  171. /data/spec/{shoryuken → lib/shoryuken}/polling/weighted_round_robin_spec.rb +0 -0
  172. /data/spec/{shoryuken → lib/shoryuken}/processor_spec.rb +0 -0
  173. /data/spec/{shoryuken → lib/shoryuken}/queue_spec.rb +0 -0
  174. /data/spec/{shoryuken → lib/shoryuken}/runner_spec.rb +0 -0
  175. /data/spec/{shoryuken → lib/shoryuken}/worker/default_executor_spec.rb +0 -0
  176. /data/spec/{shoryuken → lib/shoryuken}/worker/inline_executor_spec.rb +0 -0
  177. /data/spec/{shoryuken → lib/shoryuken}/worker_spec.rb +0 -0
  178. /data/spec/{shoryuken_spec.rb → lib/shoryuken_spec.rb} +0 -0
@@ -2,7 +2,14 @@
2
2
 
3
3
  module Shoryuken
4
4
  module Polling
5
+ # A polling strategy that processes queues in weighted round-robin order.
6
+ # Queue weights determine how often each queue is polled relative to others.
7
+ # Queues are temporarily paused when no messages are found.
5
8
  class WeightedRoundRobin < BaseStrategy
9
+ # Initializes a new WeightedRoundRobin polling strategy
10
+ #
11
+ # @param queues [Array<String>] array of queue names, with weights indicated by repetition
12
+ # @param delay [Float, nil] delay in seconds before unpausing empty queues
6
13
  def initialize(queues, delay = nil)
7
14
  @initial_queues = queues
8
15
  @queues = queues.dup.uniq
@@ -10,6 +17,9 @@ module Shoryuken
10
17
  @delay = delay
11
18
  end
12
19
 
20
+ # Returns the next queue to poll in round-robin order
21
+ #
22
+ # @return [QueueConfiguration, nil] the next queue configuration or nil if all paused
13
23
  def next_queue
14
24
  unpause_queues
15
25
  queue = @queues.shift
@@ -19,6 +29,11 @@ module Shoryuken
19
29
  QueueConfiguration.new(queue, {})
20
30
  end
21
31
 
32
+ # Handles the result of polling a queue, adjusting weight if messages were found
33
+ #
34
+ # @param queue [String] the queue name
35
+ # @param messages_found [Integer] number of messages found
36
+ # @return [void]
22
37
  def messages_found(queue, messages_found)
23
38
  if messages_found == 0
24
39
  pause(queue)
@@ -33,10 +48,17 @@ module Shoryuken
33
48
  end
34
49
  end
35
50
 
51
+ # Returns the list of active queues with their current weights
52
+ #
53
+ # @return [Array<Array>] array of [queue_name, weight] pairs
36
54
  def active_queues
37
55
  unparse_queues(@queues)
38
56
  end
39
57
 
58
+ # Called when a message from a queue has been processed
59
+ #
60
+ # @param queue [String] the queue name
61
+ # @return [void]
40
62
  def message_processed(queue)
41
63
  paused_queue = @paused_queues.find { |_time, name| name == queue }
42
64
  return unless paused_queue
@@ -46,6 +68,10 @@ module Shoryuken
46
68
 
47
69
  private
48
70
 
71
+ # Pauses a queue by removing it from active rotation
72
+ #
73
+ # @param queue [String] the queue name to pause
74
+ # @return [void]
49
75
  def pause(queue)
50
76
  return unless @queues.delete(queue)
51
77
 
@@ -53,6 +79,9 @@ module Shoryuken
53
79
  logger.debug "Paused #{queue}"
54
80
  end
55
81
 
82
+ # Unpauses queues whose delay has expired
83
+ #
84
+ # @return [void]
56
85
  def unpause_queues
57
86
  return if @paused_queues.empty?
58
87
  return if Time.now < @paused_queues.first[0]
@@ -62,14 +91,27 @@ module Shoryuken
62
91
  logger.debug "Unpaused #{pause[1]}"
63
92
  end
64
93
 
94
+ # Returns the current weight of a queue in the active rotation
95
+ #
96
+ # @param queue [String] the queue name
97
+ # @return [Integer] the current weight
65
98
  def current_queue_weight(queue)
66
99
  queue_weight(@queues, queue)
67
100
  end
68
101
 
102
+ # Returns the maximum configured weight of a queue
103
+ #
104
+ # @param queue [String] the queue name
105
+ # @return [Integer] the maximum weight
69
106
  def maximum_queue_weight(queue)
70
107
  queue_weight(@initial_queues, queue)
71
108
  end
72
109
 
110
+ # Counts how many times a queue appears in the given array
111
+ #
112
+ # @param queues [Array<String>] the array to count in
113
+ # @param queue [String] the queue name to count
114
+ # @return [Integer] the count
73
115
  def queue_weight(queues, queue)
74
116
  queues.count { |q| q == queue }
75
117
  end
@@ -1,20 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Shoryuken
4
+ # Processes SQS messages by invoking the appropriate worker.
5
+ # Handles middleware chain execution and exception handling.
4
6
  class Processor
5
7
  include Util
6
8
 
7
- attr_reader :queue, :sqs_msg
9
+ # @return [String] the queue name
10
+ attr_reader :queue
8
11
 
12
+ # @return [Aws::SQS::Types::Message, Array<Aws::SQS::Types::Message>] the message or batch
13
+ attr_reader :sqs_msg
14
+
15
+ # Processes a message from a queue
16
+ #
17
+ # @param queue [String] the queue name
18
+ # @param sqs_msg [Aws::SQS::Types::Message, Array<Aws::SQS::Types::Message>] the message or batch
19
+ # @return [Object] the result of the worker's perform method
9
20
  def self.process(queue, sqs_msg)
10
21
  new(queue, sqs_msg).process
11
22
  end
12
23
 
24
+ # Initializes a new Processor
25
+ #
26
+ # @param queue [String] the queue name
27
+ # @param sqs_msg [Aws::SQS::Types::Message, Array<Aws::SQS::Types::Message>] the message or batch
13
28
  def initialize(queue, sqs_msg)
14
29
  @queue = queue
15
30
  @sqs_msg = sqs_msg
16
31
  end
17
32
 
33
+ # Processes the message through the middleware chain and worker
34
+ #
35
+ # @return [Object] the result of the worker's perform method
18
36
  def process
19
37
  worker_perform = proc do
20
38
  return logger.error { "No worker found for #{queue}" } unless worker
@@ -41,18 +59,31 @@ module Shoryuken
41
59
 
42
60
  private
43
61
 
62
+ # Fetches the worker instance for processing
63
+ #
64
+ # @return [Object, nil] the worker instance or nil if not found
44
65
  def worker
45
66
  @_worker ||= Shoryuken.worker_registry.fetch_worker(queue, sqs_msg)
46
67
  end
47
68
 
69
+ # Returns the worker class
70
+ #
71
+ # @return [Class] the worker class
48
72
  def worker_class
49
73
  worker.class
50
74
  end
51
75
 
76
+ # Parses the message body or bodies for batch processing
77
+ #
78
+ # @return [Object, Array<Object>] the parsed body or array of bodies
52
79
  def body
53
80
  @_body ||= sqs_msg.is_a?(Array) ? sqs_msg.map(&method(:parse_body)) : parse_body(sqs_msg)
54
81
  end
55
82
 
83
+ # Parses a single message body
84
+ #
85
+ # @param sqs_msg [Aws::SQS::Types::Message] the message to parse
86
+ # @return [Object] the parsed message body
56
87
  def parse_body(sqs_msg)
57
88
  BodyParser.parse(worker_class, sqs_msg)
58
89
  end
@@ -1,20 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Shoryuken
4
+ # Represents an SQS queue and provides methods for sending and receiving messages.
5
+ # Handles both standard and FIFO queues, automatically adding required FIFO attributes.
4
6
  class Queue
5
7
  include Util
6
8
 
9
+ # SQS attribute name for FIFO queue identification
7
10
  FIFO_ATTR = 'FifoQueue'
11
+
12
+ # Default message group ID used for FIFO queues
8
13
  MESSAGE_GROUP_ID = 'ShoryukenMessage'
14
+
15
+ # SQS attribute name for visibility timeout
9
16
  VISIBILITY_TIMEOUT_ATTR = 'VisibilityTimeout'
10
17
 
11
- attr_accessor :name, :client, :url
18
+ # @return [String] the queue name
19
+ attr_accessor :name
20
+
21
+ # @return [Aws::SQS::Client] the SQS client
22
+ attr_accessor :client
23
+
24
+ # @return [String] the queue URL
25
+ attr_accessor :url
12
26
 
27
+ # Initializes a new Queue instance
28
+ #
29
+ # @param client [Aws::SQS::Client] the SQS client
30
+ # @param name_or_url_or_arn [String] the queue name, URL, or ARN
13
31
  def initialize(client, name_or_url_or_arn)
14
32
  self.client = client
15
33
  set_name_and_url(name_or_url_or_arn)
16
34
  end
17
35
 
36
+ # Returns the visibility timeout for the queue
37
+ #
38
+ # @return [Integer] the visibility timeout in seconds
18
39
  def visibility_timeout
19
40
  # Always lookup for the latest visibility when cache is disabled
20
41
  # setting it to nil, forces re-lookup
@@ -22,6 +43,11 @@ module Shoryuken
22
43
  @_visibility_timeout ||= queue_attributes.attributes[VISIBILITY_TIMEOUT_ATTR].to_i
23
44
  end
24
45
 
46
+ # Deletes messages from the queue in batch
47
+ #
48
+ # @param options [Hash] options for delete_message_batch
49
+ # @option options [Array<Hash>] :entries entries to delete with id and receipt_handle
50
+ # @return [Boolean] true if any messages failed to delete
25
51
  def delete_messages(options)
26
52
  failed_messages = client.delete_message_batch(
27
53
  options.merge(queue_url: url)
@@ -33,6 +59,15 @@ module Shoryuken
33
59
  end
34
60
  end
35
61
 
62
+ # Sends a single message to the queue
63
+ #
64
+ # @param options [Hash, String] message options or message body string
65
+ # @option options [String] :message_body the message body
66
+ # @option options [Integer] :delay_seconds delay before message becomes visible
67
+ # @option options [Hash] :message_attributes custom message attributes
68
+ # @option options [String] :message_group_id FIFO queue message group ID
69
+ # @option options [String] :message_deduplication_id FIFO queue deduplication ID
70
+ # @return [Aws::SQS::Types::SendMessageResult] the send result
36
71
  def send_message(options)
37
72
  options = sanitize_message!(options).merge(queue_url: url)
38
73
 
@@ -41,15 +76,32 @@ module Shoryuken
41
76
  end
42
77
  end
43
78
 
79
+ # Sends multiple messages to the queue in batch
80
+ #
81
+ # @param options [Hash, Array] batch options or array of message bodies/hashes
82
+ # @option options [Array<Hash>] :entries message entries to send
83
+ # @return [Aws::SQS::Types::SendMessageBatchResult] the batch send result
44
84
  def send_messages(options)
45
85
  client.send_message_batch(sanitize_messages!(options).merge(queue_url: url))
46
86
  end
47
87
 
88
+ # Receives messages from the queue
89
+ #
90
+ # @param options [Hash] options for receive_message
91
+ # @option options [Integer] :max_number_of_messages maximum messages to receive
92
+ # @option options [Integer] :visibility_timeout visibility timeout for received messages
93
+ # @option options [Integer] :wait_time_seconds long polling wait time
94
+ # @option options [Array<String>] :attribute_names SQS attributes to retrieve
95
+ # @option options [Array<String>] :message_attribute_names message attributes to retrieve
96
+ # @return [Array<Shoryuken::Message>] the received messages
48
97
  def receive_messages(options)
49
98
  messages = client.receive_message(options.merge(queue_url: url)).messages || []
50
99
  messages.map { |m| Message.new(client, self, m) }
51
100
  end
52
101
 
102
+ # Checks if the queue is a FIFO queue
103
+ #
104
+ # @return [Boolean] true if the queue is a FIFO queue
53
105
  def fifo?
54
106
  # Make sure the memoization work with boolean to avoid multiple calls to SQS
55
107
  # see https://github.com/ruby-shoryuken/shoryuken/pull/529
@@ -61,32 +113,51 @@ module Shoryuken
61
113
 
62
114
  private
63
115
 
116
+ # Initializes the FIFO attribute by calling fifo?
117
+ #
118
+ # @return [Boolean] whether the queue is FIFO
64
119
  def initialize_fifo_attribute
65
120
  # calling fifo? will also initialize it
66
121
  fifo?
67
122
  end
68
123
 
124
+ # Sets the queue name and URL from a queue name
125
+ #
126
+ # @param name [String] the queue name
127
+ # @return [void]
69
128
  def set_by_name(name) # rubocop:disable Naming/AccessorMethodName
70
129
  self.name = name
71
130
  self.url = client.get_queue_url(queue_name: name).queue_url
72
131
  end
73
132
 
133
+ # Sets the queue name and URL from a queue URL
134
+ #
135
+ # @param url [String] the queue URL
136
+ # @return [void]
74
137
  def set_by_url(url) # rubocop:disable Naming/AccessorMethodName
75
138
  self.name = url.split('/').last
76
139
  self.url = url
77
140
  end
78
141
 
142
+ # Converts an ARN to a queue URL
143
+ #
144
+ # @param arn_str [String] the ARN string
145
+ # @return [String] the queue URL
79
146
  def arn_to_url(arn_str)
80
147
  *, region, account_id, resource = arn_str.split(':')
81
148
 
82
149
  required = [region, account_id, resource].map(&:to_s)
83
150
  valid = required.none?(&:empty?)
84
151
 
85
- abort "Invalid ARN: #{arn_str}. A valid ARN must include: region, account_id and resource." unless valid
152
+ raise Errors::InvalidArnError, "Invalid ARN: #{arn_str}. A valid ARN must include: region, account_id and resource." unless valid
86
153
 
87
154
  "https://sqs.#{region}.amazonaws.com/#{account_id}/#{resource}"
88
155
  end
89
156
 
157
+ # Sets the queue name and URL from a name, URL, or ARN
158
+ #
159
+ # @param name_or_url_or_arn [String] the queue identifier
160
+ # @return [void]
90
161
  def set_name_and_url(name_or_url_or_arn) # rubocop:disable Naming/AccessorMethodName
91
162
  if name_or_url_or_arn.include?('://')
92
163
  set_by_url(name_or_url_or_arn)
@@ -106,16 +177,24 @@ module Shoryuken
106
177
  end
107
178
 
108
179
  set_by_name(name_or_url_or_arn)
109
- rescue Aws::Errors::NoSuchEndpointError, Aws::SQS::Errors::NonExistentQueue => e
110
- raise e, "The specified queue #{name_or_url_or_arn} does not exist."
180
+ rescue Aws::Errors::NoSuchEndpointError, Aws::SQS::Errors::NonExistentQueue
181
+ raise Errors::QueueNotFoundError, "The specified queue #{name_or_url_or_arn} does not exist."
111
182
  end
112
183
 
184
+ # Returns the queue attributes from SQS
185
+ #
186
+ # @return [Aws::SQS::Types::GetQueueAttributesResult] the queue attributes
113
187
  def queue_attributes
114
188
  # Note: Retrieving all queue attributes as requesting `FifoQueue` on non-FIFO queue raises error.
115
189
  # See issue: https://github.com/aws/aws-sdk-ruby/issues/1350
116
190
  client.get_queue_attributes(queue_url: url, attribute_names: ['All'])
117
191
  end
118
192
 
193
+ # Sanitizes a batch of messages, converting to proper format
194
+ #
195
+ # @param options [Hash, Array] batch options or array of messages
196
+ # @option options [Array<Hash>] :entries message entries
197
+ # @return [Hash] the sanitized options with entries key
119
198
  def sanitize_messages!(options)
120
199
  if options.is_a?(Array)
121
200
  entries = options.map.with_index do |m, index|
@@ -130,6 +209,11 @@ module Shoryuken
130
209
  options
131
210
  end
132
211
 
212
+ # Adds FIFO attributes to message options if needed
213
+ #
214
+ # @param options [Hash] the message options
215
+ # @option options [String] :message_body the message body
216
+ # @return [Hash] the options with FIFO attributes added
133
217
  def add_fifo_attributes!(options)
134
218
  return unless fifo?
135
219
 
@@ -139,6 +223,11 @@ module Shoryuken
139
223
  options
140
224
  end
141
225
 
226
+ # Sanitizes a single message, converting body to JSON if needed
227
+ #
228
+ # @param options [Hash, String] message options or body string
229
+ # @option options [String, Hash] :message_body the message body
230
+ # @return [Hash] the sanitized message options
142
231
  def sanitize_message!(options)
143
232
  options = { message_body: options } if options.is_a?(String)
144
233
 
@@ -9,13 +9,23 @@ require 'erb'
9
9
  require 'shoryuken'
10
10
 
11
11
  module Shoryuken
12
- # See: https://github.com/mperham/sidekiq/blob/33f5d6b2b6c0dfaab11e5d39688cab7ebadc83ae/lib/sidekiq/cli.rb#L20
13
- class Shutdown < Interrupt; end
14
-
12
+ # Runs the Shoryuken server process.
13
+ # Handles signal trapping, daemonization, and lifecycle management.
15
14
  class Runner
16
15
  include Util
17
16
  include Singleton
18
17
 
18
+ # @return [Shoryuken::Launcher, nil] the launcher instance, or nil if not yet initialized
19
+ attr_reader :launcher
20
+
21
+ # Runs the Shoryuken server with the given options
22
+ #
23
+ # @param options [Hash] runtime configuration options
24
+ # @option options [Boolean] :daemon whether to daemonize the process
25
+ # @option options [String] :pidfile path to the PID file
26
+ # @option options [String] :logfile path to the log file
27
+ # @option options [String] :config_file path to the configuration file
28
+ # @return [void]
19
29
  def run(options)
20
30
  self_read, self_write = IO.pipe
21
31
 
@@ -51,12 +61,18 @@ module Shoryuken
51
61
  end
52
62
  end
53
63
 
64
+ # Checks if the server is healthy
65
+ #
66
+ # @return [Boolean] true if the launcher is running and healthy
54
67
  def healthy?
55
68
  (@launcher && @launcher.healthy?) || false
56
69
  end
57
70
 
58
71
  private
59
72
 
73
+ # Initializes the Concurrent Ruby logger
74
+ #
75
+ # @return [void]
60
76
  def initialize_concurrent_logger
61
77
  return unless Shoryuken.logger
62
78
 
@@ -65,6 +81,12 @@ module Shoryuken
65
81
  end
66
82
  end
67
83
 
84
+ # Daemonizes the process
85
+ #
86
+ # @param options [Hash] options containing daemon and logfile settings
87
+ # @option options [Boolean] :daemon whether to daemonize
88
+ # @option options [String] :logfile path to the log file for daemon output
89
+ # @return [void]
68
90
  def daemonize(options)
69
91
  return unless options[:daemon]
70
92
 
@@ -90,25 +112,39 @@ module Shoryuken
90
112
  $stdin.reopen('/dev/null')
91
113
  end
92
114
 
115
+ # Writes the process ID to a file
116
+ #
117
+ # @param options [Hash] options containing the pidfile path
118
+ # @option options [String] :pidfile path to write the PID file
119
+ # @return [void]
93
120
  def write_pid(options)
94
121
  return unless (path = options[:pidfile])
95
122
 
96
123
  File.open(path, 'w') { |f| f.puts(Process.pid) }
97
124
  end
98
125
 
126
+ # Executes a soft shutdown on USR1 signal
127
+ #
128
+ # @return [void]
99
129
  def execute_soft_shutdown
100
- logger.info { 'Received USR1, will soft shutdown down' }
130
+ logger.info { 'Received USR1, will soft shutdown' }
101
131
 
102
132
  @launcher.stop
103
133
  exit 0
104
134
  end
105
135
 
136
+ # Executes a terminal stop on TSTP signal
137
+ #
138
+ # @return [void]
106
139
  def execute_terminal_stop
107
140
  logger.info { 'Received TSTP, will stop accepting new work' }
108
141
 
109
142
  @launcher.stop
110
143
  end
111
144
 
145
+ # Prints backtraces of all threads
146
+ #
147
+ # @return [void]
112
148
  def print_threads_backtrace
113
149
  Thread.list.each do |thread|
114
150
  logger.info { "Thread TID-#{thread.object_id.to_s(36)} #{thread['label']}" }
@@ -120,6 +156,11 @@ module Shoryuken
120
156
  end
121
157
  end
122
158
 
159
+ # Handles incoming signals
160
+ #
161
+ # @param sig [String] the signal name
162
+ # @return [void]
163
+ # @raise [Interrupt] on TERM or INT signals
123
164
  def handle_signal(sig)
124
165
  logger.debug "Got #{sig} signal"
125
166
 
@@ -1,11 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Shoryuken
4
+ # Utility methods shared across Shoryuken classes.
5
+ # Provides logging, event firing, and helper methods.
4
6
  module Util
7
+ # Returns the Shoryuken logger
8
+ #
9
+ # @return [Logger] the configured logger
5
10
  def logger
6
11
  Shoryuken.logger
7
12
  end
8
13
 
14
+ # Fires a lifecycle event to all registered handlers
15
+ #
16
+ # @param event [Symbol] the event name to fire
17
+ # @param reverse [Boolean] whether to call handlers in reverse order
18
+ # @param event_options [Hash] options to pass to event handlers
19
+ # @return [void]
9
20
  def fire_event(event, reverse = false, event_options = {})
10
21
  logger.debug { "Firing '#{event}' lifecycle event" }
11
22
  arr = Shoryuken.options[:lifecycle_events][event]
@@ -18,24 +29,38 @@ module Shoryuken
18
29
  end
19
30
  end
20
31
 
32
+ # Calculates elapsed time in milliseconds
33
+ #
34
+ # @param started_at [Time] the start time
35
+ # @return [Float] elapsed time in milliseconds
21
36
  def elapsed(started_at)
22
37
  # elapsed in ms
23
38
  (Time.now - started_at) * 1000
24
39
  end
25
40
 
41
+ # Converts a queue array to a hash of queue names and weights
42
+ #
43
+ # @param queues [Array<String>] array of queue names with possible duplicates
44
+ # @return [Array<Array>] array of [queue_name, weight] pairs
26
45
  def unparse_queues(queues)
27
46
  queues.each_with_object({}) do |name, queue_and_weights|
28
47
  queue_and_weights[name] = queue_and_weights[name].to_i + 1
29
48
  end.to_a
30
49
  end
31
50
 
51
+ # Returns a display name for the worker processing a message
52
+ #
53
+ # @param worker_class [Class] the worker class
54
+ # @param sqs_msg [Aws::SQS::Types::Message, Array] the message or batch
55
+ # @param body [Object, nil] the parsed message body
56
+ # @return [String] the worker display name
32
57
  def worker_name(worker_class, sqs_msg, body = nil)
33
58
  if Shoryuken.active_job? \
34
59
  && !sqs_msg.is_a?(Array) \
35
60
  && sqs_msg.message_attributes \
36
61
  && sqs_msg.message_attributes['shoryuken_class'] \
37
62
  && sqs_msg.message_attributes['shoryuken_class'][:string_value] \
38
- == ActiveJob::QueueAdapters::ShoryukenAdapter::JobWrapper.to_s \
63
+ == 'Shoryuken::ActiveJob::JobWrapper' \
39
64
  && body
40
65
 
41
66
  "ActiveJob/#{body['job_class']}"
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Shoryuken
4
- VERSION = '7.0.0.alpha2'
4
+ # Current version of the Shoryuken gem
5
+ VERSION = '7.0.0'
5
6
  end
@@ -2,8 +2,18 @@
2
2
 
3
3
  module Shoryuken
4
4
  module Worker
5
+ # Default executor that sends messages to SQS for asynchronous processing.
6
+ # This is the standard executor used in production environments.
5
7
  class DefaultExecutor
6
8
  class << self
9
+ # Enqueues a job for asynchronous processing via SQS
10
+ #
11
+ # @param worker_class [Class] the worker class that will process the message
12
+ # @param body [Object] the message body
13
+ # @param options [Hash] additional SQS message options
14
+ # @option options [Hash] :message_attributes custom message attributes
15
+ # @option options [String] :queue override the default queue
16
+ # @return [Aws::SQS::Types::SendMessageResult] the send result
7
17
  def perform_async(worker_class, body, options = {})
8
18
  options[:message_attributes] ||= {}
9
19
  options[:message_attributes]['shoryuken_class'] = {
@@ -18,6 +28,16 @@ module Shoryuken
18
28
  Shoryuken::Client.queues(queue).send_message(options)
19
29
  end
20
30
 
31
+ # Enqueues a job for delayed processing via SQS
32
+ #
33
+ # @param worker_class [Class] the worker class that will process the message
34
+ # @param interval [Integer, Float] delay in seconds or timestamp
35
+ # @param body [Object] the message body
36
+ # @param options [Hash] SQS message options for the delayed job
37
+ # @option options [Hash] :message_attributes custom message attributes
38
+ # @option options [String] :queue override the default queue
39
+ # @return [Aws::SQS::Types::SendMessageResult] the send result
40
+ # @raise [Errors::InvalidDelayError] if delay exceeds 15 minutes
21
41
  def perform_in(worker_class, interval, body, options = {})
22
42
  interval = interval.to_f
23
43
  now = Time.now.to_f
@@ -25,7 +45,7 @@ module Shoryuken
25
45
 
26
46
  delay = (ts - now).ceil
27
47
 
28
- raise 'The maximum allowed delay is 15 minutes' if delay > 15 * 60
48
+ raise Errors::InvalidDelayError, 'The maximum allowed delay is 15 minutes' if delay > 15 * 60
29
49
 
30
50
  worker_class.perform_async(body, options.merge(delay_seconds: delay))
31
51
  end
@@ -2,8 +2,18 @@
2
2
 
3
3
  module Shoryuken
4
4
  module Worker
5
+ # Executor that processes jobs synchronously in the current thread.
6
+ # Useful for testing and development environments.
5
7
  class InlineExecutor
6
8
  class << self
9
+ # Processes a job synchronously in the current thread
10
+ #
11
+ # @param worker_class [Class] the worker class that will process the message
12
+ # @param body [Object] the message body
13
+ # @param options [Hash] inline execution options
14
+ # @option options [String] :queue override the default queue name
15
+ # @option options [Hash] :message_attributes custom message attributes
16
+ # @return [Object] the result of the worker's perform method
7
17
  def perform_async(worker_class, body, options = {})
8
18
  body = JSON.dump(body) if body.is_a?(Hash)
9
19
  queue_name = options.delete(:queue) || worker_class.get_shoryuken_options['queue']
@@ -28,12 +38,26 @@ module Shoryuken
28
38
  call(worker_class, sqs_msg)
29
39
  end
30
40
 
41
+ # Processes a job synchronously, ignoring the delay interval
42
+ #
43
+ # @param worker_class [Class] the worker class that will process the message
44
+ # @param _interval [Integer, Float] ignored for inline execution
45
+ # @param body [Object] the message body
46
+ # @param options [Hash] inline execution options
47
+ # @option options [String] :queue override the default queue name
48
+ # @option options [Hash] :message_attributes custom message attributes
49
+ # @return [Object] the result of the worker's perform method
31
50
  def perform_in(worker_class, _interval, body, options = {})
32
51
  worker_class.perform_async(body, options)
33
52
  end
34
53
 
35
54
  private
36
55
 
56
+ # Instantiates and calls the worker
57
+ #
58
+ # @param worker_class [Class] the worker class
59
+ # @param sqs_msg [Shoryuken::InlineMessage] the message
60
+ # @return [Object] the result of the worker's perform method
37
61
  def call(worker_class, sqs_msg)
38
62
  parsed_body = BodyParser.parse(worker_class, sqs_msg)
39
63
  batch = worker_class.shoryuken_options_hash['batch']