shoryuken 7.0.0.alpha1 → 7.0.0.alpha2

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 (105) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/push.yml +3 -3
  3. data/.github/workflows/specs.yml +6 -9
  4. data/.github/workflows/verify-action-pins.yml +1 -1
  5. data/.rspec +2 -1
  6. data/.ruby-version +1 -1
  7. data/Appraisals +0 -6
  8. data/CHANGELOG.md +186 -142
  9. data/Gemfile +1 -0
  10. data/README.md +12 -13
  11. data/bin/cli/base.rb +1 -2
  12. data/bin/cli/sqs.rb +5 -4
  13. data/bin/shoryuken +2 -1
  14. data/gemfiles/rails_7_0.gemfile +10 -10
  15. data/gemfiles/rails_7_1.gemfile +10 -9
  16. data/gemfiles/rails_7_2.gemfile +10 -9
  17. data/gemfiles/rails_8_0.gemfile +10 -9
  18. data/lib/shoryuken/body_parser.rb +3 -1
  19. data/lib/shoryuken/client.rb +2 -0
  20. data/lib/shoryuken/default_exception_handler.rb +2 -0
  21. data/lib/shoryuken/default_worker_registry.rb +11 -11
  22. data/lib/shoryuken/environment_loader.rb +6 -6
  23. data/lib/shoryuken/extensions/active_job_adapter.rb +8 -6
  24. data/lib/shoryuken/extensions/active_job_concurrent_send_adapter.rb +5 -5
  25. data/lib/shoryuken/extensions/active_job_extensions.rb +2 -0
  26. data/lib/shoryuken/fetcher.rb +4 -2
  27. data/lib/shoryuken/helpers/atomic_boolean.rb +44 -0
  28. data/lib/shoryuken/helpers/atomic_counter.rb +104 -0
  29. data/lib/shoryuken/helpers/atomic_hash.rb +182 -0
  30. data/lib/shoryuken/helpers/hash_utils.rb +56 -0
  31. data/lib/shoryuken/helpers/string_utils.rb +65 -0
  32. data/lib/shoryuken/inline_message.rb +22 -0
  33. data/lib/shoryuken/launcher.rb +2 -0
  34. data/lib/shoryuken/logging.rb +19 -5
  35. data/lib/shoryuken/manager.rb +6 -4
  36. data/lib/shoryuken/message.rb +2 -0
  37. data/lib/shoryuken/middleware/chain.rb +2 -0
  38. data/lib/shoryuken/middleware/server/active_record.rb +2 -0
  39. data/lib/shoryuken/middleware/server/auto_delete.rb +2 -0
  40. data/lib/shoryuken/middleware/server/auto_extend_visibility.rb +10 -10
  41. data/lib/shoryuken/middleware/server/exponential_backoff_retry.rb +5 -3
  42. data/lib/shoryuken/middleware/server/timing.rb +2 -0
  43. data/lib/shoryuken/options.rb +9 -5
  44. data/lib/shoryuken/polling/base_strategy.rb +126 -0
  45. data/lib/shoryuken/polling/queue_configuration.rb +103 -0
  46. data/lib/shoryuken/polling/strict_priority.rb +2 -0
  47. data/lib/shoryuken/polling/weighted_round_robin.rb +2 -0
  48. data/lib/shoryuken/processor.rb +5 -2
  49. data/lib/shoryuken/queue.rb +6 -4
  50. data/lib/shoryuken/runner.rb +9 -12
  51. data/lib/shoryuken/util.rb +6 -6
  52. data/lib/shoryuken/version.rb +3 -1
  53. data/lib/shoryuken/worker/default_executor.rb +2 -0
  54. data/lib/shoryuken/worker/inline_executor.rb +3 -1
  55. data/lib/shoryuken/worker.rb +2 -0
  56. data/lib/shoryuken/worker_registry.rb +2 -0
  57. data/lib/shoryuken.rb +8 -28
  58. data/shoryuken.gemspec +6 -6
  59. data/spec/integration/launcher_spec.rb +2 -3
  60. data/spec/shared_examples_for_active_job.rb +13 -8
  61. data/spec/shoryuken/body_parser_spec.rb +1 -2
  62. data/spec/shoryuken/client_spec.rb +1 -1
  63. data/spec/shoryuken/default_exception_handler_spec.rb +9 -10
  64. data/spec/shoryuken/default_worker_registry_spec.rb +1 -2
  65. data/spec/shoryuken/environment_loader_spec.rb +9 -8
  66. data/spec/shoryuken/extensions/active_job_adapter_spec.rb +2 -1
  67. data/spec/shoryuken/extensions/active_job_base_spec.rb +2 -1
  68. data/spec/shoryuken/extensions/active_job_concurrent_send_adapter_spec.rb +2 -1
  69. data/spec/shoryuken/extensions/active_job_wrapper_spec.rb +2 -1
  70. data/spec/shoryuken/fetcher_spec.rb +23 -26
  71. data/spec/shoryuken/helpers/atomic_boolean_spec.rb +196 -0
  72. data/spec/shoryuken/helpers/atomic_counter_spec.rb +177 -0
  73. data/spec/shoryuken/helpers/atomic_hash_spec.rb +307 -0
  74. data/spec/shoryuken/helpers/hash_utils_spec.rb +145 -0
  75. data/spec/shoryuken/helpers/string_utils_spec.rb +124 -0
  76. data/spec/shoryuken/helpers_integration_spec.rb +96 -0
  77. data/spec/shoryuken/inline_message_spec.rb +196 -0
  78. data/spec/shoryuken/launcher_spec.rb +1 -2
  79. data/spec/shoryuken/manager_spec.rb +1 -2
  80. data/spec/shoryuken/middleware/chain_spec.rb +1 -1
  81. data/spec/shoryuken/middleware/server/auto_delete_spec.rb +1 -1
  82. data/spec/shoryuken/middleware/server/auto_extend_visibility_spec.rb +1 -1
  83. data/spec/shoryuken/middleware/server/exponential_backoff_retry_spec.rb +1 -1
  84. data/spec/shoryuken/middleware/server/timing_spec.rb +1 -1
  85. data/spec/shoryuken/options_spec.rb +4 -4
  86. data/spec/shoryuken/polling/base_strategy_spec.rb +280 -0
  87. data/spec/shoryuken/polling/queue_configuration_spec.rb +195 -0
  88. data/spec/shoryuken/polling/strict_priority_spec.rb +1 -1
  89. data/spec/shoryuken/polling/weighted_round_robin_spec.rb +1 -1
  90. data/spec/shoryuken/processor_spec.rb +1 -1
  91. data/spec/shoryuken/queue_spec.rb +2 -3
  92. data/spec/shoryuken/runner_spec.rb +1 -3
  93. data/spec/shoryuken/util_spec.rb +1 -1
  94. data/spec/shoryuken/worker/default_executor_spec.rb +1 -1
  95. data/spec/shoryuken/worker/inline_executor_spec.rb +1 -1
  96. data/spec/shoryuken/worker_spec.rb +15 -11
  97. data/spec/shoryuken_spec.rb +1 -1
  98. data/spec/spec_helper.rb +16 -0
  99. metadata +60 -27
  100. data/.github/FUNDING.yml +0 -12
  101. data/gemfiles/rails_6_1.gemfile +0 -18
  102. data/lib/shoryuken/core_ext.rb +0 -69
  103. data/lib/shoryuken/polling/base.rb +0 -67
  104. data/shoryuken.jpg +0 -0
  105. data/spec/shoryuken/core_ext_spec.rb +0 -40
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Shoryuken::InlineMessage do
4
+ let(:body) { '{"message": "test"}' }
5
+ let(:attributes) { { 'SentTimestamp' => '1234567890' } }
6
+ let(:md5_of_body) { 'abc123def456' }
7
+ let(:md5_of_message_attributes) { 'def456abc123' }
8
+ let(:message_attributes) { { 'CustomAttribute' => { string_value: 'value', data_type: 'String' } } }
9
+ let(:message_id) { 'msg-12345' }
10
+ let(:receipt_handle) { 'receipt-handle-12345' }
11
+ let(:delete) { nil }
12
+ let(:queue_name) { 'test-queue' }
13
+
14
+ describe '#new' do
15
+ context 'with positional arguments' do
16
+ subject do
17
+ described_class.new(
18
+ body: body,
19
+ attributes: attributes,
20
+ md5_of_body: md5_of_body,
21
+ md5_of_message_attributes: md5_of_message_attributes,
22
+ message_attributes: message_attributes,
23
+ message_id: message_id,
24
+ receipt_handle: receipt_handle,
25
+ delete: delete,
26
+ queue_name: queue_name
27
+ )
28
+ end
29
+
30
+ it 'initializes with all attributes' do
31
+ expect(subject.body).to eq(body)
32
+ expect(subject.attributes).to eq(attributes)
33
+ expect(subject.md5_of_body).to eq(md5_of_body)
34
+ expect(subject.md5_of_message_attributes).to eq(md5_of_message_attributes)
35
+ expect(subject.message_attributes).to eq(message_attributes)
36
+ expect(subject.message_id).to eq(message_id)
37
+ expect(subject.receipt_handle).to eq(receipt_handle)
38
+ expect(subject.delete).to eq(delete)
39
+ expect(subject.queue_name).to eq(queue_name)
40
+ end
41
+ end
42
+
43
+ context 'with keyword arguments' do
44
+ subject do
45
+ described_class.new(
46
+ body: body,
47
+ attributes: attributes,
48
+ md5_of_body: md5_of_body,
49
+ md5_of_message_attributes: md5_of_message_attributes,
50
+ message_attributes: message_attributes,
51
+ message_id: message_id,
52
+ receipt_handle: receipt_handle,
53
+ delete: delete,
54
+ queue_name: queue_name
55
+ )
56
+ end
57
+
58
+ it 'initializes with all attributes' do
59
+ expect(subject.body).to eq(body)
60
+ expect(subject.attributes).to eq(attributes)
61
+ expect(subject.md5_of_body).to eq(md5_of_body)
62
+ expect(subject.md5_of_message_attributes).to eq(md5_of_message_attributes)
63
+ expect(subject.message_attributes).to eq(message_attributes)
64
+ expect(subject.message_id).to eq(message_id)
65
+ expect(subject.receipt_handle).to eq(receipt_handle)
66
+ expect(subject.delete).to eq(delete)
67
+ expect(subject.queue_name).to eq(queue_name)
68
+ end
69
+ end
70
+
71
+ context 'with nil values' do
72
+ subject do
73
+ described_class.new(
74
+ body: body,
75
+ attributes: nil,
76
+ md5_of_body: nil,
77
+ md5_of_message_attributes: nil,
78
+ message_attributes: message_attributes,
79
+ message_id: nil,
80
+ receipt_handle: nil,
81
+ delete: nil,
82
+ queue_name: queue_name
83
+ )
84
+ end
85
+
86
+ it 'handles nil values correctly' do
87
+ expect(subject.body).to eq(body)
88
+ expect(subject.attributes).to be_nil
89
+ expect(subject.md5_of_body).to be_nil
90
+ expect(subject.md5_of_message_attributes).to be_nil
91
+ expect(subject.message_attributes).to eq(message_attributes)
92
+ expect(subject.message_id).to be_nil
93
+ expect(subject.receipt_handle).to be_nil
94
+ expect(subject.delete).to be_nil
95
+ expect(subject.queue_name).to eq(queue_name)
96
+ end
97
+ end
98
+
99
+ context 'with minimal required attributes' do
100
+ subject { described_class.new(body: body, queue_name: queue_name) }
101
+
102
+ it 'initializes with only required attributes' do
103
+ expect(subject.body).to eq(body)
104
+ expect(subject.queue_name).to eq(queue_name)
105
+ expect(subject.attributes).to be_nil
106
+ expect(subject.md5_of_body).to be_nil
107
+ expect(subject.md5_of_message_attributes).to be_nil
108
+ expect(subject.message_attributes).to be_nil
109
+ expect(subject.message_id).to be_nil
110
+ expect(subject.receipt_handle).to be_nil
111
+ expect(subject.delete).to be_nil
112
+ end
113
+ end
114
+ end
115
+
116
+ describe 'attribute accessors' do
117
+ subject do
118
+ described_class.new(
119
+ body: body,
120
+ attributes: attributes,
121
+ md5_of_body: md5_of_body,
122
+ md5_of_message_attributes: md5_of_message_attributes,
123
+ message_attributes: message_attributes,
124
+ message_id: message_id,
125
+ receipt_handle: receipt_handle,
126
+ delete: delete,
127
+ queue_name: queue_name
128
+ )
129
+ end
130
+
131
+ it 'provides read access to all attributes' do
132
+ expect(subject.body).to eq(body)
133
+ expect(subject.attributes).to eq(attributes)
134
+ expect(subject.md5_of_body).to eq(md5_of_body)
135
+ expect(subject.md5_of_message_attributes).to eq(md5_of_message_attributes)
136
+ expect(subject.message_attributes).to eq(message_attributes)
137
+ expect(subject.message_id).to eq(message_id)
138
+ expect(subject.receipt_handle).to eq(receipt_handle)
139
+ expect(subject.delete).to eq(delete)
140
+ expect(subject.queue_name).to eq(queue_name)
141
+ end
142
+
143
+ it 'provides write access to all attributes' do
144
+ new_body = '{"updated": "message"}'
145
+ new_queue_name = 'updated-queue'
146
+
147
+ subject.body = new_body
148
+ subject.queue_name = new_queue_name
149
+
150
+ expect(subject.body).to eq(new_body)
151
+ expect(subject.queue_name).to eq(new_queue_name)
152
+ end
153
+ end
154
+
155
+ describe 'struct behavior' do
156
+ subject { described_class.new(body: body, queue_name: queue_name) }
157
+
158
+ it 'behaves like a struct' do
159
+ expect(subject).to be_a(Struct)
160
+ expect(subject.class.superclass).to eq(Struct)
161
+ end
162
+
163
+ it 'supports array-like access' do
164
+ expect(subject[0]).to eq(body) # body is first attribute
165
+ expect(subject[-1]).to eq(queue_name) # queue_name is last attribute
166
+ end
167
+
168
+ it 'supports enumeration' do
169
+ values = subject.to_a
170
+ expect(values.first).to eq(body)
171
+ expect(values.last).to eq(queue_name)
172
+ expect(values.length).to eq(9) # 9 attributes total
173
+ end
174
+
175
+ it 'supports hash conversion' do
176
+ hash = subject.to_h
177
+ expect(hash[:body]).to eq(body)
178
+ expect(hash[:queue_name]).to eq(queue_name)
179
+ expect(hash.keys).to contain_exactly(
180
+ :body, :attributes, :md5_of_body, :md5_of_message_attributes,
181
+ :message_attributes, :message_id, :receipt_handle, :delete, :queue_name
182
+ )
183
+ end
184
+ end
185
+
186
+ describe 'equality' do
187
+ let(:message1) { described_class.new(body: body, queue_name: queue_name) }
188
+ let(:message2) { described_class.new(body: body, queue_name: queue_name) }
189
+ let(:message3) { described_class.new(body: 'different', queue_name: queue_name) }
190
+
191
+ it 'compares messages by attribute values' do
192
+ expect(message1).to eq(message2)
193
+ expect(message1).not_to eq(message3)
194
+ end
195
+ end
196
+ end
@@ -1,5 +1,4 @@
1
- require 'spec_helper'
2
- require 'shoryuken/launcher'
1
+ # frozen_string_literal: true
3
2
 
