shoryuken 7.0.0.alpha1 → 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.
- checksums.yaml +4 -4
- data/.github/workflows/push.yml +3 -3
- data/.github/workflows/specs.yml +27 -17
- data/.github/workflows/verify-action-pins.yml +1 -1
- data/.rspec +2 -1
- data/.ruby-version +1 -1
- data/Appraisals +6 -18
- data/CHANGELOG.md +200 -142
- data/Gemfile +1 -0
- data/README.md +12 -13
- data/bin/cli/base.rb +1 -2
- data/bin/cli/sqs.rb +6 -5
- data/bin/shoryuken +3 -2
- data/gemfiles/rails_7_2.gemfile +1 -0
- data/gemfiles/rails_8_0.gemfile +1 -0
- data/gemfiles/{rails_7_1.gemfile → rails_8_1.gemfile} +2 -1
- data/lib/shoryuken/body_parser.rb +3 -1
- data/lib/shoryuken/client.rb +2 -0
- data/lib/shoryuken/default_exception_handler.rb +2 -0
- data/lib/shoryuken/default_worker_registry.rb +11 -11
- data/lib/shoryuken/environment_loader.rb +6 -6
- data/lib/shoryuken/extensions/active_job_adapter.rb +21 -6
- data/lib/shoryuken/extensions/active_job_concurrent_send_adapter.rb +5 -5
- data/lib/shoryuken/extensions/active_job_extensions.rb +2 -0
- data/lib/shoryuken/fetcher.rb +4 -2
- data/lib/shoryuken/helpers/atomic_boolean.rb +44 -0
- data/lib/shoryuken/helpers/atomic_counter.rb +104 -0
- data/lib/shoryuken/helpers/atomic_hash.rb +182 -0
- data/lib/shoryuken/helpers/hash_utils.rb +56 -0
- data/lib/shoryuken/helpers/string_utils.rb +65 -0
- data/lib/shoryuken/helpers/timer_task.rb +66 -0
- data/lib/shoryuken/inline_message.rb +22 -0
- data/lib/shoryuken/launcher.rb +16 -0
- data/lib/shoryuken/logging/base.rb +26 -0
- data/lib/shoryuken/logging/pretty.rb +25 -0
- data/lib/shoryuken/logging/without_timestamp.rb +25 -0
- data/lib/shoryuken/logging.rb +6 -12
- data/lib/shoryuken/manager.rb +6 -4
- data/lib/shoryuken/message.rb +116 -1
- data/lib/shoryuken/middleware/chain.rb +140 -43
- data/lib/shoryuken/middleware/entry.rb +30 -0
- data/lib/shoryuken/middleware/server/active_record.rb +2 -0
- data/lib/shoryuken/middleware/server/auto_delete.rb +2 -0
- data/lib/shoryuken/middleware/server/auto_extend_visibility.rb +11 -11
- data/lib/shoryuken/middleware/server/exponential_backoff_retry.rb +5 -3
- data/lib/shoryuken/middleware/server/timing.rb +2 -0
- data/lib/shoryuken/options.rb +9 -5
- data/lib/shoryuken/polling/base_strategy.rb +126 -0
- data/lib/shoryuken/polling/queue_configuration.rb +103 -0
- data/lib/shoryuken/polling/strict_priority.rb +2 -0
- data/lib/shoryuken/polling/weighted_round_robin.rb +2 -0
- data/lib/shoryuken/processor.rb +5 -2
- data/lib/shoryuken/queue.rb +6 -4
- data/lib/shoryuken/runner.rb +12 -12
- data/lib/shoryuken/util.rb +6 -6
- data/lib/shoryuken/version.rb +3 -1
- data/lib/shoryuken/worker/default_executor.rb +2 -0
- data/lib/shoryuken/worker/inline_executor.rb +3 -1
- data/lib/shoryuken/worker.rb +173 -0
- data/lib/shoryuken/worker_registry.rb +2 -0
- data/lib/shoryuken.rb +8 -28
- data/shoryuken.gemspec +6 -6
- data/spec/integration/active_job_continuation_spec.rb +145 -0
- data/spec/integration/launcher_spec.rb +2 -3
- data/spec/shared_examples_for_active_job.rb +13 -8
- data/spec/shoryuken/body_parser_spec.rb +1 -2
- data/spec/shoryuken/client_spec.rb +1 -1
- data/spec/shoryuken/default_exception_handler_spec.rb +9 -10
- data/spec/shoryuken/default_worker_registry_spec.rb +1 -2
- data/spec/shoryuken/environment_loader_spec.rb +9 -8
- data/spec/shoryuken/extensions/active_job_adapter_spec.rb +2 -1
- data/spec/shoryuken/extensions/active_job_base_spec.rb +2 -1
- data/spec/shoryuken/extensions/active_job_concurrent_send_adapter_spec.rb +2 -1
- data/spec/shoryuken/extensions/active_job_continuation_spec.rb +110 -0
- data/spec/shoryuken/extensions/active_job_wrapper_spec.rb +2 -1
- data/spec/shoryuken/fetcher_spec.rb +23 -26
- data/spec/shoryuken/helpers/atomic_boolean_spec.rb +196 -0
- data/spec/shoryuken/helpers/atomic_counter_spec.rb +177 -0
- data/spec/shoryuken/helpers/atomic_hash_spec.rb +307 -0
- data/spec/shoryuken/helpers/hash_utils_spec.rb +145 -0
- data/spec/shoryuken/helpers/string_utils_spec.rb +124 -0
- data/spec/shoryuken/helpers/timer_task_spec.rb +298 -0
- data/spec/shoryuken/helpers_integration_spec.rb +96 -0
- data/spec/shoryuken/inline_message_spec.rb +196 -0
- data/spec/shoryuken/launcher_spec.rb +23 -2
- data/spec/shoryuken/manager_spec.rb +1 -2
- data/spec/shoryuken/middleware/chain_spec.rb +1 -1
- data/spec/shoryuken/middleware/server/auto_delete_spec.rb +1 -1
- data/spec/shoryuken/middleware/server/auto_extend_visibility_spec.rb +1 -1
- data/spec/shoryuken/middleware/server/exponential_backoff_retry_spec.rb +1 -1
- data/spec/shoryuken/middleware/server/timing_spec.rb +1 -1
- data/spec/shoryuken/options_spec.rb +4 -4
- data/spec/shoryuken/polling/base_strategy_spec.rb +280 -0
- data/spec/shoryuken/polling/queue_configuration_spec.rb +195 -0
- data/spec/shoryuken/polling/strict_priority_spec.rb +1 -1
- data/spec/shoryuken/polling/weighted_round_robin_spec.rb +1 -1
- data/spec/shoryuken/processor_spec.rb +1 -1
- data/spec/shoryuken/queue_spec.rb +2 -3
- data/spec/shoryuken/runner_spec.rb +1 -3
- data/spec/shoryuken/util_spec.rb +1 -1
- data/spec/shoryuken/worker/default_executor_spec.rb +1 -1
- data/spec/shoryuken/worker/inline_executor_spec.rb +1 -1
- data/spec/shoryuken/worker_spec.rb +15 -11
- data/spec/shoryuken_spec.rb +1 -1
- data/spec/spec_helper.rb +16 -0
- metadata +72 -29
- data/.github/FUNDING.yml +0 -12
- data/gemfiles/rails_6_1.gemfile +0 -18
- data/gemfiles/rails_7_0.gemfile +0 -19
- data/lib/shoryuken/core_ext.rb +0 -69
- data/lib/shoryuken/polling/base.rb +0 -67
- data/shoryuken.jpg +0 -0
- data/spec/shoryuken/core_ext_spec.rb +0 -40
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Shoryuken::Helpers::HashUtils do
|
|
6
|
+
describe '.deep_symbolize_keys' do
|
|
7
|
+
it 'converts string keys to symbols' do
|
|
8
|
+
input = { 'key1' => 'value1', 'key2' => 'value2' }
|
|
9
|
+
expected = { key1: 'value1', key2: 'value2' }
|
|
10
|
+
|
|
11
|
+
expect(described_class.deep_symbolize_keys(input)).to eq(expected)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it 'leaves symbol keys unchanged' do
|
|
15
|
+
input = { key1: 'value1', key2: 'value2' }
|
|
16
|
+
expected = { key1: 'value1', key2: 'value2' }
|
|
17
|
+
|
|
18
|
+
expect(described_class.deep_symbolize_keys(input)).to eq(expected)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'handles mixed key types' do
|
|
22
|
+
input = { 'string_key' => 'value1', :symbol_key => 'value2' }
|
|
23
|
+
expected = { string_key: 'value1', symbol_key: 'value2' }
|
|
24
|
+
|
|
25
|
+
expect(described_class.deep_symbolize_keys(input)).to eq(expected)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'converts keys recursively in nested hashes' do
|
|
29
|
+
input = {
|
|
30
|
+
'level1' => {
|
|
31
|
+
'level2' => {
|
|
32
|
+
'level3' => 'deep_value'
|
|
33
|
+
},
|
|
34
|
+
'other_level2' => 'value'
|
|
35
|
+
},
|
|
36
|
+
'top_level' => 'value'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
expected = {
|
|
40
|
+
level1: {
|
|
41
|
+
level2: {
|
|
42
|
+
level3: 'deep_value'
|
|
43
|
+
},
|
|
44
|
+
other_level2: 'value'
|
|
45
|
+
},
|
|
46
|
+
top_level: 'value'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
expect(described_class.deep_symbolize_keys(input)).to eq(expected)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'preserves non-hash values in nested structures' do
|
|
53
|
+
input = {
|
|
54
|
+
'config' => {
|
|
55
|
+
'timeout' => 30,
|
|
56
|
+
'enabled' => true,
|
|
57
|
+
'tags' => ['tag1', 'tag2'],
|
|
58
|
+
'metadata' => nil
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
expected = {
|
|
63
|
+
config: {
|
|
64
|
+
timeout: 30,
|
|
65
|
+
enabled: true,
|
|
66
|
+
tags: ['tag1', 'tag2'],
|
|
67
|
+
metadata: nil
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
expect(described_class.deep_symbolize_keys(input)).to eq(expected)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it 'handles empty hash' do
|
|
75
|
+
expect(described_class.deep_symbolize_keys({})).to eq({})
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'handles hash with empty nested hash' do
|
|
79
|
+
input = { 'key' => {} }
|
|
80
|
+
expected = { key: {} }
|
|
81
|
+
|
|
82
|
+
expect(described_class.deep_symbolize_keys(input)).to eq(expected)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it 'returns non-hash input unchanged' do
|
|
86
|
+
expect(described_class.deep_symbolize_keys('string')).to eq('string')
|
|
87
|
+
expect(described_class.deep_symbolize_keys(123)).to eq(123)
|
|
88
|
+
expect(described_class.deep_symbolize_keys([])).to eq([])
|
|
89
|
+
expect(described_class.deep_symbolize_keys(nil)).to be_nil
|
|
90
|
+
expect(described_class.deep_symbolize_keys(true)).to be true
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'handles keys that cannot be converted to symbols' do
|
|
94
|
+
# Create a key that will raise an exception when converted to symbol
|
|
95
|
+
problematic_key = Object.new
|
|
96
|
+
allow(problematic_key).to receive(:to_sym).and_raise(StandardError)
|
|
97
|
+
|
|
98
|
+
input = { problematic_key => 'value', 'normal_key' => 'normal_value' }
|
|
99
|
+
result = described_class.deep_symbolize_keys(input)
|
|
100
|
+
|
|
101
|
+
# The problematic key should remain as-is, normal key should be symbolized
|
|
102
|
+
expect(result[problematic_key]).to eq('value')
|
|
103
|
+
expect(result[:normal_key]).to eq('normal_value')
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it 'does not modify the original hash' do
|
|
107
|
+
input = { 'key' => { 'nested' => 'value' } }
|
|
108
|
+
original_input = input.dup
|
|
109
|
+
|
|
110
|
+
described_class.deep_symbolize_keys(input)
|
|
111
|
+
|
|
112
|
+
expect(input).to eq(original_input)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
context 'with configuration-like data' do
|
|
116
|
+
it 'processes typical YAML configuration' do
|
|
117
|
+
input = {
|
|
118
|
+
'database' => {
|
|
119
|
+
'host' => 'localhost',
|
|
120
|
+
'port' => 5432,
|
|
121
|
+
'ssl' => true
|
|
122
|
+
},
|
|
123
|
+
'queues' => {
|
|
124
|
+
'default' => { 'concurrency' => 5 },
|
|
125
|
+
'mailers' => { 'concurrency' => 2 }
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
expected = {
|
|
130
|
+
database: {
|
|
131
|
+
host: 'localhost',
|
|
132
|
+
port: 5432,
|
|
133
|
+
ssl: true
|
|
134
|
+
},
|
|
135
|
+
queues: {
|
|
136
|
+
default: { concurrency: 5 },
|
|
137
|
+
mailers: { concurrency: 2 }
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
expect(described_class.deep_symbolize_keys(input)).to eq(expected)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Shoryuken::Helpers::StringUtils do
|
|
6
|
+
describe '.constantize' do
|
|
7
|
+
it 'returns a simple constant' do
|
|
8
|
+
expect(described_class.constantize('Object')).to eq(Object)
|
|
9
|
+
expect(described_class.constantize('String')).to eq(String)
|
|
10
|
+
expect(described_class.constantize('Hash')).to eq(Hash)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'returns nested constants' do
|
|
14
|
+
expect(described_class.constantize('Shoryuken::Helpers::StringUtils')).to eq(Shoryuken::Helpers::StringUtils)
|
|
15
|
+
expect(described_class.constantize('Shoryuken::Helpers::AtomicCounter')).to eq(Shoryuken::Helpers::AtomicCounter)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'handles leading double colon' do
|
|
19
|
+
expect(described_class.constantize('::Object')).to eq(Object)
|
|
20
|
+
expect(described_class.constantize('::String')).to eq(String)
|
|
21
|
+
expect(described_class.constantize('::Shoryuken::Helpers::StringUtils')).to eq(Shoryuken::Helpers::StringUtils)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'handles empty string' do
|
|
25
|
+
expect(described_class.constantize('')).to eq(Object)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'handles string with only double colons' do
|
|
29
|
+
expect(described_class.constantize('::')).to eq(Object)
|
|
30
|
+
expect(described_class.constantize('::::')).to eq(Object)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'raises NameError for non-existent constants' do
|
|
34
|
+
expect { described_class.constantize('NonExistentClass') }.to raise_error(NameError)
|
|
35
|
+
expect { described_class.constantize('Shoryuken::NonExistentClass') }.to raise_error(NameError)
|
|
36
|
+
expect { described_class.constantize('NonExistent::AlsoNonExistent') }.to raise_error(NameError)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'raises NameError for partially invalid nested constants' do
|
|
40
|
+
expect { described_class.constantize('Shoryuken::NonExistent::AlsoNonExistent') }.to raise_error(NameError)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
context 'with dynamically defined constants' do
|
|
44
|
+
before do
|
|
45
|
+
# Define a temporary constant for testing
|
|
46
|
+
Object.const_set('TempTestClass', Class.new)
|
|
47
|
+
TempTestClass.const_set('NestedClass', Class.new)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
after do
|
|
51
|
+
# Clean up the temporary constant
|
|
52
|
+
Object.send(:remove_const, 'TempTestClass') if Object.const_defined?('TempTestClass')
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it 'finds dynamically defined constants' do
|
|
56
|
+
expect(described_class.constantize('TempTestClass')).to eq(TempTestClass)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'finds nested dynamically defined constants' do
|
|
60
|
+
expect(described_class.constantize('TempTestClass::NestedClass')).to eq(TempTestClass::NestedClass)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
context 'with module constants' do
|
|
65
|
+
it 'returns module constants' do
|
|
66
|
+
expect(described_class.constantize('Shoryuken')).to eq(Shoryuken)
|
|
67
|
+
expect(described_class.constantize('Shoryuken::Helpers')).to eq(Shoryuken::Helpers)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
context 'with worker class scenarios' do
|
|
72
|
+
before do
|
|
73
|
+
# Simulate typical worker class scenarios
|
|
74
|
+
unless Object.const_defined?('MyApp')
|
|
75
|
+
Object.const_set('MyApp', Module.new)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
unless MyApp.const_defined?('EmailWorker')
|
|
79
|
+
MyApp.const_set('EmailWorker', Class.new)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
after do
|
|
84
|
+
# Clean up
|
|
85
|
+
MyApp.send(:remove_const, 'EmailWorker') if MyApp.const_defined?('EmailWorker')
|
|
86
|
+
Object.send(:remove_const, 'MyApp') if Object.const_defined?('MyApp')
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it 'loads worker classes from string names' do
|
|
90
|
+
worker_class = described_class.constantize('MyApp::EmailWorker')
|
|
91
|
+
expect(worker_class).to eq(MyApp::EmailWorker)
|
|
92
|
+
expect(worker_class.new).to be_an_instance_of(MyApp::EmailWorker)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
context 'edge cases' do
|
|
97
|
+
it 'handles constants with numbers' do
|
|
98
|
+
# Using existing constants that might have numbers
|
|
99
|
+
expect(described_class.constantize('Encoding::UTF_8')).to eq(Encoding::UTF_8)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it 'is case sensitive' do
|
|
103
|
+
expect { described_class.constantize('object') }.to raise_error(NameError)
|
|
104
|
+
expect { described_class.constantize('STRING') }.to raise_error(NameError)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it 'handles single character constant names' do
|
|
108
|
+
# Define a single character constant for testing
|
|
109
|
+
Object.const_set('A', Class.new) unless Object.const_defined?('A')
|
|
110
|
+
|
|
111
|
+
expect(described_class.constantize('A')).to eq(A)
|
|
112
|
+
|
|
113
|
+
# Clean up
|
|
114
|
+
Object.send(:remove_const, 'A') if Object.const_defined?('A')
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
context 'error messages' do
|
|
119
|
+
it 'provides meaningful error messages' do
|
|
120
|
+
expect { described_class.constantize('NonExistent') }.to raise_error(NameError, /NonExistent/)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
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
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe 'Helpers Integration' do
|
|
4
|
+
# Integration tests for helper utility methods that replaced core extensions
|
|
5
|
+
|
|
6
|
+
describe Shoryuken::Helpers::HashUtils do
|
|
7
|
+
describe '.deep_symbolize_keys' do
|
|
8
|
+
it 'converts keys into symbols recursively' do
|
|
9
|
+
input = { :key1 => 'value1',
|
|
10
|
+
'key2' => 'value2',
|
|
11
|
+
'key3' => {
|
|
12
|
+
'key31' => { 'key311' => 'value311' },
|
|
13
|
+
'key32' => 'value32'
|
|
14
|
+
} }
|
|
15
|
+
|
|
16
|
+
expected = { key1: 'value1',
|
|
17
|
+
key2: 'value2',
|
|
18
|
+
key3: { key31: { key311: 'value311' },
|
|
19
|
+
key32: 'value32' } }
|
|
20
|
+
|
|
21
|
+
expect(Shoryuken::Helpers::HashUtils.deep_symbolize_keys(input)).to eq(expected)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'handles non-hash input gracefully' do
|
|
25
|
+
expect(Shoryuken::Helpers::HashUtils.deep_symbolize_keys('string')).to eq('string')
|
|
26
|
+
expect(Shoryuken::Helpers::HashUtils.deep_symbolize_keys(123)).to eq(123)
|
|
27
|
+
expect(Shoryuken::Helpers::HashUtils.deep_symbolize_keys(nil)).to be_nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'handles empty hash' do
|
|
31
|
+
expect(Shoryuken::Helpers::HashUtils.deep_symbolize_keys({})).to eq({})
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'handles mixed value types' do
|
|
35
|
+
input = { 'key1' => 'string', 'key2' => 123, 'key3' => { 'nested' => true } }
|
|
36
|
+
expected = { key1: 'string', key2: 123, key3: { nested: true } }
|
|
37
|
+
|
|
38
|
+
expect(Shoryuken::Helpers::HashUtils.deep_symbolize_keys(input)).to eq(expected)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
describe Shoryuken::Helpers::StringUtils do
|
|
44
|
+
describe '.constantize' do
|
|
45
|
+
class HelloWorld; end
|
|
46
|
+
|
|
47
|
+
it 'returns a class from a string' do
|
|
48
|
+
expect(Shoryuken::Helpers::StringUtils.constantize('HelloWorld')).to eq(HelloWorld)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'handles nested constants' do
|
|
52
|
+
expect(Shoryuken::Helpers::StringUtils.constantize('Shoryuken::Helpers::StringUtils')).to eq(Shoryuken::Helpers::StringUtils)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it 'raises NameError for non-existent constants' do
|
|
56
|
+
expect { Shoryuken::Helpers::StringUtils.constantize('NonExistentClass') }.to raise_error(NameError)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'handles empty string' do
|
|
60
|
+
expect(Shoryuken::Helpers::StringUtils.constantize('')).to eq(Object)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'handles leading double colon' do
|
|
64
|
+
expect(Shoryuken::Helpers::StringUtils.constantize('::Object')).to eq(Object)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
describe 'Integration scenarios' do
|
|
70
|
+
it 'processes configuration data end-to-end' do
|
|
71
|
+
# Simulate loading YAML config and converting worker class names
|
|
72
|
+
config_data = {
|
|
73
|
+
'queues' => {
|
|
74
|
+
'default' => { 'worker_class' => 'Object' },
|
|
75
|
+
'mailers' => { 'worker_class' => 'String' }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
symbolized = Shoryuken::Helpers::HashUtils.deep_symbolize_keys(config_data)
|
|
80
|
+
|
|
81
|
+
expect(symbolized).to eq({
|
|
82
|
+
queues: {
|
|
83
|
+
default: { worker_class: 'Object' },
|
|
84
|
+
mailers: { worker_class: 'String' }
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
# Test constantizing the worker classes
|
|
89
|
+
default_worker = Shoryuken::Helpers::StringUtils.constantize(symbolized[:queues][:default][:worker_class])
|
|
90
|
+
mailer_worker = Shoryuken::Helpers::StringUtils.constantize(symbolized[:queues][:mailers][:worker_class])
|
|
91
|
+
|
|
92
|
+
expect(default_worker).to eq(Object)
|
|
93
|
+
expect(mailer_worker).to eq(String)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|