shoryuken 7.0.0.alpha2 → 7.0.0.rc1

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.
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_job'
4
+ require 'shared_examples_for_active_job'
5
+ require 'shoryuken/extensions/active_job_adapter'
6
+ require 'shoryuken/extensions/active_job_extensions'
7
+
8
+ RSpec.describe 'ActiveJob Continuation support' do
9
+ let(:adapter) { ActiveJob::QueueAdapters::ShoryukenAdapter.new }
10
+ let(:job) do
11
+ job = TestJob.new
12
+ job.sqs_send_message_parameters = {}
13
+ job
14
+ end
15
+ let(:queue) { double('Queue', fifo?: false) }
16
+
17
+ before do
18
+ allow(Shoryuken::Client).to receive(:queues).with(job.queue_name).and_return(queue)
19
+ allow(Shoryuken).to receive(:register_worker)
20
+ end
21
+
22
+ describe '#stopping?' do
23
+ context 'when Launcher is not initialized' do
24
+ it 'returns false' do
25
+ runner = instance_double(Shoryuken::Runner, launcher: nil)
26
+ allow(Shoryuken::Runner).to receive(:instance).and_return(runner)
27
+
28
+ expect(adapter.stopping?).to be false
29
+ end
30
+ end
31
+
32
+ context 'when Launcher is initialized' do
33
+ let(:runner) { instance_double(Shoryuken::Runner) }
34
+ let(:launcher) { instance_double(Shoryuken::Launcher) }
35
+
36
+ before do
37
+ allow(Shoryuken::Runner).to receive(:instance).and_return(runner)
38
+ allow(runner).to receive(:launcher).and_return(launcher)
39
+ end
40
+
41
+ it 'returns false when not stopping' do
42
+ allow(launcher).to receive(:stopping?).and_return(false)
43
+ expect(adapter.stopping?).to be false
44
+ end
45
+
46
+ it 'returns true when stopping' do
47
+ allow(launcher).to receive(:stopping?).and_return(true)
48
+ expect(adapter.stopping?).to be true
49
+ end
50
+ end
51
+ end
52
+
53
+ describe '#enqueue_at with past timestamps' do
54
+ let(:past_timestamp) { Time.current.to_f - 60 } # 60 seconds ago
55
+
56
+ it 'enqueues with negative delay_seconds when timestamp is in the past' do
57
+ expect(queue).to receive(:send_message) do |hash|
58
+ expect(hash[:delay_seconds]).to be <= 0
59
+ expect(hash[:delay_seconds]).to be >= -61 # Allow for rounding and timing
60
+ end
61
+
62
+ adapter.enqueue_at(job, past_timestamp)
63
+ end
64
+
65
+ it 'does not raise an error for past timestamps' do
66
+ allow(queue).to receive(:send_message)
67
+
68
+ expect { adapter.enqueue_at(job, past_timestamp) }.not_to raise_error
69
+ end
70
+ end
71
+
72
+ describe '#enqueue_at with future timestamps' do
73
+ let(:future_timestamp) { Time.current.to_f + 60 } # 60 seconds from now
74
+
75
+ it 'enqueues with delay_seconds when timestamp is in the future' do
76
+ expect(queue).to receive(:send_message) do |hash|
77
+ expect(hash[:delay_seconds]).to be > 0
78
+ expect(hash[:delay_seconds]).to be <= 60
79
+ end
80
+
81
+ adapter.enqueue_at(job, future_timestamp)
82
+ end
83
+ end
84
+
85
+ describe '#enqueue_at with current timestamp' do
86
+ let(:current_timestamp) { Time.current.to_f }
87
+
88
+ it 'enqueues with delay_seconds close to 0' do
89
+ expect(queue).to receive(:send_message) do |hash|
90
+ expect(hash[:delay_seconds]).to be_between(-1, 1) # Allow for timing/rounding
91
+ end
92
+
93
+ adapter.enqueue_at(job, current_timestamp)
94
+ end
95
+ end
96
+
97
+ describe 'retry_on with zero wait' do
98
+ it 'allows immediate retries through continuation mechanism' do
99
+ # Simulate a job with retry_on configuration that uses zero wait
100
+ past_timestamp = Time.current.to_f - 1
101
+
102
+ expect(queue).to receive(:send_message) do |hash|
103
+ # Negative delay for past timestamp - SQS will handle immediate delivery
104
+ expect(hash[:delay_seconds]).to be <= 0
105
+ end
106
+
107
+ adapter.enqueue_at(job, past_timestamp)
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,298 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Shoryuken::Helpers::TimerTask do
6
+ let(:execution_interval) { 0.1 }
7
+ let!(:timer_task) do
8
+ described_class.new(execution_interval: execution_interval) do
9
+ @execution_count = (@execution_count || 0) + 1
10
+ end
11
+ end
12
+
13
+ describe '#initialize' do
14
+ it 'creates a timer task with the specified interval' do
15
+ timer = described_class.new(execution_interval: 5) {}
16
+ expect(timer).to be_a(described_class)
17
+ end
18
+
19
+ it 'requires a block' do
20
+ expect { described_class.new(execution_interval: 5) }.to raise_error(ArgumentError, 'A block must be provided')
21
+ end
22
+
23
+ it 'requires a positive execution_interval' do
24
+ expect { described_class.new(execution_interval: 0) {} }.to raise_error(ArgumentError, 'execution_interval must be positive')
25
+ expect { described_class.new(execution_interval: -1) {} }.to raise_error(ArgumentError, 'execution_interval must be positive')
26
+ end
27
+
28
+ it 'accepts string numbers as execution_interval' do
29
+ timer = described_class.new(execution_interval: '5.5') {}
30
+ expect(timer.instance_variable_get(:@execution_interval)).to eq(5.5)
31
+ end
32
+
33
+ it 'raises ArgumentError for non-numeric execution_interval' do
34
+ expect { described_class.new(execution_interval: 'invalid') {} }.to raise_error(ArgumentError)
35
+ expect { described_class.new(execution_interval: nil) {} }.to raise_error(TypeError)
36
+ expect { described_class.new(execution_interval: {}) {} }.to raise_error(TypeError)
37
+ end
38
+
39
+ it 'stores the task block in @task instance variable' do
40
+ task_proc = proc { puts 'test' }
41
+ timer = described_class.new(execution_interval: 1, &task_proc)
42
+ expect(timer.instance_variable_get(:@task)).to eq(task_proc)
43
+ end
44
+
45
+ it 'stores the execution interval' do
46
+ timer = described_class.new(execution_interval: 5) {}
47
+ expect(timer.instance_variable_get(:@execution_interval)).to eq(5)
48
+ end
49
+
50
+ it 'initializes state variables correctly' do
51
+ timer = described_class.new(execution_interval: 1) {}
52
+ expect(timer.instance_variable_get(:@running)).to be false
53
+ expect(timer.instance_variable_get(:@killed)).to be false
54
+ expect(timer.instance_variable_get(:@thread)).to be_nil
55
+ end
56
+ end
57
+
58
+ describe '#execute' do
59
+ it 'returns self for method chaining' do
60
+ result = timer_task.execute
61
+ expect(result).to eq(timer_task)
62
+ timer_task.kill
63
+ end
64
+
65
+ it 'sets @running to true when executed' do
66
+ timer_task.execute
67
+ expect(timer_task.instance_variable_get(:@running)).to be true
68
+ timer_task.kill
69
+ end
70
+
71
+ it 'creates a new thread' do
72
+ timer_task.execute
73
+ thread = timer_task.instance_variable_get(:@thread)
74
+ expect(thread).to be_a(Thread)
75
+ timer_task.kill
76
+ end
77
+
78
+ it 'does not start multiple times' do
79
+ timer_task.execute
80
+ first_thread = timer_task.instance_variable_get(:@thread)
81
+ timer_task.execute
82
+ second_thread = timer_task.instance_variable_get(:@thread)
83
+ expect(first_thread).to eq(second_thread)
84
+ timer_task.kill
85
+ end
86
+
87
+ it 'does not execute if already killed' do
88
+ timer_task.instance_variable_set(:@killed, true)
89
+ result = timer_task.execute
90
+ expect(result).to eq(timer_task)
91
+ expect(timer_task.instance_variable_get(:@thread)).to be_nil
92
+ end
93
+ end
94
+
95
+ describe '#kill' do
96
+ it 'returns true when successfully killed' do
97
+ timer_task.execute
98
+ expect(timer_task.kill).to be true
99
+ end
100
+
101
+ it 'returns false when already killed' do
102
+ timer_task.execute
103
+ timer_task.kill
104
+ expect(timer_task.kill).to be false
105
+ end
106
+
107
+ it 'sets @killed to true' do
108
+ timer_task.execute
109
+ timer_task.kill
110
+ expect(timer_task.instance_variable_get(:@killed)).to be true
111
+ end
112
+
113
+ it 'sets @running to false' do
114
+ timer_task.execute
115
+ timer_task.kill
116
+ expect(timer_task.instance_variable_get(:@running)).to be false
117
+ end
118
+
119
+ it 'kills the thread if alive' do
120
+ timer_task.execute
121
+ thread = timer_task.instance_variable_get(:@thread)
122
+ timer_task.kill
123
+ sleep(0.01) # Give time for thread to be killed
124
+ expect(thread.alive?).to be false
125
+ end
126
+
127
+ it 'is safe to call multiple times' do
128
+ timer_task.execute
129
+ expect { timer_task.kill }.not_to raise_error
130
+ expect { timer_task.kill }.not_to raise_error
131
+ end
132
+
133
+ it 'handles case when thread is nil' do
134
+ timer = described_class.new(execution_interval: 1) {}
135
+ result = nil
136
+ expect { result = timer.kill }.not_to raise_error
137
+ expect(result).to be true
138
+ end
139
+ end
140
+
141
+ describe 'execution behavior' do
142
+ it 'executes the task at the specified interval' do
143
+ execution_count = 0
144
+ timer = described_class.new(execution_interval: 0.05) do
145
+ execution_count += 1
146
+ end
147
+
148
+ timer.execute
149
+ sleep(0.15) # Should allow for ~3 executions
150
+ timer.kill
151
+
152
+ expect(execution_count).to be >= 2
153
+ expect(execution_count).to be <= 4 # Allow some timing variance
154
+ end
155
+
156
+ it 'calls the task block correctly' do
157
+ task_called = false
158
+ timer = described_class.new(execution_interval: 0.05) do
159
+ task_called = true
160
+ end
161
+
162
+ timer.execute
163
+ sleep(0.1)
164
+ timer.kill
165
+
166
+ expect(task_called).to be true
167
+ end
168
+
169
+ it 'handles exceptions in the task gracefully' do
170
+ error_count = 0
171
+ timer = described_class.new(execution_interval: 0.05) do
172
+ error_count += 1
173
+ raise StandardError, 'Test error'
174
+ end
175
+
176
+ # Capture stderr to check for error messages
177
+ original_stderr = $stderr
178
+ captured_stderr = StringIO.new
179
+ $stderr = captured_stderr
180
+
181
+ # Mock warn method to prevent warning gem from raising exceptions
182
+ # but still capture the output
183
+ allow_any_instance_of(Object).to receive(:warn) do |*args|
184
+ captured_stderr.puts(*args)
185
+ end
186
+
187
+ timer.execute
188
+ sleep(0.15)
189
+ timer.kill
190
+
191
+ error_output = captured_stderr.string
192
+ $stderr = original_stderr
193
+
194
+ expect(error_count).to be >= 2
195
+ expect(error_output).to include('Test error')
196
+ end
197
+
198
+ it 'continues execution after exceptions' do
199
+ execution_count = 0
200
+ timer = described_class.new(execution_interval: 0.05) do
201
+ execution_count += 1
202
+ raise StandardError, 'Test error' if execution_count == 1
203
+ end
204
+
205
+ # Mock warn method to prevent warning gem from raising exceptions
206
+ allow_any_instance_of(Object).to receive(:warn)
207
+
208
+ timer.execute
209
+ sleep(0.15)
210
+ timer.kill
211
+
212
+ expect(execution_count).to be >= 2 # Should continue after first error
213
+ end
214
+
215
+ it 'stops execution when killed' do
216
+ execution_count = 0
217
+ timer = described_class.new(execution_interval: 0.05) do
218
+ execution_count += 1
219
+ end
220
+
221
+ timer.execute
222
+ sleep(0.1)
223
+ initial_count = execution_count
224
+ timer.kill
225
+ sleep(0.1)
226
+ final_count = execution_count
227
+
228
+ expect(final_count).to eq(initial_count)
229
+ end
230
+
231
+ it 'respects the execution interval' do
232
+ execution_times = []
233
+ timer = described_class.new(execution_interval: 0.1) do
234
+ execution_times << Time.now
235
+ end
236
+
237
+ timer.execute
238
+ sleep(0.35) # Allow for ~3 executions
239
+ timer.kill
240
+
241
+ expect(execution_times.length).to be >= 2
242
+ if execution_times.length >= 2
243
+ interval = execution_times[1] - execution_times[0]
244
+ expect(interval).to be_within(0.05).of(0.1)
245
+ end
246
+ end
247
+ end
248
+
249
+ describe 'thread safety' do
250
+ it 'can be safely accessed from multiple threads' do
251
+ timer = described_class.new(execution_interval: 0.1) {}
252
+
253
+ threads = 10.times.map do
254
+ Thread.new do
255
+ timer.execute
256
+ sleep(0.01)
257
+ timer.kill
258
+ end
259
+ end
260
+
261
+ threads.each(&:join)
262
+ # Timer should be stopped after all threads complete
263
+ expect(timer.instance_variable_get(:@killed)).to be true
264
+ end
265
+
266
+ it 'handles concurrent execute calls safely' do
267
+ timer = described_class.new(execution_interval: 0.1) {}
268
+
269
+ threads = 5.times.map do
270
+ Thread.new { timer.execute }
271
+ end
272
+
273
+ threads.each(&:join)
274
+
275
+ # Should only have one thread created
276
+ expect(timer.instance_variable_get(:@thread)).to be_a(Thread)
277
+ timer.kill
278
+ end
279
+
280
+ it 'handles concurrent kill calls safely' do
281
+ timer = described_class.new(execution_interval: 0.1) {}
282
+ timer.execute
283
+
284
+ threads = 5.times.map do
285
+ Thread.new { timer.kill }
286
+ end
287
+
288
+ results = threads.map(&:value)
289
+
290
+ # Only one kill should return true, others should return false
291
+ true_count = results.count(true)
292
+ false_count = results.count(false)
293
+
294
+ expect(true_count).to eq(1)
295
+ expect(false_count).to eq(4)
296
+ end
297
+ end
298
+ end
@@ -101,4 +101,26 @@ RSpec.describe Shoryuken::Launcher do
101
101
  expect(second_group_manager).to have_received(:stop_new_dispatching)
