shoryuken 7.0.0.alpha2 → 7.0.0

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 (178) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/push.yml +2 -2
  3. data/.github/workflows/specs.yml +38 -43
  4. data/.github/workflows/verify-action-pins.yml +1 -1
  5. data/.gitignore +3 -0
  6. data/.rspec +1 -0
  7. data/.ruby-version +1 -1
  8. data/.yard-lint.yml +279 -0
  9. data/CHANGELOG.md +69 -1
  10. data/Gemfile +1 -1
  11. data/README.md +2 -7
  12. data/Rakefile +4 -10
  13. data/bin/clean_localstack +52 -0
  14. data/bin/cli/base.rb +21 -0
  15. data/bin/cli/sqs.rb +61 -2
  16. data/bin/integrations +275 -0
  17. data/bin/scenario +154 -0
  18. data/bin/shoryuken +1 -1
  19. data/lib/{shoryuken/extensions/active_job_extensions.rb → active_job/extensions.rb} +15 -4
  20. data/lib/active_job/queue_adapters/shoryuken_adapter.rb +208 -0
  21. data/lib/active_job/queue_adapters/shoryuken_concurrent_send_adapter.rb +78 -0
  22. data/lib/shoryuken/active_job/current_attributes.rb +139 -0
  23. data/lib/shoryuken/active_job/job_wrapper.rb +28 -0
  24. data/lib/shoryuken/body_parser.rb +8 -0
  25. data/lib/shoryuken/client.rb +14 -0
  26. data/lib/shoryuken/default_exception_handler.rb +9 -0
  27. data/lib/shoryuken/default_worker_registry.rb +29 -1
  28. data/lib/shoryuken/environment_loader.rb +78 -8
  29. data/lib/shoryuken/errors.rb +33 -0
  30. data/lib/shoryuken/fetcher.rb +37 -1
  31. data/lib/shoryuken/helpers/atomic_boolean.rb +19 -5
  32. data/lib/shoryuken/helpers/timer_task.rb +80 -0
  33. data/lib/shoryuken/launcher.rb +53 -0
  34. data/lib/shoryuken/logging/base.rb +26 -0
  35. data/lib/shoryuken/logging/pretty.rb +25 -0
  36. data/lib/shoryuken/logging/without_timestamp.rb +25 -0
  37. data/lib/shoryuken/logging.rb +39 -25
  38. data/lib/shoryuken/manager.rb +70 -1
  39. data/lib/shoryuken/message.rb +114 -1
  40. data/lib/shoryuken/middleware/chain.rb +139 -43
  41. data/lib/shoryuken/middleware/entry.rb +30 -0
  42. data/lib/shoryuken/middleware/server/active_record.rb +8 -0
  43. data/lib/shoryuken/middleware/server/auto_delete.rb +10 -0
  44. data/lib/shoryuken/middleware/server/auto_extend_visibility.rb +27 -1
  45. data/lib/shoryuken/middleware/server/exponential_backoff_retry.rb +29 -0
  46. data/lib/shoryuken/middleware/server/timing.rb +11 -0
  47. data/lib/shoryuken/options.rb +129 -6
  48. data/lib/shoryuken/polling/base_strategy.rb +1 -0
  49. data/lib/shoryuken/polling/strict_priority.rb +39 -0
  50. data/lib/shoryuken/polling/weighted_round_robin.rb +42 -0
  51. data/lib/shoryuken/processor.rb +32 -1
  52. data/lib/shoryuken/queue.rb +93 -4
  53. data/lib/shoryuken/runner.rb +45 -4
  54. data/lib/shoryuken/util.rb +26 -1
  55. data/lib/shoryuken/version.rb +2 -1
  56. data/lib/shoryuken/worker/default_executor.rb +21 -1
  57. data/lib/shoryuken/worker/inline_executor.rb +24 -0
  58. data/lib/shoryuken/worker.rb +193 -0
  59. data/lib/shoryuken/worker_registry.rb +33 -0
  60. data/lib/shoryuken.rb +18 -6
  61. data/renovate.json +29 -2
  62. data/shoryuken.gemspec +2 -1
  63. data/spec/integration/.rspec +1 -0
  64. data/spec/integration/active_job/adapter_configuration/configuration_spec.rb +26 -0
  65. data/spec/integration/active_job/bulk_enqueue/bulk_enqueue_spec.rb +53 -0
  66. data/spec/integration/active_job/current_attributes/bulk_enqueue_spec.rb +50 -0
  67. data/spec/integration/active_job/current_attributes/complex_types_spec.rb +55 -0
  68. data/spec/integration/active_job/current_attributes/empty_context_spec.rb +41 -0
  69. data/spec/integration/active_job/current_attributes/full_context_spec.rb +63 -0
  70. data/spec/integration/active_job/current_attributes/partial_context_spec.rb +57 -0
  71. data/spec/integration/active_job/custom_attributes/number_attributes_spec.rb +37 -0
  72. data/spec/integration/active_job/custom_attributes/string_attributes_spec.rb +39 -0
  73. data/spec/integration/active_job/error_handling/job_wrapper_spec.rb +53 -0
  74. data/spec/integration/active_job/fifo_and_attributes/deduplication_spec.rb +86 -0
  75. data/spec/integration/active_job/retry/discard_on_spec.rb +43 -0
  76. data/spec/integration/active_job/retry/retry_on_spec.rb +36 -0
  77. data/spec/integration/active_job/roundtrip/roundtrip_spec.rb +52 -0
  78. data/spec/integration/active_job/scheduled/scheduled_spec.rb +76 -0
  79. data/spec/integration/active_record_middleware/active_record_middleware_spec.rb +84 -0
  80. data/spec/integration/auto_delete/auto_delete_spec.rb +53 -0
  81. data/spec/integration/auto_extend_visibility/auto_extend_visibility_spec.rb +57 -0
  82. data/spec/integration/aws_config/aws_config_spec.rb +59 -0
  83. data/spec/integration/batch_processing/batch_processing_spec.rb +37 -0
  84. data/spec/integration/body_parser/json_parser_spec.rb +45 -0
  85. data/spec/integration/body_parser/proc_parser_spec.rb +54 -0
  86. data/spec/integration/body_parser/text_parser_spec.rb +43 -0
  87. data/spec/integration/concurrent_processing/concurrent_processing_spec.rb +45 -0
  88. data/spec/integration/dead_letter_queue/dead_letter_queue_spec.rb +91 -0
  89. data/spec/integration/exception_handlers/exception_handlers_spec.rb +69 -0
  90. data/spec/integration/exponential_backoff/exponential_backoff_spec.rb +67 -0
  91. data/spec/integration/fifo_ordering/fifo_ordering_spec.rb +44 -0
  92. data/spec/integration/large_payloads/large_payloads_spec.rb +30 -0
  93. data/spec/integration/launcher/launcher_spec.rb +40 -0
  94. data/spec/integration/message_attributes/message_attributes_spec.rb +54 -0
  95. data/spec/integration/message_operations/message_operations_spec.rb +59 -0
  96. data/spec/integration/middleware_chain/empty_chain_spec.rb +11 -0
  97. data/spec/integration/middleware_chain/execution_order_spec.rb +33 -0
  98. data/spec/integration/middleware_chain/removal_spec.rb +31 -0
  99. data/spec/integration/middleware_chain/short_circuit_spec.rb +40 -0
  100. data/spec/integration/polling_strategies/polling_strategies_spec.rb +46 -0
  101. data/spec/integration/queue_operations/queue_operations_spec.rb +84 -0
  102. data/spec/integration/rails/rails_72/Gemfile +6 -0
  103. data/spec/integration/rails/rails_72/activejob_adapter_spec.rb +98 -0
  104. data/spec/integration/rails/rails_80/Gemfile +6 -0
  105. data/spec/integration/rails/rails_80/activejob_adapter_spec.rb +98 -0
  106. data/spec/integration/rails/rails_80/continuation_spec.rb +79 -0
  107. data/spec/integration/rails/rails_81/Gemfile +6 -0
  108. data/spec/integration/rails/rails_81/activejob_adapter_spec.rb +98 -0
  109. data/spec/integration/rails/rails_81/continuation_spec.rb +79 -0
  110. data/spec/integration/retry_behavior/retry_behavior_spec.rb +45 -0
  111. data/spec/integration/spec_helper.rb +7 -0
  112. data/spec/integration/strict_priority_polling/strict_priority_polling_spec.rb +58 -0
  113. data/spec/integration/visibility_timeout/visibility_timeout_spec.rb +37 -0
  114. data/spec/integration/worker_enqueueing/worker_enqueueing_spec.rb +60 -0
  115. data/spec/integration/worker_groups/worker_groups_spec.rb +79 -0
  116. data/spec/integration/worker_lifecycle/worker_lifecycle_spec.rb +33 -0
  117. data/spec/integrations_helper.rb +243 -0
  118. data/spec/lib/active_job/extensions_spec.rb +149 -0
  119. data/spec/lib/active_job/queue_adapters/shoryuken_adapter_spec.rb +29 -0
  120. data/spec/{shoryuken/extensions/active_job_concurrent_send_adapter_spec.rb → lib/active_job/queue_adapters/shoryuken_concurrent_send_adapter_spec.rb} +3 -3
  121. data/spec/{shoryuken/extensions/active_job_wrapper_spec.rb → lib/shoryuken/active_job/job_wrapper_spec.rb} +4 -4
  122. data/spec/{shoryuken → lib/shoryuken}/environment_loader_spec.rb +1 -1
  123. data/spec/{shoryuken → lib/shoryuken}/helpers/hash_utils_spec.rb +14 -14
  124. data/spec/{shoryuken → lib/shoryuken}/helpers/string_utils_spec.rb +3 -3
  125. data/spec/lib/shoryuken/helpers/timer_task_spec.rb +298 -0
  126. data/spec/{shoryuken → lib/shoryuken}/helpers_integration_spec.rb +9 -9
  127. data/spec/{shoryuken → lib/shoryuken}/launcher_spec.rb +22 -0
  128. data/spec/lib/shoryuken/logging_spec.rb +242 -0
  129. data/spec/lib/shoryuken/message_spec.rb +109 -0
  130. data/spec/lib/shoryuken/middleware/entry_spec.rb +68 -0
  131. data/spec/lib/shoryuken/middleware/server/active_record_spec.rb +133 -0
  132. data/spec/{shoryuken → lib/shoryuken}/middleware/server/auto_extend_visibility_spec.rb +50 -0
  133. data/spec/{shoryuken → lib/shoryuken}/options_spec.rb +2 -2
  134. data/spec/{shoryuken → lib/shoryuken}/util_spec.rb +1 -1
  135. data/spec/lib/shoryuken/version_spec.rb +17 -0
  136. data/spec/lib/shoryuken/worker_registry_spec.rb +63 -0
  137. data/spec/shared_examples_for_active_job.rb +29 -9
  138. data/spec/spec_helper.rb +34 -3
  139. metadata +230 -91
  140. data/.devcontainer/Dockerfile +0 -17
  141. data/.devcontainer/base.Dockerfile +0 -43
  142. data/.devcontainer/devcontainer.json +0 -35
  143. data/Appraisals +0 -23
  144. data/gemfiles/.gitignore +0 -1
  145. data/gemfiles/rails_7_0.gemfile +0 -19
  146. data/gemfiles/rails_7_1.gemfile +0 -19
  147. data/gemfiles/rails_7_2.gemfile +0 -19
  148. data/gemfiles/rails_8_0.gemfile +0 -19
  149. data/lib/shoryuken/extensions/active_job_adapter.rb +0 -110
  150. data/lib/shoryuken/extensions/active_job_concurrent_send_adapter.rb +0 -50
  151. data/spec/integration/launcher_spec.rb +0 -127
  152. data/spec/shoryuken/extensions/active_job_adapter_spec.rb +0 -8
  153. data/spec/shoryuken/extensions/active_job_base_spec.rb +0 -85
  154. /data/spec/{shoryuken → lib/shoryuken}/body_parser_spec.rb +0 -0
  155. /data/spec/{shoryuken → lib/shoryuken}/client_spec.rb +0 -0
  156. /data/spec/{shoryuken → lib/shoryuken}/default_exception_handler_spec.rb +0 -0
  157. /data/spec/{shoryuken → lib/shoryuken}/default_worker_registry_spec.rb +0 -0
  158. /data/spec/{shoryuken → lib/shoryuken}/fetcher_spec.rb +0 -0
  159. /data/spec/{shoryuken → lib/shoryuken}/helpers/atomic_boolean_spec.rb +0 -0
  160. /data/spec/{shoryuken → lib/shoryuken}/helpers/atomic_counter_spec.rb +0 -0
  161. /data/spec/{shoryuken → lib/shoryuken}/helpers/atomic_hash_spec.rb +0 -0
  162. /data/spec/{shoryuken → lib/shoryuken}/inline_message_spec.rb +0 -0
  163. /data/spec/{shoryuken → lib/shoryuken}/manager_spec.rb +0 -0
  164. /data/spec/{shoryuken → lib/shoryuken}/middleware/chain_spec.rb +0 -0
  165. /data/spec/{shoryuken → lib/shoryuken}/middleware/server/auto_delete_spec.rb +0 -0
  166. /data/spec/{shoryuken → lib/shoryuken}/middleware/server/exponential_backoff_retry_spec.rb +0 -0
  167. /data/spec/{shoryuken → lib/shoryuken}/middleware/server/timing_spec.rb +0 -0
  168. /data/spec/{shoryuken → lib/shoryuken}/polling/base_strategy_spec.rb +0 -0
  169. /data/spec/{shoryuken → lib/shoryuken}/polling/queue_configuration_spec.rb +0 -0
  170. /data/spec/{shoryuken → lib/shoryuken}/polling/strict_priority_spec.rb +0 -0
  171. /data/spec/{shoryuken → lib/shoryuken}/polling/weighted_round_robin_spec.rb +0 -0
  172. /data/spec/{shoryuken → lib/shoryuken}/processor_spec.rb +0 -0
  173. /data/spec/{shoryuken → lib/shoryuken}/queue_spec.rb +0 -0
  174. /data/spec/{shoryuken → lib/shoryuken}/runner_spec.rb +0 -0
  175. /data/spec/{shoryuken → lib/shoryuken}/worker/default_executor_spec.rb +0 -0
  176. /data/spec/{shoryuken → lib/shoryuken}/worker/inline_executor_spec.rb +0 -0
  177. /data/spec/{shoryuken → lib/shoryuken}/worker_spec.rb +0 -0
  178. /data/spec/{shoryuken_spec.rb → lib/shoryuken_spec.rb} +0 -0
