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,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Shoryuken::Middleware::Server::ActiveRecord do
6
+ subject { described_class.new }
7
+
8
+ # Mock ActiveRecord to avoid requiring the actual gem in tests
9
+ before do
10
+ # Create mock ActiveRecord module
11
+ active_record_module = Module.new
12
+
13
+ # Create mock Base class with simplified methods
14
+ active_record_base = Class.new do
15
+ @connection_handler = nil
16
+
17
+ def self.clear_active_connections!
18
+ # Mock implementation for Rails < 7.1
19
+ end
20
+
21
+ def self.connection_handler
22
+ @connection_handler ||= Object.new.tap do |handler|
23
+ def handler.clear_active_connections!(_pool_key)
24
+ # Mock implementation for Rails 7.1+
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ active_record_module.const_set('Base', active_record_base)
31
+ stub_const('ActiveRecord', active_record_module)
32
+
33
+ # Mock version checking - start with a simple approach
34
+ def active_record_module.version
35
+ @version ||= Object.new.tap do |v|
36
+ def v.>=(other)
37
+ # For our tests, we'll control this with instance variables
38
+ @is_rails_71_or_higher ||= false
39
+ end
40
+
41
+ def v.rails_71_or_higher!
42
+ @is_rails_71_or_higher = true
43
+ end
44
+
45
+ def v.rails_70!
46
+ @is_rails_71_or_higher = false
47
+ end
48
+ end
49
+ end
50
+
51
+ # Mock Gem::Version
52
+ unless defined?(Gem::Version)
53
+ gem_module = Module.new
54
+ gem_version_class = Class.new do
55
+ def initialize(_version)
56
+ # Simple mock
57
+ end
58
+ end
59
+ gem_module.const_set('Version', gem_version_class)
60
+ stub_const('Gem', gem_module)
61
+ end
62
+ end
63
+
64
+ describe '#call' do
65
+ it 'yields to the block' do
66
+ block_called = false
67
+ subject.call do
68
+ block_called = true
69
+ end
70
+ expect(block_called).to be true
71
+ end
72
+
73
+ it 'returns the value from the block' do
74
+ result = subject.call { 'block_result' }
75
+ expect(result).to eq('block_result')
76
+ end
77
+
78
+ context 'when ActiveRecord version is 7.1 or higher' do
79
+ before do
80
+ # Mock Rails 7.1+ behavior
81
+ allow(ActiveRecord).to receive(:version).and_return(double('>=' => true))
82
+ end
83
+
84
+ it 'calls clear_active_connections! on connection_handler with :all parameter' do
85
+ connection_handler = ActiveRecord::Base.connection_handler
86
+ expect(connection_handler).to receive(:clear_active_connections!).with(:all)
87
+
88
+ subject.call { 'test' }
89
+ end
90
+
91
+ it 'clears connections even when an exception is raised' do
92
+ connection_handler = ActiveRecord::Base.connection_handler
93
+ expect(connection_handler).to receive(:clear_active_connections!).with(:all)
94
+
95
+ expect do
96
+ subject.call { raise StandardError, 'test error' }
97
+ end.to raise_error(StandardError, 'test error')
98
+ end
99
+ end
100
+
101
+ context 'when ActiveRecord version is lower than 7.1' do
102
+ before do
103
+ # Mock Rails < 7.1 behavior
104
+ allow(ActiveRecord).to receive(:version).and_return(double('>=' => false))
105
+ end
106
+
107
+ it 'calls clear_active_connections! directly on ActiveRecord::Base' do
108
+ expect(ActiveRecord::Base).to receive(:clear_active_connections!)
109
+
110
+ subject.call { 'test' }
111
+ end
112
+
113
+ it 'clears connections even when an exception is raised' do
114
+ expect(ActiveRecord::Base).to receive(:clear_active_connections!)
115
+
116
+ expect do
117
+ subject.call { raise StandardError, 'test error' }
118
+ end.to raise_error(StandardError, 'test error')
119
+ end
120
+ end
121
+
122
+ it 'works with middleware arguments (ignores them)' do
123
+ allow(ActiveRecord).to receive(:version).and_return(double('>=' => false))
124
+ expect(ActiveRecord::Base).to receive(:clear_active_connections!)
125
+
126
+ worker = double('worker')
127
+ message = double('message')
128
+
129
+ result = subject.call(worker, message) { 'middleware_result' }
130
+ expect(result).to eq('middleware_result')
131
+ end
132
+ end
133
+ end
@@ -1,4 +1,4 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
2
 