102
102
  end
103
103
  end
104
+
105
+ describe '#stopping?' do
106
+ it 'returns false by default' do
107
+ expect(subject.stopping?).to be false
108
+ end
109
+
110
+ it 'returns true after stop is called' do
111
+ allow(first_group_manager).to receive(:stop_new_dispatching)
112
+ allow(first_group_manager).to receive(:await_dispatching_in_progress)
113
+ allow(second_group_manager).to receive(:stop_new_dispatching)
114
+ allow(second_group_manager).to receive(:await_dispatching_in_progress)
115
+
116
+ expect { subject.stop }.to change { subject.stopping? }.from(false).to(true)
117
+ end
118
+
119
+ it 'returns true after stop! is called' do
120
+ allow(first_group_manager).to receive(:stop_new_dispatching)
121
+ allow(second_group_manager).to receive(:stop_new_dispatching)
122
+
123
+ expect { subject.stop! }.to change { subject.stopping? }.from(false).to(true)
124
+ end
125
+ end
104
126
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shoryuken
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.0.0.alpha2
4
+ version: 7.0.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pablo Cantero
@@ -152,10 +152,9 @@ files:
152
152
  - examples/bootstrap_queues.rb
153
153
  - examples/default_worker.rb
154
154
  - gemfiles/.gitignore