@@ -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
@@ -2,7 +2,7 @@
2
2
 
3
3
  RSpec.describe 'Helpers Integration' do
4
4
  # Integration tests for helper utility methods that replaced core extensions
5
-
5
+
6
6
  describe Shoryuken::Helpers::HashUtils do
7
7
  describe '.deep_symbolize_keys' do
8
8
  it 'converts keys into symbols recursively' do
@@ -12,12 +12,12 @@ RSpec.describe 'Helpers Integration' do
12
12
  'key31' => { 'key311' => 'value311' },
13
13
  'key32' => 'value32'
14
14
  } }
15
-
15
+
16
16
  expected = { key1: 'value1',
17
17
  key2: 'value2',
18
18
  key3: { key31: { key311: 'value311' },
19
19
  key32: 'value32' } }
20
-
20
+
21
21
  expect(Shoryuken::Helpers::HashUtils.deep_symbolize_keys(input)).to eq(expected)
22
22
  end
23
23
 
@@ -34,7 +34,7 @@ RSpec.describe 'Helpers Integration' do
34
34
  it 'handles mixed value types' do
35
35
  input = { 'key1' => 'string', 'key2' => 123, 'key3' => { 'nested' => true } }
36
36
  expected = { key1: 'string', key2: 123, key3: { nested: true } }