3
3
  describe Shoryuken::Middleware::Server::AutoDelete do
4
4
  let(:queue) { 'default' }
@@ -1,4 +1,4 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
2
 
3
3
  RSpec.describe Shoryuken::Middleware::Server::AutoExtendVisibility do
4
4
  let(:queue) { 'default' }
@@ -64,4 +64,54 @@ RSpec.describe Shoryuken::Middleware::Server::AutoExtendVisibility do
64
64
 
65
65
  Runner.new.run_and_sleep(TestWorker.new, queue, sqs_msg, visibility_timeout)
66
66
  end
67
+
68
+ context 'when batch worker with auto_visibility_timeout' do
69
+ it 'warns and does not extend visibility for batch workers' do
70
+ TestWorker.get_shoryuken_options['auto_visibility_timeout'] = true
71
+
72
+ expect(Shoryuken.logger).to receive(:warn) do |&block|
73
+ expect(block.call).to include("Auto extend visibility isn't supported for batch workers")
74
+ end
75
+
76
+ expect { |b| subject.call(TestWorker.new, queue, [sqs_msg], nil, &b) }.to yield_control
77
+ end
78
+ end
79
+
80
+ context 'when visibility extension fails' do
81
+ it 'logs error when change_visibility raises an exception' do
82
+ TestWorker.get_shoryuken_options['auto_visibility_timeout'] = true
83
+
84
+ allow(sqs_msg).to receive(:queue) { sqs_queue }
85
+ allow(sqs_msg).to receive(:message_id).and_return('test-message-id')
86
+ allow(sqs_msg).to receive(:change_visibility).and_raise(StandardError, 'AWS error')
87
+
88
+ expect(Shoryuken.logger).to receive(:error) do |&block|
89
+ msg = block.call
90
+ expect(msg).to include('Could not auto extend the message')
91
+ expect(msg).to include('test-message-id')
92
+ expect(msg).to include('AWS error')
93
+ end
94
+
95
+ Runner.new.run_and_sleep(TestWorker.new, queue, sqs_msg, visibility_timeout)
96
+ end
97
+ end
98
+
99
+ context 'debug logging' do
100
+ it 'logs debug message when extending visibility' do
101
+ TestWorker.get_shoryuken_options['auto_visibility_timeout'] = true
102
+
103
+ allow(sqs_msg).to receive(:queue) { sqs_queue }
104
+ allow(sqs_msg).to receive(:message_id).and_return('test-message-id')
105
+ allow(sqs_msg).to receive(:change_visibility)
106
+
107
+ expect(Shoryuken.logger).to receive(:debug) do |&block|
108
+ msg = block.call
109
+ expect(msg).to include('Extending message')
110
+ expect(msg).to include('test-message-id')
111
+ expect(msg).to include("by #{visibility_timeout}s")
112
+ end
113
+
114
+ Runner.new.run_and_sleep(TestWorker.new, queue, sqs_msg, visibility_timeout)
115
+ end
116
+ end
67
117
  end
@@ -1,4 +1,4 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
2
 
3
3
  # rubocop:disable /BlockLength, Metrics/
4
4
  RSpec.describe Shoryuken::Middleware::Server::ExponentialBackoffRetry do