155
- - gemfiles/rails_7_0.gemfile
156
- - gemfiles/rails_7_1.gemfile
157
155
  - gemfiles/rails_7_2.gemfile
158
156
  - gemfiles/rails_8_0.gemfile
157
+ - gemfiles/rails_8_1.gemfile
159
158
  - lib/shoryuken.rb
160
159
  - lib/shoryuken/body_parser.rb
161
160
  - lib/shoryuken/client.rb
@@ -171,12 +170,17 @@ files:
171
170
  - lib/shoryuken/helpers/atomic_hash.rb
172
171
  - lib/shoryuken/helpers/hash_utils.rb
173
172
  - lib/shoryuken/helpers/string_utils.rb
173
+ - lib/shoryuken/helpers/timer_task.rb
174
174
  - lib/shoryuken/inline_message.rb
175
175
  - lib/shoryuken/launcher.rb
176
176
  - lib/shoryuken/logging.rb
177
+ - lib/shoryuken/logging/base.rb
178
+ - lib/shoryuken/logging/pretty.rb
179
+ - lib/shoryuken/logging/without_timestamp.rb
177
180
  - lib/shoryuken/manager.rb
178
181
  - lib/shoryuken/message.rb
179
182
  - lib/shoryuken/middleware/chain.rb
183
+ - lib/shoryuken/middleware/entry.rb
180
184
  - lib/shoryuken/middleware/server/active_record.rb