4
3
  RSpec.describe Shoryuken::Launcher do
5
4
  let(:executor) do
@@ -1,5 +1,4 @@
1
- require 'spec_helper'
2
- require 'shoryuken/manager'
1
+ # frozen_string_literal: true
3
2
 
4
3
  RSpec::Matchers.define :queue_config_of do |expected|
5
4
  match do |actual|
@@ -1,4 +1,4 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
2
 
3
3
  RSpec.describe Shoryuken::Middleware::Chain do
4
4
  class CustomMiddleware
@@ -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' }
@@ -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
@@ -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)
@@ -81,7 +81,7 @@ RSpec.describe Shoryuken::Options do
81
81
 
82
82
  expect(Shoryuken.sqs_client_receive_message_opts).to eq(
83
83
  'default' => { test: 1 },
84
- 'group1' => { test: 2 }
84
+ 'group1' => { test: 2 }
85
85
  )
86
86
  end
87
87
  end
@@ -0,0 +1,280 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Shoryuken::Polling::BaseStrategy do
4
+ # Create a concrete implementation for testing
5
+ let(:test_strategy_class) do
6
+ Class.new(described_class) do
7
+ def initialize(queues, delay = nil)
8
+ @queues = queues
9
+ @delay = delay
10
+ end
11
+
12
+ def next_queue
13
+ @queues.first
14
+ end
15
+
16
+ def messages_found(queue, messages_found)
17
+ # Test implementation - store the call
18
+ @last_messages_found = { queue: queue, count: messages_found }
19
+ end
20
+
21
+ def active_queues
22
+ @queues
23
+ end
24
+
25
+ attr_reader :last_messages_found
26
+ end
27
+ end
28
+
29
+ let(:strategy) { test_strategy_class.new(['queue1', 'queue2']) }
30
+
31
+ describe 'abstract interface' do
32
+ it 'includes Util module' do
33
+ expect(described_class.included_modules).to include(Shoryuken::Util)
34
+ end
35
+
36
+ describe '#next_queue' do
37
+ it 'raises NotImplementedError in base class' do
38
+ base_strategy = described_class.new
39
+
40
+ expect { base_strategy.next_queue }.to raise_error(NotImplementedError)
41
+ end
42
+
43
+ it 'can be implemented by subclasses' do
44
+ expect(strategy.next_queue).to eq('queue1')
45
+ end
46
+ end
47
+
48
+ describe '#messages_found' do
49
+ it 'raises NotImplementedError in base class' do
50
+ base_strategy = described_class.new
51
+
52
+ expect { base_strategy.messages_found('queue', 5) }.to raise_error(NotImplementedError)
53
+ end
54
+
55
+ it 'can be implemented by subclasses' do
56
+ strategy.messages_found('test_queue', 3)
57
+
58
+ expect(strategy.last_messages_found).to eq({ queue: 'test_queue', count: 3 })
59
+ end
60
+
61
+ it 'accepts zero messages found' do
62
+ strategy.messages_found('empty_queue', 0)
63
+
64
+ expect(strategy.last_messages_found).to eq({ queue: 'empty_queue', count: 0 })
65
+ end
66
+ end
67
+
68
+ describe '#message_processed' do
69
+ it 'has default empty implementation' do
70
+ base_strategy = described_class.new
71
+
72
+ expect { base_strategy.message_processed('queue') }.not_to raise_error
73
+ end
74
+
75
+ it 'can be overridden by subclasses' do
76
+ # Default implementation should do nothing
77
+ expect { strategy.message_processed('queue') }.not_to raise_error
78
+ end
79
+ end
80
+
81
+ describe '#active_queues' do
82
+ it 'raises NotImplementedError in base class' do
83
+ base_strategy = described_class.new
84
+
85
+ expect { base_strategy.active_queues }.to raise_error(NotImplementedError)
86
+ end
87
+
88
+ it 'can be implemented by subclasses' do
89
+ expect(strategy.active_queues).to eq(['queue1', 'queue2'])
90
+ end
91
+ end
92
+ end
93
+
94
+ describe '#==' do
95
+ context 'when comparing with Array' do
96
+ it 'compares against @queues instance variable' do
97
+ expect(strategy).to eq(['queue1', 'queue2'])
98
+ end
99
+
100
+ it 'returns false for different arrays' do
101
+ expect(strategy).not_to eq(['queue3', 'queue4'])
102
+ end
103
+
104
+ it 'returns false for arrays with different order' do
105
+ expect(strategy).not_to eq(['queue2', 'queue1'])
106
+ end
107
+ end
108
+
109
+ context 'when comparing with another strategy' do
110
+ it 'compares active_queues when other responds to active_queues' do
111
+ other_strategy = test_strategy_class.new(['queue1', 'queue2'])
112
+
113
+ expect(strategy).to eq(other_strategy)
114
+ end
115
+
116
+ it 'returns false when active_queues differ' do
117
+ other_strategy = test_strategy_class.new(['queue3', 'queue4'])
118
+
119
+ expect(strategy).not_to eq(other_strategy)
120
+ end
121
+
122
+ it 'handles strategies with different active_queues order' do
123
+ other_strategy = test_strategy_class.new(['queue2', 'queue1'])
124
+
125
+ expect(strategy).not_to eq(other_strategy)
126
+ end
127
+ end
128
+
129
+ context 'when comparing with objects that do not respond to active_queues' do
130
+ it 'returns false for strings' do
131
+ expect(strategy).not_to eq('some_string')
132
+ end
133
+
134
+ it 'returns false for numbers' do
135
+ expect(strategy).not_to eq(123)
136
+ end
137
+
138
+ it 'returns false for hashes' do
139
+ expect(strategy).not_to eq({ queues: ['queue1', 'queue2'] })
140
+ end
141
+
142
+ it 'returns false for objects without active_queues method' do
143
+ plain_object = Object.new
144
+
145
+ expect(strategy).not_to eq(plain_object)
146
+ end
147
+ end
148
+
149
+ context 'when @queues is not set' do
150
+ let(:strategy_without_queues) { test_strategy_class.new(nil) }
151
+
152
+ it 'handles nil @queues when comparing with nil' do
153
+ # nil is not an Array, so this goes to the else branch
154
+ # nil doesn't respond to active_queues, so it returns false
155
+ expect(strategy_without_queues == nil).to be false
156
+ end
157
+
158
+ it 'handles nil @queues when comparing with empty array' do
159
+ expect(strategy_without_queues == []).to be false
160
+ end
161
+ end
162
+ end
163
+
164
+ describe '#delay' do
165
+ context 'when delay is set on strategy' do
166
+ let(:strategy_with_delay) { test_strategy_class.new(['queue1'], 5.0) }
167
+
168
+ it 'returns the strategy-specific delay' do
169
+ expect(strategy_with_delay.delay).to eq(5.0)
170
+ end
171
+ end
172
+
173
+ context 'when delay is not set on strategy' do
174
+ before do
175
+ allow(Shoryuken.options).to receive(:[]).with(:delay).and_return(3.5)
176
+ end
177
+
178
+ it 'returns the global Shoryuken delay converted to float' do
179
+ expect(strategy.delay).to eq(3.5)
180
+ end
181
+ end
182
+
183
+ context 'when global delay is a string' do
184
+ before do
185
+ allow(Shoryuken.options).to receive(:[]).with(:delay).and_return('2.5')
186
+ end
187
+
188
+ it 'converts string delay to float' do
189
+ expect(strategy.delay).to eq(2.5)
190
+ end
191
+ end
192
+
193
+ context 'when global delay is an integer' do
194
+ before do
195
+ allow(Shoryuken.options).to receive(:[]).with(:delay).and_return(4)
196
+ end
197
+
198
+ it 'converts integer delay to float' do
199
+ expect(strategy.delay).to eq(4.0)
200
+ end
201
+ end
202
+
203
+ context 'when delay is explicitly set to nil' do
204
+ let(:strategy_with_nil_delay) { test_strategy_class.new(['queue1'], nil) }
205
+
206
+ before do
207
+ allow(Shoryuken.options).to receive(:[]).with(:delay).and_return(1.5)
208
+ end
209
+
210
+ it 'falls back to global delay' do
211
+ expect(strategy_with_nil_delay.delay).to eq(1.5)
212
+ end
213
+ end
214
+ end
215
+
216
+ describe 'inheritance patterns' do
217
+ it 'allows subclasses to call super for implemented methods' do
218
+ subclass = Class.new(described_class) do
219
+ def next_queue
220
+ begin
221
+ super
222
+ rescue NotImplementedError
223
+ 'fallback'
224
+ end
225
+ end
226
+
227
+ def active_queues
228
+ []
229
+ end
230
+
231
+ def messages_found(queue, count)
232
+ # Implementation required
233
+ end
234
+ end
235
+
236
+ instance = subclass.new
237
+ expect(instance.next_queue).to eq('fallback')
238
+ end
239
+
240
+ it 'supports method chaining in subclasses' do
241
+ chainable_strategy = Class.new(described_class) do
242
+ def initialize(queues)
243
+ @queues = queues
244
+ @call_chain = []
245
+ end
246
+
247
+ def next_queue
248
+ @call_chain << :next_queue
249
+ @queues.first
250
+ end
251
+
252
+ def messages_found(queue, count)
253
+ @call_chain << :messages_found
254
+ self
255
+ end
256
+
257
+ def active_queues
258
+ @call_chain << :active_queues
259
+ @queues
260
+ end
261
+
262
+ attr_reader :call_chain
263
+ end
264
+
265
+ strategy = chainable_strategy.new(['test'])
266
+ result = strategy.messages_found('queue', 1)
267
+
268
+ expect(result).to be(strategy)
269
+ expect(strategy.call_chain).to eq([:messages_found])
270
+ end
271
+ end
272
+
273
+ describe 'utility method access' do
274
+ it 'provides access to utility methods through Util module' do
275
+ # Verify that utility methods are available
276
+ expect(strategy).to respond_to(:unparse_queues)
277
+ expect(strategy).to respond_to(:logger)
278
+ end
279
+ end
280
+ end