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,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Scheduled ActiveJob integration test
4
+ # Tests jobs scheduled with set(wait:) are delivered after the delay
5
+
6
+ setup_sqs
7
+ setup_active_job
8
+
9
+ queue_name = DT.queue
10
+ create_test_queue(queue_name)
11
+
12
+ class ScheduledTestJob < ActiveJob::Base
13
+ def perform(label)
14
+ DT[:executions] << {
15
+ label: label,
16
+ job_id: job_id,
17
+ executed_at: Time.now
18
+ }
19
+ end
20
+ end
21
+
22
+ ScheduledTestJob.queue_as(queue_name)
23
+
24
+ Shoryuken.add_group('default', 1)
25
+ Shoryuken.add_queue(queue_name, 1, 'default')
26
+ Shoryuken.register_worker(queue_name, Shoryuken::ActiveJob::JobWrapper)
27
+
28
+ immediate_enqueue_time = Time.now
29
+ ScheduledTestJob.perform_later('immediate')
30
+ DT[:timestamps] << { label: 'immediate', time: immediate_enqueue_time }
31
+
32
+ delayed_enqueue_time = Time.now
33
+ ScheduledTestJob.set(wait: 3.seconds).perform_later('delayed_3s')
34
+ DT[:timestamps] << { label: 'delayed_3s', time: delayed_enqueue_time }
35
+
36
+ delayed_5s_enqueue_time = Time.now
37
+ ScheduledTestJob.set(wait: 5.seconds).perform_later('delayed_5s')
38
+ DT[:timestamps] << { label: 'delayed_5s', time: delayed_5s_enqueue_time }
39
+
40
+ poll_queues_until(timeout: 30) do
41
+ DT[:executions].size >= 3
42
+ end
43
+
44
+ assert_equal(3, DT[:executions].size, "Expected 3 job executions")
45
+
46
+ # Find each job's execution
47
+ immediate_job = DT[:executions].find { |e| e[:label] == 'immediate' }
48
+ delayed_3s_job = DT[:executions].find { |e| e[:label] == 'delayed_3s' }
49
+ delayed_5s_job = DT[:executions].find { |e| e[:label] == 'delayed_5s' }
50
+
51
+ assert(immediate_job, "Immediate job should have executed")
52
+ assert(delayed_3s_job, "3s delayed job should have executed")
53
+ assert(delayed_5s_job, "5s delayed job should have executed")
54
+
55
+ def enqueue_time(label)
56
+ DT[:timestamps].find { |t| t[:label] == label }[:time]
57
+ end
58
+
59
+ immediate_delay = immediate_job[:executed_at] - enqueue_time('immediate')
60
+ assert(immediate_delay < 10, "Immediate job should execute within 10 seconds, took #{immediate_delay}s")
61
+
62
+ # Using 2 seconds tolerance for SQS delivery variation
63
+ delayed_3s_actual_delay = delayed_3s_job[:executed_at] - enqueue_time('delayed_3s')
64
+ assert(delayed_3s_actual_delay >= 2, "3s delayed job should execute after at least 2s, took #{delayed_3s_actual_delay}s")
65
+
66
+ delayed_5s_actual_delay = delayed_5s_job[:executed_at] - enqueue_time('delayed_5s')
67
+ assert(delayed_5s_actual_delay >= 4, "5s delayed job should execute after at least 4s, took #{delayed_5s_actual_delay}s")
68
+
69
+ assert(
70
+ immediate_job[:executed_at] <= delayed_3s_job[:executed_at],
71
+ "Immediate job should execute before 3s delayed job"
72
+ )
73
+ assert(
74
+ delayed_3s_job[:executed_at] <= delayed_5s_job[:executed_at],
75
+ "3s delayed job should execute before 5s delayed job"
76
+ )
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This spec tests the ActiveRecord middleware functionality.
4
+ # The middleware clears database connections after each message is processed.
5
+
6
+ setup_sqs
7
+
8
+ queue_name = DT.queue
9
+ create_test_queue(queue_name)
10
+ Shoryuken.add_group('default', 1)
11
+ Shoryuken.add_queue(queue_name, 1, 'default')
12
+
13
+ # Mock ActiveRecord module to track connection clearing
14
+ module ActiveRecord
15
+ VERSION = Gem::Version.new('7.2.0')
16
+
17
+ def self.version
18
+ VERSION
19
+ end
20
+
21
+ class Base
22
+ class << self
23
+ attr_accessor :connections_cleared
24
+
25
+ def connection_handler
26
+ @connection_handler ||= ConnectionHandler.new
27
+ end
28
+ end
29
+
30
+ self.connections_cleared = []
31
+ end
32
+
33
+ class ConnectionHandler
34
+ def clear_active_connections!(scope)
35
+ ActiveRecord::Base.connections_cleared << { scope: scope, time: Time.now }
36
+ end
37
+ end
38
+ end
39
+
40
+ # Add the ActiveRecord middleware to the chain
41
+ require 'shoryuken/middleware/server/active_record'
42
+ Shoryuken.configure_server do |config|
43
+ config.server_middleware do |chain|
44
+ chain.add Shoryuken::Middleware::Server::ActiveRecord
45
+ end
46
+ end
47
+
48
+ worker_class = Class.new do
49
+ include Shoryuken::Worker
50
+
51
+ shoryuken_options auto_delete: true, batch: false
52
+
53
+ def perform(sqs_msg, body)
54
+ DT[:processed] << { message_id: sqs_msg.message_id, body: body }
55
+ end
56
+ end
57
+
58
+ worker_class.get_shoryuken_options['queue'] = queue_name
59
+ Shoryuken.register_worker(queue_name, worker_class)
60
+
61
+ # Clear any prior connection clearing records
62
+ ActiveRecord::Base.connections_cleared.clear
63
+
64
+ # Send multiple messages
65
+ 3.times { |i| Shoryuken::Client.queues(queue_name).send_message(message_body: "ar-test-#{i}") }
66
+
67
+ sleep 1
68
+
69
+ poll_queues_until { DT[:processed].size >= 3 }
70
+
71
+ # Verify all messages were processed
72
+ assert_equal(3, DT[:processed].size)
73
+
74
+ # Verify ActiveRecord connections were cleared after each message
75
+ # The middleware should have called clear_active_connections! for each message
76
+ assert(
77
+ ActiveRecord::Base.connections_cleared.size >= 3,
78
+ "ActiveRecord connections should be cleared after each message (cleared #{ActiveRecord::Base.connections_cleared.size} times)"
79
+ )
80
+
81
+ # Verify the :all scope was used (Rails 7.1+ behavior)
82
+ ActiveRecord::Base.connections_cleared.each do |record|
83
+ assert_equal(:all, record[:scope], 'Should use :all scope for Rails 7.1+')
84
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This spec tests the auto_delete middleware functionality.
4
+ # When auto_delete: true, messages should be automatically deleted after successful processing.
5
+
6
+ setup_sqs
7
+
8
+ queue_name = DT.queue
9
+ create_test_queue(queue_name)
10
+ Shoryuken.add_group('default', 1)
11
+ Shoryuken.add_queue(queue_name, 1, 'default')
12
+
13
+ auto_delete_worker = Class.new do
14
+ include Shoryuken::Worker
15
+
16
+ shoryuken_options auto_delete: true
17
+
18
+ def perform(sqs_msg, body)
19
+ DT[:auto_delete_processed] << { message_id: sqs_msg.message_id, body: body }
20
+ end
21
+ end
22
+
23
+ auto_delete_worker.get_shoryuken_options['queue'] = queue_name
24
+ Shoryuken.register_worker(queue_name, auto_delete_worker)
25
+
26
+ queue_url = Shoryuken::Client.sqs.get_queue_url(queue_name: queue_name).queue_url
27
+
28
+ # Send a message
29
+ Shoryuken::Client.sqs.send_message(
30
+ queue_url: queue_url,
31
+ message_body: 'auto delete test'
32
+ )
33
+
34
+ sleep 1
35
+
36
+ # Process the message
37
+ poll_queues_until { DT[:auto_delete_processed].size >= 1 }
38
+
39
+ assert_equal(1, DT[:auto_delete_processed].size)
40
+ assert_equal('auto delete test', DT[:auto_delete_processed].first[:body])
41
+
42
+ # Wait a moment for deletion to complete
43
+ sleep 2
44
+
45
+ # Verify message was deleted - queue should be empty
46
+ attributes = Shoryuken::Client.sqs.get_queue_attributes(
47
+ queue_url: queue_url,
48
+ attribute_names: ['ApproximateNumberOfMessages', 'ApproximateNumberOfMessagesNotVisible']
49
+ ).attributes
50
+
51
+ total_messages = attributes['ApproximateNumberOfMessages'].to_i +
52
+ attributes['ApproximateNumberOfMessagesNotVisible'].to_i
53
+ assert_equal(0, total_messages, "Message should be deleted when auto_delete: true")
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This spec tests the auto_visibility_timeout middleware functionality.
4
+ # When auto_visibility_timeout: true, the message visibility timeout should be
5
+ # automatically extended during long-running job processing to prevent re-delivery.
6
+
7
+ setup_sqs
8
+
9
+ queue_name = DT.uuid
10
+
11
+ # Create queue with short visibility timeout (10 seconds)
12
+ create_test_queue(queue_name, attributes: { 'VisibilityTimeout' => '10' })
13
+ Shoryuken.add_group('default', 1)
14
+ Shoryuken.add_queue(queue_name, 1, 'default')
15
+
16
+ # Worker with auto_visibility_timeout enabled that takes longer than visibility timeout
17
+ auto_visibility_worker = Class.new do
18
+ include Shoryuken::Worker
19
+
20
+ shoryuken_options auto_visibility_timeout: true, auto_delete: true
21
+
22
+ def perform(sqs_msg, body)
23
+ DT[:processing_started] << Time.now
24
+ # Sleep longer than the queue's visibility timeout (10s)
25
+ # The middleware should extend visibility before it expires
26
+ sleep 12
27
+ DT[:processing_completed] << Time.now
28
+ DT[:processed_messages] << { message_id: sqs_msg.message_id, body: body }
29
+ end
30
+ end
31
+
32
+ auto_visibility_worker.get_shoryuken_options['queue'] = queue_name
33
+ Shoryuken.register_worker(queue_name, auto_visibility_worker)
34
+
35
+ queue_url = Shoryuken::Client.sqs.get_queue_url(queue_name: queue_name).queue_url
36
+
37
+ # Send a message
38
+ Shoryuken::Client.sqs.send_message(
39
+ queue_url: queue_url,
40
+ message_body: 'long running job'
41
+ )
42
+
43
+ sleep 1
44
+
45
+ # Process the message - this should take ~12 seconds but not fail
46
+ poll_queues_until(timeout: 30) { DT[:processed_messages].size >= 1 }
47
+
48
+ # Verify message was processed exactly once (visibility was extended, not re-delivered)
49
+ assert_equal(1, DT[:processed_messages].size, "Message should be processed exactly once")
50
+ assert_equal('long running job', DT[:processed_messages].first[:body])
51
+
52
+ # Verify processing took longer than the visibility timeout
53
+ processing_time = DT[:processing_completed].first - DT[:processing_started].first
54
+ assert(processing_time >= 12, "Processing should have taken at least 12 seconds")
55
+
56
+ # Cleanup
57
+ delete_test_queue(queue_name)
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This spec tests that AWS configuration from Shoryuken.options[:aws]
4
+ # is properly passed to the SQS client initialization.
5
+ # This verifies the fix for issue #815: PORO setup does not load AWS config
6
+
7
+ # Reset any cached SQS client to ensure fresh initialization
8
+ Shoryuken.sqs_client = nil
9
+
10
+ # Configure AWS options programmatically (simulating PORO setup with config file)
11
+ Shoryuken.options[:aws] = {
12
+ region: 'us-east-1',
13
+ endpoint: 'http://localhost:9324',
14
+ access_key_id: 'test-key-from-config',
15
+ secret_access_key: 'test-secret-from-config'
16
+ }
17
+
18
+ # Get the SQS client - this should use the AWS config from options
19
+ client = Shoryuken.sqs_client
20
+
21
+ # Verify the client was configured with our options
22
+ config = client.config
23
+
24
+ assert_equal('us-east-1', config.region, "Region should be set from options[:aws]")
25
+ assert_equal('http://localhost:9324', config.endpoint.to_s, "Endpoint should be set from options[:aws]")
26
+
27
+ # Verify the client actually works by creating a queue
28
+ queue_name = "aws-config-test-#{SecureRandom.hex(6)}"
29
+
30
+ begin
31
+ result = client.create_queue(queue_name: queue_name)
32
+ assert(result.queue_url.include?(queue_name), "Should be able to create queue with configured client")
33
+
34
+ # Clean up
35
+ client.delete_queue(queue_url: result.queue_url)
36
+ rescue Aws::SQS::Errors::ServiceError => e
37
+ raise "SQS client should work with configured AWS options: #{e.message}"
38
+ end
39
+
40
+ # Test 2: Verify that reconfiguring options and resetting client works
41
+ Shoryuken.sqs_client = nil
42
+ Shoryuken.options[:aws][:region] = 'us-west-2'
43
+
44
+ client2 = Shoryuken.sqs_client
45
+ assert_equal('us-west-2', client2.config.region, "New client should use updated region")
46
+
47
+ # Test 3: Verify that credentials from options are used
48
+ # Reset and reconfigure with explicit credentials
49
+ Shoryuken.sqs_client = nil
50
+ Shoryuken.options[:aws] = {
51
+ region: 'us-east-1',
52
+ endpoint: 'http://localhost:9324',
53
+ access_key_id: 'another-key',
54
+ secret_access_key: 'another-secret'
55
+ }
56
+
57
+ client3 = Shoryuken.sqs_client
58
+ assert(client3.is_a?(Aws::SQS::Client), "Client should be created with new credentials")
59
+ assert_equal('us-east-1', client3.config.region, "Region should match configured value")
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This spec tests batch processing including batch message reception (up to 10
4
+ # messages), batch vs single worker behavior differences, JSON body parsing in
5
+ # batch mode, and maximum batch size handling.
6
+
7
+ setup_sqs
8
+
9
+ queue_name = DT.queue
10
+ create_test_queue(queue_name)
11
+ Shoryuken.add_group('default', 1)
12
+ Shoryuken.add_queue(queue_name, 1, 'default')
13
+
14
+ worker_class = Class.new do
15
+ include Shoryuken::Worker
16
+
17
+ def perform(sqs_msgs, bodies)
18
+ msgs = Array(sqs_msgs)
19
+ DT[:batch_sizes] << msgs.size
20
+ DT[:messages].concat(Array(bodies))
21
+ end
22
+ end
23
+
24
+ worker_class.get_shoryuken_options['queue'] = queue_name
25
+ worker_class.get_shoryuken_options['auto_delete'] = true
26
+ worker_class.get_shoryuken_options['batch'] = true
27
+ Shoryuken.register_worker(queue_name, worker_class)
28
+
29
+ entries = 5.times.map { |i| { id: SecureRandom.uuid, message_body: "message-#{i}" } }
30
+ Shoryuken::Client.queues(queue_name).send_messages(entries: entries)
31
+
32
+ sleep 1
33
+
34
+ poll_queues_until { DT[:messages].size >= 5 }
35
+
36
+ assert_equal(5, DT[:messages].size)
37
+ assert(DT[:batch_sizes].any? { |size| size > 1 }, "Expected at least one batch with size > 1")
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This spec tests the body_parser option with :json setting
4
+ # Verifies that JSON messages are automatically parsed into Ruby hashes
5
+
6
+ setup_sqs
7
+
8
+ queue_name = DT.uuid
9
+ create_test_queue(queue_name)
10
+ Shoryuken.add_group('default', 1)
11
+ Shoryuken.add_queue(queue_name, 1, 'default')
12
+
13
+ worker_class = Class.new do
14
+ include Shoryuken::Worker
15
+
16
+ shoryuken_options body_parser: :json
17
+
18
+ def perform(sqs_msg, body)
19
+ DT[:parsed_bodies] << body
20
+ DT[:body_classes] << body.class.name
21
+ end
22
+ end
23
+
24
+ worker_class.get_shoryuken_options['queue'] = queue_name
25
+ worker_class.get_shoryuken_options['auto_delete'] = true
26
+ Shoryuken.register_worker(queue_name, worker_class)
27
+
28
+ queue_url = Shoryuken::Client.sqs.get_queue_url(queue_name: queue_name).queue_url
29
+
30
+ # Send a JSON message
31
+ json_body = { 'name' => 'test', 'value' => 42, 'nested' => { 'key' => 'val' } }
32
+ Shoryuken::Client.sqs.send_message(
33
+ queue_url: queue_url,
34
+ message_body: JSON.dump(json_body)
35
+ )
36
+
37
+ sleep 1
38
+
39
+ poll_queues_until { DT[:parsed_bodies].size >= 1 }
40
+
41
+ assert_equal(1, DT[:parsed_bodies].size)
42
+ assert_equal('Hash', DT[:body_classes].first)
43
+ assert_equal('test', DT[:parsed_bodies].first['name'])
44
+ assert_equal(42, DT[:parsed_bodies].first['value'])
45
+ assert_equal('val', DT[:parsed_bodies].first['nested']['key'])
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This spec tests the body_parser option with a custom Proc
4
+ # Verifies that custom parsing logic can be applied to messages
5
+
6
+ setup_sqs
7
+
8
+ queue_name = DT.uuid
9
+ create_test_queue(queue_name)
10
+ Shoryuken.add_group('default', 1)
11
+ Shoryuken.add_queue(queue_name, 1, 'default')
12
+
13
+ # Custom parser that uppercases the body and adds metadata
14
+ custom_parser = proc do |sqs_msg|
15
+ {
16
+ original: sqs_msg.body,
17
+ transformed: sqs_msg.body.upcase,
18
+ message_id: sqs_msg.message_id
19
+ }
20
+ end
21
+
22
+ worker_class = Class.new do
23
+ include Shoryuken::Worker
24
+
25
+ def perform(sqs_msg, body)
26
+ DT[:parsed_bodies] << body
27
+ DT[:body_classes] << body.class.name
28
+ end
29
+ end
30
+
31
+ worker_class.get_shoryuken_options['queue'] = queue_name
32
+ worker_class.get_shoryuken_options['auto_delete'] = true
33
+ worker_class.get_shoryuken_options['body_parser'] = custom_parser
34
+ Shoryuken.register_worker(queue_name, worker_class)
35
+
36
+ queue_url = Shoryuken::Client.sqs.get_queue_url(queue_name: queue_name).queue_url
37
+
38
+ # Send a message to be processed by custom parser
39
+ Shoryuken::Client.sqs.send_message(
40
+ queue_url: queue_url,
41
+ message_body: 'hello world'
42
+ )
43
+
44
+ sleep 1
45
+
46
+ poll_queues_until { DT[:parsed_bodies].size >= 1 }
47
+
48
+ assert_equal(1, DT[:parsed_bodies].size)
49
+ assert_equal('Hash', DT[:body_classes].first)
50
+
51
+ parsed = DT[:parsed_bodies].first
52
+ assert_equal('hello world', parsed[:original])
53
+ assert_equal('HELLO WORLD', parsed[:transformed])
54
+ assert(parsed[:message_id], "Should include message_id from sqs_msg")
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This spec tests the body_parser option with :text setting (default)
4
+ # Verifies that messages are returned as plain strings
5
+
6
+ setup_sqs
7
+
8
+ queue_name = DT.uuid
9
+ create_test_queue(queue_name)
10
+ Shoryuken.add_group('default', 1)
11
+ Shoryuken.add_queue(queue_name, 1, 'default')
12
+
13
+ worker_class = Class.new do
14
+ include Shoryuken::Worker
15
+
16
+ shoryuken_options body_parser: :text
17
+
18
+ def perform(sqs_msg, body)
19
+ DT[:parsed_bodies] << body
20
+ DT[:body_classes] << body.class.name
21
+ end
22
+ end
23
+
24
+ worker_class.get_shoryuken_options['queue'] = queue_name
25
+ worker_class.get_shoryuken_options['auto_delete'] = true
26
+ Shoryuken.register_worker(queue_name, worker_class)
27
+
28
+ queue_url = Shoryuken::Client.sqs.get_queue_url(queue_name: queue_name).queue_url
29
+
30
+ # Send a plain text message
31
+ text_body = 'Hello, this is a plain text message!'
32
+ Shoryuken::Client.sqs.send_message(
33
+ queue_url: queue_url,
34
+ message_body: text_body
35
+ )
36
+
37
+ sleep 1
38
+
39
+ poll_queues_until { DT[:parsed_bodies].size >= 1 }
40
+
41
+ assert_equal(1, DT[:parsed_bodies].size)
42
+ assert_equal('String', DT[:body_classes].first)
43
+ assert_equal(text_body, DT[:parsed_bodies].first)
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This spec tests concurrent message processing with multiple processors.
4
+
5
+ require 'concurrent'
6
+
7
+ setup_sqs
8
+
9
+ queue_name = DT.queue
10
+ create_test_queue(queue_name)
11
+ Shoryuken.add_group('concurrent', 5) # 5 concurrent processors
12
+ Shoryuken.add_queue(queue_name, 1, 'concurrent')
13
+
14
+ # Atomic counters for tracking concurrency
15
+ concurrent_count = Concurrent::AtomicFixnum.new(0)
16
+ max_concurrent = Concurrent::AtomicFixnum.new(0)
17
+
18
+ worker_class = Class.new do
19
+ include Shoryuken::Worker
20
+
21
+ shoryuken_options auto_delete: true, batch: false
22
+
23
+ define_method(:perform) do |sqs_msg, body|
24
+ concurrent_count.increment
25
+ current = concurrent_count.value
26
+ max_concurrent.update { |max| [max, current].max }
27
+
28
+ sleep 0.5 # Simulate work
29
+
30
+ DT[:processing_times] << Time.now
31
+
32
+ concurrent_count.decrement
33
+ end
34
+ end
35
+
36
+ worker_class.get_shoryuken_options['queue'] = queue_name
37
+ Shoryuken.register_worker(queue_name, worker_class)
38
+
39
+ 10.times { |i| Shoryuken::Client.queues(queue_name).send_message(message_body: "msg-#{i}") }
40
+
41
+ poll_queues_until(timeout: 20) { DT[:processing_times].size >= 10 }
42
+
43
+ assert_equal(10, DT[:processing_times].size)
44
+ # With multiple processors, we should see concurrency > 1
45
+ assert(max_concurrent.value > 1, "Expected concurrency > 1, got #{max_concurrent.value}")
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This spec tests that a custom polling strategy can be configured per-group
4
+ # via the add_group API.
5
+ #
6
+ # Bug #925: Cannot configure custom polling_strategy from YAML config
7
+ # The add_group method does not accept a polling_strategy parameter,
8
+ # making it impossible to set a per-group polling strategy programmatically.
9
+ # Additionally, polling_strategy() reads from options[:groups] (raw config)
10
+ # rather than from groups (the processed hash populated by add_group).
11
+
12
+ # Define a custom polling strategy
13
+ class CustomRoundRobin < Shoryuken::Polling::BaseStrategy
14
+ def initialize(queues, delay = nil)
15
+ @queues = queues.dup.uniq
16
+ @delay = delay
17
+ @index = 0
18
+ end
19
+
20
+ def next_queue
21
+ return nil if @queues.empty?
22
+
23
+ queue = @queues[@index % @queues.length]
24
+ @index += 1
25
+ Shoryuken::Polling::QueueConfiguration.new(queue, {})
26
+ end
27
+
28
+ def messages_found(_queue, _count)
29
+ # noop
30
+ end
31
+
32
+ def active_queues
33
+ @queues.map { |q| [q, 1] }
34
+ end
35
+ end
36
+
37
+ # ---- Part 1: API assertions (no SQS needed) ----
38
+ # add_group accepts polling_strategy: keyword argument
39
+ Shoryuken.add_group('custom_group', 1, polling_strategy: CustomRoundRobin)
40
+
41
+ # polling_strategy() returns our custom strategy for the group
42
+ strategy = Shoryuken.polling_strategy('custom_group')
43
+ assert_equal(
44
+ CustomRoundRobin,
45
+ strategy,
46
+ "Expected polling_strategy('custom_group') to return CustomRoundRobin, got #{strategy.inspect}"
47
+ )
48
+
49
+ # add_group raises InvalidPollingStrategyError for invalid types
50
+ begin
51
+ Shoryuken.add_group('bad_group', 1, polling_strategy: 123)
52
+ raise 'Expected InvalidPollingStrategyError to be raised'
53
+ rescue Shoryuken::Errors::InvalidPollingStrategyError
54
+ # expected
55
+ end
56
+
57
+ # ---- Part 2: End-to-end with SQS ----
58
+ setup_sqs
59
+
60
+ queue_name = DT.queues[0]
61
+ create_test_queue(queue_name)
62
+
63
+ Shoryuken.groups.clear
64
+ Shoryuken.add_group('custom_group', 1, polling_strategy: CustomRoundRobin)
65
+ Shoryuken.add_queue(queue_name, 1, 'custom_group')
66
+
67
+ worker_class = Class.new do
68
+ include Shoryuken::Worker
69
+
70
+ shoryuken_options auto_delete: true, batch: false
71
+
72
+ def perform(_sqs_msg, body)
73
+ DT[:processed] << body
74
+ end
75
+ end
76
+
77
+ worker_class.get_shoryuken_options['queue'] = queue_name
78
+ Shoryuken.register_worker(queue_name, worker_class)
79
+
80
+ Shoryuken::Client.queues(queue_name).send_message(message_body: 'custom-strategy-msg')
81
+
82
+ sleep 1
83
+
84
+ poll_queues_until { DT[:processed].size >= 1 }
85
+
86
+ assert_equal(1, DT[:processed].size)
87
+ assert_equal('custom-strategy-msg', DT[:processed].first)