181
185
  - lib/shoryuken/middleware/server/auto_delete.rb
182
186
  - lib/shoryuken/middleware/server/auto_extend_visibility.rb
@@ -198,6 +202,7 @@ files:
198
202
  - lib/shoryuken/worker_registry.rb
199
203
  - renovate.json
200
204
  - shoryuken.gemspec
205
+ - spec/integration/active_job_continuation_spec.rb
201
206
  - spec/integration/launcher_spec.rb
202
207
  - spec/shared_examples_for_active_job.rb
203
208
  - spec/shoryuken.yml
@@ -209,6 +214,7 @@ files:
209
214
  - spec/shoryuken/extensions/active_job_adapter_spec.rb
210
215
  - spec/shoryuken/extensions/active_job_base_spec.rb
211
216
  - spec/shoryuken/extensions/active_job_concurrent_send_adapter_spec.rb
217
+ - spec/shoryuken/extensions/active_job_continuation_spec.rb
212
218
  - spec/shoryuken/extensions/active_job_wrapper_spec.rb
213
219
  - spec/shoryuken/fetcher_spec.rb
214
220
  - spec/shoryuken/helpers/atomic_boolean_spec.rb
@@ -216,6 +222,7 @@ files:
216
222
  - spec/shoryuken/helpers/atomic_hash_spec.rb