@@ -0,0 +1,214 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Shoryuken::Middleware::Server::NonRetryableException do
4
+ let(:queue) { 'default' }
5
+ let(:sqs_queue) { double Shoryuken::Queue }
6
+
7
+ def build_message
8
+ double Shoryuken::Message,
9
+ queue_url: queue,
10
+ body: 'test',
11
+ message_id: SecureRandom.uuid,
12
+ receipt_handle: SecureRandom.uuid
13
+ end
14
+
15
+ let(:sqs_msg) { build_message }
16
+
17
+ before do
18
+ allow(Shoryuken::Client).to receive(:queues).with(queue).and_return(sqs_queue)
19
+ end
20
+
21
+ context 'when non_retryable_exceptions is not configured' do
22
+ it 're-raises the exception' do
23
+ TestWorker.get_shoryuken_options['non_retryable_exceptions'] = nil
24
+
25
+ expect(sqs_queue).not_to receive(:delete_messages)
26
+
27
+ expect {
28
+ subject.call(TestWorker.new, queue, sqs_msg, sqs_msg.body) { raise StandardError, 'test error' }
29
+ }.to raise_error(StandardError, 'test error')
30
+ end
31
+ end
32
+
33
+ context 'when exception is not in non_retryable_exceptions list' do
34
+ it 're-raises the exception' do
35
+ TestWorker.get_shoryuken_options['non_retryable_exceptions'] = [ArgumentError]
36
+
37
+ expect(sqs_queue).not_to receive(:delete_messages)
38
+
39
+ expect {
40
+ subject.call(TestWorker.new, queue, sqs_msg, sqs_msg.body) { raise StandardError, 'test error' }
41
+ }.to raise_error(StandardError, 'test error')
42
+ end
43
+ end
44
+
45
+ context 'when exception is in non_retryable_exceptions list' do
46
+ it 'deletes the message and does not re-raise' do
47
+ TestWorker.get_shoryuken_options['non_retryable_exceptions'] = [StandardError]
48
+
49
+ expect(sqs_queue).to receive(:delete_messages).with(entries: [
50
+ { id: '0', receipt_handle: sqs_msg.receipt_handle }
51
+ ])
52
+
53
+ expect(Shoryuken.logger).to receive(:warn) do |&block|
54
+ expect(block.call).to match(/Non-retryable exception StandardError/)
55
+ end
56
+
57
+ expect {
58
+ subject.call(TestWorker.new, queue, sqs_msg, sqs_msg.body) { raise StandardError, 'test error' }
59
+ }.not_to raise_error
60
+ end
61
+
62
+ it 'logs the exception backtrace in debug mode' do
63
+ TestWorker.get_shoryuken_options['non_retryable_exceptions'] = [StandardError]
64
+
65
+ error = StandardError.new('test error')
66
+ error.set_backtrace(['backtrace line 1', 'backtrace line 2'])
67
+
68
+ allow(sqs_queue).to receive(:delete_messages)
69
+
70
+ expect(Shoryuken.logger).to receive(:warn)
71
+ expect(Shoryuken.logger).to receive(:debug) do |&block|
72
+ expect(block.call).to eq("backtrace line 1\nbacktrace line 2")
73
+ end
74
+
75
+ subject.call(TestWorker.new, queue, sqs_msg, sqs_msg.body) { raise error }
76
+ end
77
+
78
+ it 'handles multiple exception classes' do
79
+ TestWorker.get_shoryuken_options['non_retryable_exceptions'] = [ArgumentError, StandardError]
80
+
81
+ expect(sqs_queue).to receive(:delete_messages).with(entries: [
82
+ { id: '0', receipt_handle: sqs_msg.receipt_handle }
83
+ ])
84
+
85
+ expect {
86
+ subject.call(TestWorker.new, queue, sqs_msg, sqs_msg.body) { raise ArgumentError, 'test error' }
87
+ }.not_to raise_error
88
+ end
89
+
90
+ it 'handles custom exception classes' do
91
+ custom_error = Class.new(StandardError)
92
+ TestWorker.get_shoryuken_options['non_retryable_exceptions'] = [custom_error]
93
+
94
+ expect(sqs_queue).to receive(:delete_messages).with(entries: [
95
+ { id: '0', receipt_handle: sqs_msg.receipt_handle }
96
+ ])
97
+
98
+ expect {
99
+ subject.call(TestWorker.new, queue, sqs_msg, sqs_msg.body) { raise custom_error, 'test error' }
100
+ }.not_to raise_error
101
+ end
102
+ end
103
+
104
+ context 'with batch messages' do
105
+ it 'deletes all messages in the batch' do
106
+ TestWorker.get_shoryuken_options['non_retryable_exceptions'] = [StandardError]
107
+
108
+ sqs_msg2 = build_message
109
+ sqs_msg3 = build_message
110
+ sqs_msgs = [sqs_msg, sqs_msg2, sqs_msg3]
111
+
112
+ expect(sqs_queue).to receive(:delete_messages).with(entries: [
113
+ { id: '0', receipt_handle: sqs_msg.receipt_handle },
114
+ { id: '1', receipt_handle: sqs_msg2.receipt_handle },
115
+ { id: '2', receipt_handle: sqs_msg3.receipt_handle }
116
+ ])
117
+
118
+ expect(Shoryuken.logger).to receive(:warn) do |&block|
119
+ expect(block.call).to match(/Non-retryable exception StandardError/)
120
+ expect(block.call).to match(/#{sqs_msg.message_id}/)
121
+ expect(block.call).to match(/#{sqs_msg2.message_id}/)
122
+ expect(block.call).to match(/#{sqs_msg3.message_id}/)
123
+ end
124
+
125
+ expect {
126
+ subject.call(TestWorker.new, queue, sqs_msgs, [sqs_msg.body, sqs_msg2.body, sqs_msg3.body]) do
127
+ raise StandardError, 'test error'
128
+ end
129
+ }.not_to raise_error
130
+ end
131
+ end
132
+
133
+ context 'when no exception occurs' do
134
+ it 'does not delete the message' do
135
+ TestWorker.get_shoryuken_options['non_retryable_exceptions'] = [StandardError]
136
+
137
+ expect(sqs_queue).not_to receive(:delete_messages)
138
+
139
+ subject.call(TestWorker.new, queue, sqs_msg, sqs_msg.body) {}
140
+ end
141
+ end
142
+
143
+ context 'when using lambda for dynamic classification' do
144
+ it 'calls the lambda with the exception and deletes if lambda returns true' do
145
+ lambda_called = false
146
+ lambda_result = true
147
+
148
+ TestWorker.get_shoryuken_options['non_retryable_exceptions'] = lambda do |error|
149
+ lambda_called = true
150
+ expect(error).to be_a(StandardError)
151
+ expect(error.message).to eq('test error')
152
+ lambda_result
153
+ end
154
+
155
+ expect(sqs_queue).to receive(:delete_messages).with(entries: [
156
+ { id: '0', receipt_handle: sqs_msg.receipt_handle }
157
+ ])
158
+
159
+ expect {
160
+ subject.call(TestWorker.new, queue, sqs_msg, sqs_msg.body) { raise StandardError, 'test error' }
161
+ }.not_to raise_error
162
+
163
+ expect(lambda_called).to be true
164
+ end
165
+
166
+ it 're-raises if lambda returns false' do
167
+ TestWorker.get_shoryuken_options['non_retryable_exceptions'] = ->(_error) { false }
168
+
169
+ expect(sqs_queue).not_to receive(:delete_messages)
170
+
171
+ expect {
172
+ subject.call(TestWorker.new, queue, sqs_msg, sqs_msg.body) { raise StandardError, 'test error' }
173
+ }.to raise_error(StandardError, 'test error')
174
+ end
175
+
176
+ it 'supports complex lambda logic based on exception properties' do
177
+ TestWorker.get_shoryuken_options['non_retryable_exceptions'] = lambda do |error|
178
+ error.is_a?(ArgumentError) || (error.is_a?(StandardError) && error.message.include?('permanent'))
179
+ end
180
+
181
+ # ArgumentError should be deleted
182
+ expect(sqs_queue).to receive(:delete_messages).with(entries: [
183
+ { id: '0', receipt_handle: sqs_msg.receipt_handle }
184
+ ])
185
+
186
+ expect {
187
+ subject.call(TestWorker.new, queue, sqs_msg, sqs_msg.body) { raise ArgumentError, 'invalid argument' }
188
+ }.not_to raise_error
189
+
190
+ # StandardError with 'permanent' should be deleted
191
+ sqs_msg2 = build_message
192
+ allow(Shoryuken::Client).to receive(:queues).with(queue).and_return(sqs_queue)
193
+
194
+ expect(sqs_queue).to receive(:delete_messages).with(entries: [
195
+ { id: '0', receipt_handle: sqs_msg2.receipt_handle }
196
+ ])
197
+
198
+ expect {
199
+ subject.call(TestWorker.new, queue, sqs_msg2, sqs_msg2.body) { raise StandardError, 'permanent failure' }
200
+ }.not_to raise_error
201
+
202
+ # StandardError without 'permanent' should be retried
203
+ sqs_msg3 = build_message
204
+ allow(Shoryuken::Client).to receive(:queues).with(queue).and_return(sqs_queue)
205
+
206
+ expect(sqs_queue).not_to receive(:delete_messages)
207
+
208
+ expect {
209
+ subject.call(TestWorker.new, queue, sqs_msg3, sqs_msg3.body) { raise StandardError, 'temporary failure' }
210
+ }.to raise_error(StandardError, 'temporary failure')
211
+ end
212
+ end
213
+ end
214
+
@@ -1,4 +1,4 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
2
 
3
3
  RSpec.describe Shoryuken::Middleware::Server::Timing do
4
4
  let(:queue) { 'default' }
@@ -1,11 +1,11 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
2
 
3
3
  RSpec.describe Shoryuken::Options do
4
4
  subject { Shoryuken.shoryuken_options }
5
5
 
6
6
  describe '.on_stop' do
7
7
  specify do
8
- on_stop = Proc.new {}
8
+ on_stop = proc {}
9
9
  Shoryuken.on_stop(&on_stop)
10
10
 
11
11
  expect(Shoryuken.stop_callback).to eq(on_stop)
@@ -14,7 +14,7 @@ RSpec.describe Shoryuken::Options do
14
14
 
15
15
  describe '.on_start' do
16
16
  specify do
17
- on_start = Proc.new {}
17
+ on_start = proc {}
18
18
  Shoryuken.on_start(&on_start)
19
19
 
20
20
  expect(Shoryuken.start_callback).to eq(on_start)
@@ -41,6 +41,36 @@ RSpec.describe Shoryuken::Options do
41
41
  end
42
42
  end
43
43
 
44
+ describe '.add_group with polling_strategy' do
45
+ before do
46
+ Shoryuken.groups.clear
47
+ end
48
+
49
+ it 'stores the polling_strategy in the group configuration' do
50
+ Shoryuken.add_group('group1', 25, polling_strategy: Shoryuken::Polling::StrictPriority)
51
+
52
+ expect(Shoryuken.groups['group1'][:polling_strategy]).to eq(Shoryuken::Polling::StrictPriority)
53
+ end
54
+
55
+ it 'defaults polling_strategy to nil when not provided' do
56
+ Shoryuken.add_group('group1', 25)
57
+
58
+ expect(Shoryuken.groups['group1'][:polling_strategy]).to be_nil
59
+ end
60
+
61
+ it 'accepts a string polling_strategy' do
62
+ Shoryuken.add_group('group1', 25, polling_strategy: 'StrictPriority')
63
+
64
+ expect(Shoryuken.groups['group1'][:polling_strategy]).to eq('StrictPriority')
65
+ end
66
+
67
+ it 'raises InvalidPollingStrategyError for invalid polling_strategy type' do
68
+ expect {
69
+ Shoryuken.add_group('group1', 25, polling_strategy: 123)
70
+ }.to raise_error(Shoryuken::Errors::InvalidPollingStrategyError)
71
+ end
72
+ end
73
+
44
74
  describe '.delay works for each group' do
45
75
  specify do
46
76
  Shoryuken.add_group('group1', 25)
@@ -81,7 +111,7 @@ RSpec.describe Shoryuken::Options do
81
111
 
82
112
  expect(Shoryuken.sqs_client_receive_message_opts).to eq(
83
113
  'default' => { test: 1 },
84
- 'group1' => { test: 2 }
114
+ 'group1' => { test: 2 }
85
115
  )
86
116
  end
87
117
  end
@@ -188,8 +218,8 @@ RSpec.describe Shoryuken::Options do
188
218
  end
189
219
 
190
220
  specify do
191
- expect { Shoryuken.polling_strategy('default') }.to raise_error(ArgumentError)
192
- expect { Shoryuken.polling_strategy('group1') }.to raise_error(ArgumentError)
221
+ expect { Shoryuken.polling_strategy('default') }.to raise_error(Shoryuken::Errors::InvalidPollingStrategyError)
222
+ expect { Shoryuken.polling_strategy('group1') }.to raise_error(Shoryuken::Errors::InvalidPollingStrategyError)
193
223
  end
194
224
  end
195
225
 
@@ -212,5 +242,18 @@ RSpec.describe Shoryuken::Options do
212
242
  expect(Shoryuken.polling_strategy('group1')).to eq Bar
213
243
  end
214
244
  end
245
+
246
+ context 'when set via add_group' do
247
+ before do
248
+ class Baz < Shoryuken::Polling::BaseStrategy; end
249
+
250
+ Shoryuken.groups.clear
251
+ Shoryuken.add_group('custom_group', 25, polling_strategy: Baz)
252
+ end
253
+
254
+ specify do
255
+ expect(Shoryuken.polling_strategy('custom_group')).to eq Baz
256
+ end
257
+ end
215
258
  end
216
259
  end