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,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ActiveJob Continuations integration tests for Rails 8.1+
4
+ # Tests the stopping? method and continuation timestamp handling
5
+
6
+ setup_active_job
7
+
8
+ # Skip if ActiveJob::Continuable is not available (Rails < 8.1)
9
+ unless defined?(ActiveJob::Continuable)
10
+ puts "Skipping continuation tests - ActiveJob::Continuable not available (requires Rails 8.1+)"
11
+ exit 0
12
+ end
13
+
14
+ # Test stopping? returns false when launcher is not initialized
15
+ adapter = ActiveJob::QueueAdapters::ShoryukenAdapter.new
16
+ assert_equal(false, adapter.stopping?)
17
+
18
+ # Test stopping? returns true when launcher is stopping
19
+ launcher = Shoryuken::Launcher.new
20
+ runner = Shoryuken::Runner.instance
21
+ runner.instance_variable_set(:@launcher, launcher)
22
+
23
+ adapter2 = ActiveJob::QueueAdapters::ShoryukenAdapter.new
24
+ assert_equal(false, adapter2.stopping?)
25
+
26
+ launcher.instance_variable_set(:@stopping, true)
27
+ assert_equal(true, adapter2.stopping?)
28
+
29
+ # Reset launcher state
30
+ launcher.instance_variable_set(:@stopping, false)
31
+
32
+ # Test past timestamps for continuation retries
33
+ job_capture = JobCapture.new
34
+ job_capture.start_capturing
35
+
36
+ class ContinuableTestJob < ActiveJob::Base
37
+ include ActiveJob::Continuable if defined?(ActiveJob::Continuable)
38
+ queue_as :default
39
+ def perform; end
40
+ end
41
+
42
+ adapter3 = ActiveJob::QueueAdapters::ShoryukenAdapter.new
43
+ job = ContinuableTestJob.new
44
+ job.sqs_send_message_parameters = {}
45
+
46
+ past_timestamp = Time.current.to_f - 60
47
+ adapter3.enqueue_at(job, past_timestamp)
48
+
49
+ captured_job = job_capture.last_job
50
+ assert(captured_job[:delay_seconds] <= 0, "Past timestamp should result in immediate delivery")
51
+
52
+ # Test current timestamp
53
+ job_capture2 = JobCapture.new
54
+ job_capture2.start_capturing
55
+
56
+ job2 = ContinuableTestJob.new
57
+ job2.sqs_send_message_parameters = {}
58
+
59
+ current_timestamp = Time.current.to_f
60
+ adapter3.enqueue_at(job2, current_timestamp)
61
+
62
+ captured_job2 = job_capture2.last_job
63
+ delay = captured_job2[:delay_seconds]
64
+ assert(delay >= -1 && delay <= 1, "Current timestamp should have minimal delay")
65
+
66
+ # Test future timestamp
67
+ job_capture3 = JobCapture.new
68
+ job_capture3.start_capturing
69
+
70
+ job3 = ContinuableTestJob.new
71
+ job3.sqs_send_message_parameters = {}
72
+
73
+ future_timestamp = Time.current.to_f + 30
74
+ adapter3.enqueue_at(job3, future_timestamp)
75
+
76
+ captured_job3 = job_capture3.last_job
77
+ delay3 = captured_job3[:delay_seconds]
78
+ assert(delay3 > 0, "Future timestamp should have positive delay")
79
+ assert(delay3 <= 30, "Delay should not exceed scheduled time")
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This spec tests retry behavior including ApproximateReceiveCount tracking
4
+ # across message redeliveries.
5
+
6
+ require 'concurrent'
7
+
8
+ setup_sqs
9
+
10
+ queue_name = DT.queue
11
+ create_test_queue(queue_name, attributes: { 'VisibilityTimeout' => '2' })
12
+ Shoryuken.add_group('default', 1)
13
+ Shoryuken.add_queue(queue_name, 1, 'default')
14
+
15
+ # Atomic counter for fail tracking
16
+ fail_counter = Concurrent::AtomicFixnum.new(2)
17
+
18
+ worker_class = Class.new do
19
+ include Shoryuken::Worker
20
+
21
+ shoryuken_options auto_delete: false, batch: false
22
+
23
+ define_method(:perform) do |sqs_msg, body|
24
+ receive_count = sqs_msg.attributes['ApproximateReceiveCount'].to_i
25
+ DT[:receive_counts] << receive_count
26
+
27
+ if fail_counter.value > 0
28
+ fail_counter.decrement
29
+ raise "Simulated failure"
30
+ else
31
+ sqs_msg.delete
32
+ end
33
+ end
34
+ end
35
+
36
+ worker_class.get_shoryuken_options['queue'] = queue_name
37
+ Shoryuken.register_worker(queue_name, worker_class)
38
+
39
+ Shoryuken::Client.queues(queue_name).send_message(message_body: 'retry-count-test')
40
+
41
+ poll_queues_until(timeout: 20) { DT[:receive_counts].size >= 3 }
42
+
43
+ assert(DT[:receive_counts].size >= 3)
44
+ assert_equal(DT[:receive_counts], DT[:receive_counts].sort, "Receive counts should be increasing")
45
+ assert_equal(1, DT[:receive_counts].first)
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Integration test spec helper
4
+ # This file is auto-required by RSpec for integration tests
5
+
6
+ require 'shoryuken'
7
+ require_relative '../integrations_helper'
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This spec tests the StrictPriority polling strategy.
4
+ # Higher priority queues are always processed before lower priority queues.
5
+
6
+ setup_sqs
7
+
8
+ queue_high = DT.queues[0]
9
+ queue_low = DT.queues[1]
10
+
11
+ [queue_high, queue_low].each { |q| create_test_queue(q) }
12
+
13
+ # Configure StrictPriority polling strategy
14
+ Shoryuken.options[:polling_strategy] = 'StrictPriority'
15
+
16
+ Shoryuken.add_group('default', 1)
17
+ # Higher weight = higher priority (queue_high appears 3 times, queue_low appears 1 time)
18
+ Shoryuken.add_queue(queue_high, 3, 'default')
19
+ Shoryuken.add_queue(queue_low, 1, 'default')
20
+
21
+ worker_class = Class.new do
22
+ include Shoryuken::Worker
23
+
24
+ shoryuken_options auto_delete: true, batch: false
25
+
26
+ def perform(sqs_msg, body)
27
+ queue = sqs_msg.queue_url.split('/').last
28
+ DT[:processed_order] << { queue: queue, body: body, time: Time.now }
29
+ end
30
+ end
31
+
32
+ [queue_high, queue_low].each do |queue|
33
+ worker_class.get_shoryuken_options['queue'] = queue
34
+ Shoryuken.register_worker(queue, worker_class)
35
+ end
36
+
37
+ # Send messages to low priority queue first
38
+ 3.times { |i| Shoryuken::Client.queues(queue_low).send_message(message_body: "low-#{i}") }
39
+
40
+ # Then send messages to high priority queue
41
+ 3.times { |i| Shoryuken::Client.queues(queue_high).send_message(message_body: "high-#{i}") }
42
+
43
+ sleep 1
44
+
45
+ poll_queues_until(timeout: 20) { DT[:processed_order].size >= 6 }
46
+
47
+ assert_equal(6, DT[:processed_order].size)
48
+
49
+ # With StrictPriority, high priority messages should generally be processed first
50
+ high_messages = DT[:processed_order].select { |m| m[:queue] == queue_high }
51
+ low_messages = DT[:processed_order].select { |m| m[:queue] == queue_low }
52
+
53
+ assert_equal(3, high_messages.size, "All high priority messages should be processed")
54
+ assert_equal(3, low_messages.size, "All low priority messages should be processed")
55
+
56
+ # Verify both queues were processed
57
+ queues_processed = DT[:processed_order].map { |m| m[:queue] }.uniq
58
+ assert_equal(2, queues_processed.size, "Both queues should have messages processed")
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This spec tests visibility timeout management including manual visibility
4
+ # extension during long processing.
5
+
6
+ setup_sqs
7
+
8
+ queue_name = DT.queue
9
+ create_test_queue(queue_name, attributes: { 'VisibilityTimeout' => '5' })
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
+ def perform(sqs_msg, body)
17
+ # Extend visibility before long processing
18
+ sqs_msg.change_visibility(visibility_timeout: 30)
19
+ DT[:visibility_extended] << true
20
+
21
+ sleep 2 # Simulate slow processing
22
+
23
+ DT[:messages] << body
24
+ end
25
+ end
26
+
27
+ worker_class.get_shoryuken_options['queue'] = queue_name
28
+ worker_class.get_shoryuken_options['auto_delete'] = true
29
+ worker_class.get_shoryuken_options['batch'] = false
30
+ Shoryuken.register_worker(queue_name, worker_class)
31
+
32
+ Shoryuken::Client.queues(queue_name).send_message(message_body: 'extend-test')
33
+
34
+ poll_queues_until { DT[:messages].size >= 1 }
35
+
36
+ assert_equal(1, DT[:messages].size)
37
+ assert(DT[:visibility_extended].any?, "Expected visibility to be extended")
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This spec tests the worker enqueueing methods:
4
+ # - perform_async - enqueue a job for immediate processing
5
+ # - perform_in - enqueue a job with a delay
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 for testing enqueueing methods
15
+ enqueueing_worker = Class.new do
16
+ include Shoryuken::Worker
17
+
18
+ shoryuken_options auto_delete: true
19
+
20
+ def perform(sqs_msg, body)
21
+ DT[:processed_messages] << {
22
+ message_id: sqs_msg.message_id,
23
+ body: body,
24
+ processed_at: Time.now
25
+ }
26
+ end
27
+ end
28
+
29
+ enqueueing_worker.get_shoryuken_options['queue'] = queue_name
30
+ Shoryuken.register_worker(queue_name, enqueueing_worker)
31
+
32
+ # Test 1: perform_async - immediate enqueueing with string body
33
+ enqueueing_worker.perform_async('async string message')
34
+
35
+ # Test 2: perform_async with hash body
36
+ enqueueing_worker.perform_async('action' => 'test', 'data' => [1, 2, 3])
37
+
38
+ # Test 3: perform_in - delayed enqueueing (use short 1 second delay)
39
+ enqueueing_worker.perform_in(1, 'delayed message')
40
+
41
+ sleep 1
42
+
43
+ # Poll for all 3 messages
44
+ poll_queues_until(timeout: 15) { DT[:processed_messages].size >= 3 }
45
+
46
+ assert_equal(3, DT[:processed_messages].size)
47
+
48
+ # Verify string message was processed
49
+ string_msg = DT[:processed_messages].find { |m| m[:body] == 'async string message' }
50
+ assert(string_msg, 'String message should have been processed')
51
+
52
+ # Verify hash message was processed (bodies might be stringified depending on serialization)
53
+ hash_msg = DT[:processed_messages].find do |m|
54
+ m[:body].is_a?(Hash) || (m[:body].is_a?(String) && m[:body].include?('action'))
55
+ end
56
+ assert(hash_msg, 'Hash message should have been processed')
57
+
58
+ # Verify delayed message was processed
59
+ delayed_msg = DT[:processed_messages].find { |m| m[:body] == 'delayed message' }
60
+ assert(delayed_msg, 'Delayed message should have been processed')
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This spec tests multiple worker groups with different concurrency settings.
4
+ # Each group can have its own queues and concurrency level.
5
+
6
+ require 'concurrent'
7
+
8
+ setup_sqs
9
+
10
+ queue_group1 = DT.queues[0]
11
+ queue_group2 = DT.queues[1]
12
+
13
+ %w[queue_group1 queue_group2].each_with_index { |_, i| create_test_queue(DT.queues[i]) }
14
+
15
+ # Configure two separate groups with different concurrency
16
+ Shoryuken.add_group('group1', 3) # 3 concurrent processors
17
+ Shoryuken.add_group('group2', 1) # 1 concurrent processor
18
+
19
+ Shoryuken.add_queue(queue_group1, 1, 'group1')
20
+ Shoryuken.add_queue(queue_group2, 1, 'group2')
21
+
22
+ # Track concurrent processing per group
23
+ group1_concurrent = Concurrent::AtomicFixnum.new(0)
24
+ group1_max = Concurrent::AtomicFixnum.new(0)
25
+ group2_concurrent = Concurrent::AtomicFixnum.new(0)
26
+ group2_max = Concurrent::AtomicFixnum.new(0)
27
+
28
+ worker_class = Class.new do
29
+ include Shoryuken::Worker
30
+
31
+ shoryuken_options auto_delete: true, batch: false
32
+
33
+ define_method(:perform) do |sqs_msg, body|
34
+ queue = sqs_msg.queue_url.split('/').last
35
+
36
+ if queue == queue_group1
37
+ group1_concurrent.increment
38
+ current = group1_concurrent.value
39
+ group1_max.update { |max| [max, current].max }
40
+ sleep 0.5 # Longer sleep to increase chance of concurrent execution
41
+ group1_concurrent.decrement
42
+ else
43
+ group2_concurrent.increment
44
+ current = group2_concurrent.value
45
+ group2_max.update { |max| [max, current].max }
46
+ sleep 0.3
47
+ group2_concurrent.decrement
48
+ end
49
+
50
+ DT[:processed] << { queue: queue, body: body }
51
+ end
52
+ end
53
+
54
+ [queue_group1, queue_group2].each do |queue|
55
+ worker_class.get_shoryuken_options['queue'] = queue
56
+ Shoryuken.register_worker(queue, worker_class)
57
+ end
58
+
59
+ # Send messages to both groups
60
+ 5.times { |i| Shoryuken::Client.queues(queue_group1).send_message(message_body: "group1-#{i}") }
61
+ 5.times { |i| Shoryuken::Client.queues(queue_group2).send_message(message_body: "group2-#{i}") }
62
+
63
+ sleep 1
64
+
65
+ poll_queues_until(timeout: 20) { DT[:processed].size >= 10 }
66
+
67
+ assert_equal(10, DT[:processed].size)
68
+
69
+ # Verify messages from both groups were processed
70
+ group1_messages = DT[:processed].select { |m| m[:queue] == queue_group1 }
71
+ group2_messages = DT[:processed].select { |m| m[:queue] == queue_group2 }
72
+
73
+ assert_equal(5, group1_messages.size, 'All group1 messages should be processed')
74
+ assert_equal(5, group2_messages.size, 'All group2 messages should be processed')
75
+
76
+ # Verify concurrency was used - group1 with concurrency 3 should process concurrently
77
+ # group2 with concurrency 1 should process sequentially (max = 1)
78
+ assert(group1_max.value >= 1, "Group1 should have processed messages (max concurrent: #{group1_max.value})")
79
+ assert_equal(1, group2_max.value, 'Group2 with concurrency 1 should process sequentially')
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This spec tests worker lifecycle including worker registration and discovery.
4
+
5
+ setup_sqs
6
+
7
+ queue_name = DT.queue
8
+ create_test_queue(queue_name)
9
+ Shoryuken.add_group('default', 1)
10
+ Shoryuken.add_queue(queue_name, 1, 'default')
11
+
12
+ worker_class = Class.new do
13
+ include Shoryuken::Worker
14
+
15
+ def perform(sqs_msg, body)
16
+ DT[:messages] << body
17
+ end
18
+ end
19
+
20
+ worker_class.get_shoryuken_options['queue'] = queue_name
21
+ worker_class.get_shoryuken_options['auto_delete'] = true
22
+ worker_class.get_shoryuken_options['batch'] = false
23
+ Shoryuken.register_worker(queue_name, worker_class)
24
+
25
+ registered = Shoryuken.worker_registry.workers(queue_name)
26
+ assert_includes(registered, worker_class)
27
+
28
+ Shoryuken::Client.queues(queue_name).send_message(message_body: 'lifecycle-test')
29
+
30
+ poll_queues_until { DT[:messages].size >= 1 }
31
+
32
+ assert_equal(1, DT[:messages].size)
33
+ assert_equal('lifecycle-test', DT[:messages].first)
@@ -0,0 +1,243 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Integration test helper for process-isolated testing
4
+
5
+ # Enable Ruby warnings to catch deprecations and potential issues
6
+ Warning[:performance] = true if RUBY_VERSION >= '3.3'
7
+ Warning[:deprecated] = true
8
+ $VERBOSE = true
9
+
10
+ require 'warning'
11
+
12
+ # Process warnings and raise on unexpected ones from our code
13
+ Warning.process do |warning|
14
+ # Only check warnings from our code (not dependencies)
15
+ next unless warning.include?(Dir.pwd)
16
+
17
+ # Filter out warnings we don't care about in specs
18
+ next if warning.include?('_spec')
19
+
20
+ # We redefine methods to simulate various scenarios in tests
21
+ next if warning.include?('previous definition of')
22
+ next if warning.include?('method redefined')
23
+
24
+ # Ignore vendor directory
25
+ next if warning.include?('vendor/')
26
+
27
+ # Ignore bundle path
28
+ next if warning.include?('bundle/')
29
+
30
+ raise "Warning in your code: #{warning}"
31
+ end
32
+
33
+ require 'timeout'
34
+ require 'json'
35
+ require 'securerandom'
36
+ require 'aws-sdk-sqs'
37
+ require 'shoryuken'
38
+ require 'singleton'
39
+
40
+ # Thread-safe data collector for integration tests
41
+ # Inspired by Karafka's DataCollector pattern
42
+ # Usage: DT[:key] << value, DT[:key].size, DT.clear
43
+ class DataCollector
44
+ include Singleton
45
+
46
+ MUTEX = Mutex.new
47
+ private_constant :MUTEX
48
+
49
+ attr_reader :queues, :data
50
+
51
+ class << self
52
+ def queue
53
+ instance.queue
54
+ end
55
+
56
+ def queues
57
+ instance.queues
58
+ end
59
+
60
+ def data
61
+ instance.data
62
+ end
63
+
64
+ def [](key)
65
+ MUTEX.synchronize { data[key] }
66
+ end
67
+
68
+ def []=(key, value)
69
+ MUTEX.synchronize { data[key] = value }
70
+ end
71
+
72
+ def uuids(amount)
73
+ Array.new(amount) { uuid }
74
+ end
75
+
76
+ def uuid
77
+ "it-#{SecureRandom.uuid[0, 8]}"
78
+ end
79
+
80
+ def clear
81
+ MUTEX.synchronize { instance.clear }
82
+ end
83
+
84
+ def key?(key)
85
+ instance.data.key?(key)
86
+ end
87
+ end
88
+
89
+ def initialize
90
+ @mutex = Mutex.new
91
+ @queues = Array.new(100) { "it-#{SecureRandom.hex(6)}" }
92
+ @data = Hash.new do |hash, key|
93
+ @mutex.synchronize do
94
+ break hash[key] if hash.key?(key)
95
+
96
+ hash[key] = []
97
+ end
98
+ end
99
+ end
100
+
101
+ def queue
102
+ queues.first
103
+ end
104
+
105
+ def clear
106
+ @mutex.synchronize do
107
+ @queues.clear
108
+ @queues.concat(Array.new(100) { "it-#{SecureRandom.hex(6)}" })
109
+ @data.clear
110
+ end
111
+ end
112
+ end
113
+
114
+ # Short alias for DataCollector
115
+ DT = DataCollector
116
+
117
+ module IntegrationsHelper
118
+ class TestFailure < StandardError; end
119
+
120
+ # Assertions
121
+ def assert(condition, message = "Assertion failed")
122
+ raise TestFailure, message unless condition
123
+ end
124
+
125
+ def assert_equal(expected, actual, message = nil)
126
+ message ||= "Expected #{expected.inspect}, got #{actual.inspect}"
127
+ assert(expected == actual, message)
128
+ end
129
+
130
+ def assert_includes(collection, item, message = nil)
131
+ message ||= "Expected #{collection.inspect} to include #{item.inspect}"
132
+ assert(collection.include?(item), message)
133
+ end
134
+
135
+ def refute(condition, message = "Refutation failed")
136
+ assert(!condition, message)
137
+ end
138
+
139
+ # Configure ActiveJob with Shoryuken adapter
140
+ def setup_active_job
141
+ require 'active_job'
142
+ require 'active_job/queue_adapters/shoryuken_adapter'
143
+ require 'active_job/extensions'
144
+
145
+ ActiveJob::Base.queue_adapter = :shoryuken
146
+ end
147
+
148
+ # Configure Shoryuken to use ElasticMQ for real SQS integration tests
149
+ def setup_sqs
150
+ Aws.config[:stub_responses] = false
151
+
152
+ sqs_client = Aws::SQS::Client.new(
153
+ region: 'us-east-1',
154
+ endpoint: 'http://localhost:9324',
155
+ access_key_id: 'fake',
156
+ secret_access_key: 'fake'
157
+ )
158
+
159
+ Shoryuken.options[:concurrency] = 25
160
+ Shoryuken.options[:delay] = 0
161
+ Shoryuken.options[:timeout] = 8
162
+
163
+ executor = Concurrent::CachedThreadPool.new(auto_terminate: true)
164
+ Shoryuken.define_singleton_method(:launcher_executor) { executor }
165
+
166
+ Shoryuken.configure_client { |config| config.sqs_client = sqs_client }
167
+ Shoryuken.configure_server { |config| config.sqs_client = sqs_client }
168
+ end
169
+
170
+ # Queue helpers
171
+ def create_test_queue(queue_name, attributes: {})
172
+ Shoryuken::Client.sqs.create_queue(queue_name: queue_name, attributes: attributes)
173
+ end
174
+
175
+ def delete_test_queue(queue_name)
176
+ queue_url = Shoryuken::Client.sqs.get_queue_url(queue_name: queue_name).queue_url
177
+ Shoryuken::Client.sqs.delete_queue(queue_url: queue_url)
178
+ rescue Aws::SQS::Errors::NonExistentQueue
179
+ end
180
+
181
+ def create_fifo_queue(queue_name)
182
+ create_test_queue(queue_name, attributes: {
183
+ 'FifoQueue' => 'true',
184
+ 'ContentBasedDeduplication' => 'true'
185
+ })
186
+ end
187
+
188
+ # Poll until condition met
189
+ def poll_queues_until(timeout: 15)
190
+ launcher = Shoryuken::Launcher.new
191
+ launcher.start
192
+ Timeout.timeout(timeout) { sleep 0.5 until yield }
193
+ ensure
194
+ launcher.stop
195
+ end
196
+
197
+ # Simple mock object
198
+ def double(_name = nil)
199
+ Object.new
200
+ end
201
+
202
+ # Job capture for ActiveJob tests
203
+ class JobCapture
204
+ attr_reader :jobs
205
+
206
+ def initialize
207
+ @jobs = []
208
+ end
209
+
210
+ def start_capturing
211
+ @jobs.clear
212
+ capture = self
213
+
214
+ queue_mock = Object.new
215
+ queue_mock.define_singleton_method(:fifo?) { false }
216
+ queue_mock.define_singleton_method(:send_message) do |params|
217
+ capture.jobs << {
218
+ queue: params[:queue_name] || :default,
219
+ message_body: params[:message_body],
220
+ delay_seconds: params[:delay_seconds],
221
+ message_attributes: params[:message_attributes]
222
+ }
223
+ end
224
+
225
+ Shoryuken::Client.define_singleton_method(:queues) do |queue_name = nil|
226
+ queue_mock.define_singleton_method(:name) { queue_name } if queue_name
227
+ queue_name ? queue_mock : { default: queue_mock }
228
+ end
229
+
230
+ Shoryuken.define_singleton_method(:register_worker) { |*| nil }
231
+ end
232
+
233
+ def last_job
234
+ @jobs.last
235
+ end
236
+
237
+ def job_count
238
+ @jobs.size
239
+ end
240
+ end
241
+ end
242
+
243
+ include IntegrationsHelper