37
-
37
+
38
38
  expect(Shoryuken::Helpers::HashUtils.deep_symbolize_keys(input)).to eq(expected)
39
39
  end
40
40
  end
@@ -43,7 +43,7 @@ RSpec.describe 'Helpers Integration' do
43
43
  describe Shoryuken::Helpers::StringUtils do
44
44
  describe '.constantize' do
45
45
  class HelloWorld; end
46
-
46
+
47
47
  it 'returns a class from a string' do
48
48
  expect(Shoryuken::Helpers::StringUtils.constantize('HelloWorld')).to eq(HelloWorld)
49
49
  end
@@ -75,20 +75,20 @@ RSpec.describe 'Helpers Integration' do
75
75
  'mailers' => { 'worker_class' => 'String' }
76
76
  }
77
77
  }
78
-
78
+
79
79
  symbolized = Shoryuken::Helpers::HashUtils.deep_symbolize_keys(config_data)
80
-
80
+
81
81
  expect(symbolized).to eq({
82
82
  queues: {
83
83
  default: { worker_class: 'Object' },
84
84
  mailers: { worker_class: 'String' }
85
85
  }
86
86
  })
87
-
87
+
88
88
  # Test constantizing the worker classes
89
89
  default_worker = Shoryuken::Helpers::StringUtils.constantize(symbolized[:queues][:default][:worker_class])