217
223
  - spec/shoryuken/helpers/hash_utils_spec.rb
218
224
  - spec/shoryuken/helpers/string_utils_spec.rb
225
+ - spec/shoryuken/helpers/timer_task_spec.rb
219
226
  - spec/shoryuken/helpers_integration_spec.rb
220
227
  - spec/shoryuken/inline_message_spec.rb
221
228
  - spec/shoryuken/launcher_spec.rb
@@ -263,6 +270,7 @@ rubygems_version: 3.6.9
263
270
  specification_version: 4
264
271
  summary: Shoryuken is a super efficient AWS SQS thread based message processor
265
272
  test_files:
273
+ - spec/integration/active_job_continuation_spec.rb
266
274
  - spec/integration/launcher_spec.rb
267
275
  - spec/shared_examples_for_active_job.rb
268
276
  - spec/shoryuken.yml
@@ -274,6 +282,7 @@ test_files:
274
282
  - spec/shoryuken/extensions/active_job_adapter_spec.rb
275
283
  - spec/shoryuken/extensions/active_job_base_spec.rb
276
284
  - spec/shoryuken/extensions/active_job_concurrent_send_adapter_spec.rb
285
+ - spec/shoryuken/extensions/active_job_continuation_spec.rb
277
286
  - spec/shoryuken/extensions/active_job_wrapper_spec.rb
278
287
  - spec/shoryuken/fetcher_spec.rb
279
288
  - spec/shoryuken/helpers/atomic_boolean_spec.rb
@@ -281,6 +290,7 @@ test_files:
281
290
  - spec/shoryuken/helpers/atomic_hash_spec.rb
282
291
  - spec/shoryuken/helpers/hash_utils_spec.rb
283
292
  - spec/shoryuken/helpers/string_utils_spec.rb
293
+ - spec/shoryuken/helpers/timer_task_spec.rb
284
294
  - spec/shoryuken/helpers_integration_spec.rb
285
295
  - spec/shoryuken/inline_message_spec.rb
286
296
  - spec/shoryuken/launcher_spec.rb
@@ -1,19 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source 'https://rubygems.org'
4
-
5
- group :test do
6
- gem 'activejob', '~> 7.0'
7
- gem 'httparty'
8
- gem 'multi_xml'
9
- gem 'simplecov'
10
- gem 'warning'
11
- end
12
-
13
- group :development do
14
- gem 'appraisal', git: 'https://github.com/thoughtbot/appraisal.git'
15
- gem 'pry-byebug'
16
- gem 'rubocop'
17
- end
18
-
19
- gemspec path: '../'
@@ -1,19 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source 'https://rubygems.org'
4
-
5
- group :test do
6
- gem 'activejob', '~> 7.1'
7
- gem 'httparty'
8
- gem 'multi_xml'
9
- gem 'simplecov'
10
- gem 'warning'
11
- end
12
-
13
- group :development do
14
- gem 'appraisal', git: 'https://github.com/thoughtbot/appraisal.git'
15
- gem 'pry-byebug'
16
- gem 'rubocop'
17
- end
18
-
19
- gemspec path: '../'