90
90
  mailer_worker = Shoryuken::Helpers::StringUtils.constantize(symbolized[:queues][:mailers][:worker_class])
91
-
91
+
92
92
  expect(default_worker).to eq(Object)
93
93
  expect(mailer_worker).to eq(String)
94
94
  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
@@ -0,0 +1,242 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Shoryuken::Logging do
6
+ describe Shoryuken::Logging::Base do
7
+ let(:formatter) { described_class.new }
8
+
9
+ describe '#tid' do
10
+ it 'returns a string representing the thread ID' do
11
+ expect(formatter.tid).to be_a(String)
12
+ end
13
+
14
+ it 'returns the same value for the same thread' do
15
+ tid1 = formatter.tid
16
+ tid2 = formatter.tid
17
+ expect(tid1).to eq(tid2)
18
+ end
19
+
20
+ it 'caches the thread ID in thread-local storage' do
21
+ tid = formatter.tid
22
+ expect(Thread.current['shoryuken_tid']).to eq(tid)
23
+ end
24
+ end
25
+
26
+ describe '#context' do
27
+ after do
28
+ Shoryuken::Logging.context_storage[:shoryuken_context] = nil
29
+ end
30
+
31
+ it 'returns empty string when no context is set' do
32
+ Shoryuken::Logging.context_storage[:shoryuken_context] = nil
33
+ expect(formatter.context).to eq('')
34
+ end
35
+
36
+ it 'returns formatted context when context is set' do
37
+ Shoryuken::Logging.context_storage[:shoryuken_context] = 'test_context'
38
+ expect(formatter.context).to eq(' test_context')
39
+ end
40
+ end
41
+ end
42
+
43
+ describe Shoryuken::Logging::Pretty do
44
+ let(:formatter) { described_class.new }
45
+ let(:time) { Time.new(2023, 8, 15, 10, 30, 45, '+00:00') }
46
+
47
+ describe '#call' do
48
+ after do
49
+ Shoryuken::Logging.context_storage[:shoryuken_context] = nil
50
+ end
51
+
52
+ it 'formats log messages with timestamp' do
53
+ allow(formatter).to receive(:tid).and_return('abc123')
54
+ Shoryuken::Logging.context_storage[:shoryuken_context] = nil
55
+
56
+ result = formatter.call('INFO', time, 'program', 'test message')
57
+ expect(result).to eq("2023-08-15T10:30:45Z #{Process.pid} TID-abc123 INFO: test message\n")
58
+ end
59
+
60
+ it 'includes context when present' do
61
+ allow(formatter).to receive(:tid).and_return('abc123')
62
+ Shoryuken::Logging.context_storage[:shoryuken_context] = 'worker-1'
63
+
64
+ result = formatter.call('ERROR', time, 'program', 'error message')
65
+ expect(result).to eq("2023-08-15T10:30:45Z #{Process.pid} TID-abc123 worker-1 ERROR: error message\n")
66
+ end
67
+ end
68
+ end
69
+
70
+ describe Shoryuken::Logging::WithoutTimestamp do
71
+ let(:formatter) { described_class.new }
72
+
73
+ describe '#call' do
74
+ after do
75
+ Shoryuken::Logging.context_storage[:shoryuken_context] = nil
76
+ end
77
+
78
+ it 'formats log messages without timestamp' do
79
+ allow(formatter).to receive(:tid).and_return('xyz789')
80
+ Shoryuken::Logging.context_storage[:shoryuken_context] = nil
81
+
82
+ result = formatter.call('DEBUG', Time.now, 'program', 'debug message')
83
+ expect(result).to eq("pid=#{Process.pid} tid=xyz789 DEBUG: debug message\n")
84
+ end
85
+
86
+ it 'includes context when present' do
87
+ allow(formatter).to receive(:tid).and_return('xyz789')
88
+ Shoryuken::Logging.context_storage[:shoryuken_context] = 'queue-processor'
89
+
90
+ result = formatter.call('WARN', Time.now, 'program', 'warning message')
91
+ expect(result).to eq("pid=#{Process.pid} tid=xyz789 queue-processor WARN: warning message\n")
92
+ end
93
+ end
94
+ end
95
+
96
+ describe '.with_context' do
97
+ after do
98
+ described_class.context_storage[:shoryuken_context] = nil
99
+ end
100
+
101
+ it 'sets context for the duration of the block' do
102
+ described_class.with_context('test_context') do
103
+ expect(described_class.current_context).to eq('test_context')
104
+ end
105
+ end
106
+
107
+ it 'clears context after the block completes' do
108
+ described_class.with_context('test_context') do
109
+ # context is set
110
+ end
111
+ expect(described_class.current_context).to be_nil
112
+ end
113
+
114
+ it 'clears context even when an exception is raised' do
115
+ expect do
116
+ described_class.with_context('test_context') do
117
+ raise StandardError, 'test error'
118
+ end
119
+ end.to raise_error(StandardError, 'test error')
120
+
121
+ expect(described_class.current_context).to be_nil
122
+ end
123
+
124
+ it 'returns the value of the block' do
125
+ result = described_class.with_context('test_context') do
126
+ 'block_result'
127
+ end
128
+ expect(result).to eq('block_result')
129
+ end
130
+
131
+ it 'preserves outer context in nested calls' do
132
+ described_class.with_context('outer') do
133
+ expect(described_class.current_context).to eq('outer')
134
+
135
+ described_class.with_context('inner') do
136
+ expect(described_class.current_context).to eq('inner')
137
+ end
138
+
139
+ expect(described_class.current_context).to eq('outer')
140
+ end
141
+ expect(described_class.current_context).to be_nil
142
+ end
143
+
144
+ it 'restores outer context even when inner block raises' do
145
+ described_class.with_context('outer') do
146
+ expect do
147
+ described_class.with_context('inner') do
148
+ raise StandardError, 'inner error'
149
+ end
150
+ end.to raise_error(StandardError, 'inner error')
151
+
152
+ expect(described_class.current_context).to eq('outer')
153
+ end
154
+ end
155
+ end
156
+
157
+ describe '.current_context' do
158
+ after do
159
+ described_class.context_storage[:shoryuken_context] = nil
160
+ end
161
+
162
+ it 'returns nil when no context is set' do
163
+ expect(described_class.current_context).to be_nil
164
+ end
165
+
166
+ it 'returns the current context value' do
167
+ described_class.context_storage[:shoryuken_context] = 'test_value'
168
+ expect(described_class.current_context).to eq('test_value')
169
+ end
170
+ end
171
+
172
+ describe '.context_storage' do
173
+ it 'returns Fiber for fiber-local storage' do
174
+ expect(described_class.context_storage).to eq(Fiber)
175
+ end
176
+ end
177
+
178
+ describe '.initialize_logger' do
179
+ it 'creates a new Logger instance' do
180
+ logger = described_class.initialize_logger
181
+ expect(logger).to be_a(Logger)
182
+ end
183
+
184
+ it 'sets default log level to INFO' do
185
+ logger = described_class.initialize_logger
186
+ expect(logger.level).to eq(Logger::INFO)
187
+ end
188
+
189
+ it 'uses Pretty formatter by default' do
190
+ logger = described_class.initialize_logger
191
+ expect(logger.formatter).to be_a(Shoryuken::Logging::Pretty)
192
+ end
193
+
194
+ it 'accepts custom log target' do
195
+ log_target = StringIO.new
196
+ logger = described_class.initialize_logger(log_target)
197
+ expect(logger.instance_variable_get(:@logdev).dev).to eq(log_target)
198
+ end
199
+ end
200
+
201
+ describe '.logger' do
202
+ after do
203
+ # Reset the instance variable to avoid affecting other tests
204
+ described_class.instance_variable_set(:@logger, nil)
205
+ end
206
+
207
+ it 'returns a logger instance' do
208
+ expect(described_class.logger).to be_a(Logger)
209
+ end
210
+
211
+ it 'memoizes the logger instance' do
212
+ logger1 = described_class.logger
213
+ logger2 = described_class.logger
214
+ expect(logger1).to be(logger2)
215
+ end
216
+
217
+ it 'initializes logger if not already set' do
218
+ expect(described_class).to receive(:initialize_logger).and_call_original
219
+ described_class.logger
220
+ end
221
+ end
222
+
223
+ describe '.logger=' do
224
+ after do
225
+ # Reset the instance variable to avoid affecting other tests
226
+ described_class.instance_variable_set(:@logger, nil)
227
+ end
228
+
229
+ it 'sets the logger instance' do
230
+ custom_logger = Logger.new('/dev/null')
231
+ described_class.logger = custom_logger
232
+ expect(described_class.logger).to be(custom_logger)
233
+ end
234
+
235
+ it 'sets null logger when passed nil' do
236
+ described_class.logger = nil
237
+ logger = described_class.logger
238
+ # The logger should be configured to output to /dev/null
239
+ expect(logger).to be_a(Logger)
240
+ end
241
+ end
242
+ end