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,196 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'shoryuken/helpers/atomic_boolean'
|
|
3
|
+
|
|
4
|
+
RSpec.describe Shoryuken::Helpers::AtomicBoolean do
|
|
5
|
+
subject { described_class.new }
|
|
6
|
+
|
|
7
|
+
describe '#initialize' do
|
|
8
|
+
it 'initializes with default value of false' do
|
|
9
|
+
boolean = described_class.new
|
|
10
|
+
expect(boolean.value).to eq(false)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'initializes with true value' do
|
|
14
|
+
boolean = described_class.new(true)
|
|
15
|
+
expect(boolean.value).to eq(true)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'initializes with false value' do
|
|
19
|
+
boolean = described_class.new(false)
|
|
20
|
+
expect(boolean.value).to eq(false)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'converts truthy values to true' do
|
|
24
|
+
boolean = described_class.new('truthy')
|
|
25
|
+
expect(boolean.value).to eq(true)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'converts falsy values to false' do
|
|
29
|
+
boolean = described_class.new(nil)
|
|
30
|
+
expect(boolean.value).to eq(false)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
describe '#value' do
|
|
35
|
+
it 'returns the current value' do
|
|
36
|
+
expect(subject.value).to eq(false)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'returns the updated value after operations' do
|
|
40
|
+
subject.make_true
|
|
41
|
+
expect(subject.value).to eq(true)
|
|
42
|
+
|
|
43
|
+
subject.make_false
|
|
44
|
+
expect(subject.value).to eq(false)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
describe '#make_true' do
|
|
49
|
+
it 'sets the value to true' do
|
|
50
|
+
expect { subject.make_true }.to change { subject.value }.from(false).to(true)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'returns true' do
|
|
54
|
+
result = subject.make_true
|
|
55
|
+
expect(result).to eq(true)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'keeps the value true if already true' do
|
|
59
|
+
subject.make_true
|
|
60
|
+
expect { subject.make_true }.not_to change { subject.value }
|
|
61
|
+
expect(subject.value).to eq(true)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
describe '#make_false' do
|
|
66
|
+
it 'sets the value to false' do
|
|
67
|
+
subject.make_true # Start with true
|
|
68
|
+
expect { subject.make_false }.to change { subject.value }.from(true).to(false)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it 'returns false' do
|
|
72
|
+
result = subject.make_false
|
|
73
|
+
expect(result).to eq(false)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it 'keeps the value false if already false' do
|
|
77
|
+
expect { subject.make_false }.not_to change { subject.value }
|
|
78
|
+
expect(subject.value).to eq(false)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
describe '#true?' do
|
|
83
|
+
it 'returns true when value is true' do
|
|
84
|
+
subject.make_true
|
|
85
|
+
expect(subject.true?).to eq(true)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'returns false when value is false' do
|
|
89
|
+
expect(subject.true?).to eq(false)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
describe '#false?' do
|
|
94
|
+
it 'returns true when value is false' do
|
|
95
|
+
expect(subject.false?).to eq(true)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it 'returns false when value is true' do
|
|
99
|
+
subject.make_true
|
|
100
|
+
expect(subject.false?).to eq(false)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
describe 'thread safety' do
|
|
105
|
+
it 'handles concurrent make_true operations correctly' do
|
|
106
|
+
boolean = described_class.new(false)
|
|
107
|
+
threads = []
|
|
108
|
+
|
|
109
|
+
10.times do
|
|
110
|
+
threads << Thread.new do
|
|
111
|
+
100.times { boolean.make_true }
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
threads.each(&:join)
|
|
116
|
+
expect(boolean.value).to eq(true)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it 'handles concurrent make_false operations correctly' do
|
|
120
|
+
boolean = described_class.new(true)
|
|
121
|
+
threads = []
|
|
122
|
+
|
|
123
|
+
10.times do
|
|
124
|
+
threads << Thread.new do
|
|
125
|
+
100.times { boolean.make_false }
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
threads.each(&:join)
|
|
130
|
+
expect(boolean.value).to eq(false)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it 'handles mixed concurrent operations correctly' do
|
|
134
|
+
boolean = described_class.new(false)
|
|
135
|
+
threads = []
|
|
136
|
+
results = []
|
|
137
|
+
|
|
138
|
+
# Multiple threads setting to true and false
|
|
139
|
+
10.times do
|
|
140
|
+
threads << Thread.new do
|
|
141
|
+
50.times do
|
|
142
|
+
boolean.make_true
|
|
143
|
+
boolean.make_false
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Reader threads
|
|
149
|
+
5.times do
|
|
150
|
+
threads << Thread.new do
|
|
151
|
+
100.times { results << boolean.value }
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
threads.each(&:join)
|
|
156
|
+
|
|
157
|
+
# All read values should be valid booleans
|
|
158
|
+
expect(results).to all(satisfy { |v| v == true || v == false })
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
describe 'drop-in replacement for Concurrent::AtomicBoolean' do
|
|
163
|
+
it 'provides the same basic API' do
|
|
164
|
+
# Test that our implementation has the same methods as Concurrent::AtomicBoolean
|
|
165
|
+
expect(subject).to respond_to(:value)
|
|
166
|
+
expect(subject).to respond_to(:make_true)
|
|
167
|
+
expect(subject).to respond_to(:make_false)
|
|
168
|
+
expect(subject).to respond_to(:true?)
|
|
169
|
+
expect(subject).to respond_to(:false?)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
it 'does not expose counter-specific methods' do
|
|
173
|
+
expect(subject).not_to respond_to(:increment)
|
|
174
|
+
expect(subject).not_to respond_to(:decrement)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
it 'behaves identically to Concurrent::AtomicBoolean for basic operations' do
|
|
178
|
+
# This test documents the expected behavior that matches Concurrent::AtomicBoolean
|
|
179
|
+
boolean = described_class.new(false)
|
|
180
|
+
|
|
181
|
+
expect(boolean.value).to eq(false)
|
|
182
|
+
expect(boolean.false?).to eq(true)
|
|
183
|
+
expect(boolean.true?).to eq(false)
|
|
184
|
+
|
|
185
|
+
boolean.make_true
|
|
186
|
+
expect(boolean.value).to eq(true)
|
|
187
|
+
expect(boolean.true?).to eq(true)
|
|
188
|
+
expect(boolean.false?).to eq(false)
|
|
189
|
+
|
|
190
|
+
boolean.make_false
|
|
191
|
+
expect(boolean.value).to eq(false)
|
|
192
|
+
expect(boolean.false?).to eq(true)
|
|
193
|
+
expect(boolean.true?).to eq(false)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Shoryuken::Helpers::AtomicCounter do
|
|
4
|
+
subject { described_class.new }
|
|
5
|
+
|
|
6
|
+
describe '#initialize' do
|
|
7
|
+
it 'initializes with default value of 0' do
|
|
8
|
+
counter = described_class.new
|
|
9
|
+
expect(counter.value).to eq(0)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it 'initializes with custom value' do
|
|
13
|
+
counter = described_class.new(42)
|
|
14
|
+
expect(counter.value).to eq(42)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'initializes with negative value' do
|
|
18
|
+
counter = described_class.new(-10)
|
|
19
|
+
expect(counter.value).to eq(-10)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
describe '#value' do
|
|
24
|
+
it 'returns the current value' do
|
|
25
|
+
expect(subject.value).to eq(0)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'returns the updated value after operations' do
|
|
29
|
+
subject.increment
|
|
30
|
+
expect(subject.value).to eq(1)
|
|
31
|
+
|
|
32
|
+
subject.decrement
|
|
33
|
+
expect(subject.value).to eq(0)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
describe '#increment' do
|
|
38
|
+
it 'increments the counter by 1' do
|
|
39
|
+
expect { subject.increment }.to change { subject.value }.from(0).to(1)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'returns the new value' do
|
|
43
|
+
result = subject.increment
|
|
44
|
+
expect(result).to eq(1)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'can be called multiple times' do
|
|
48
|
+
3.times { subject.increment }
|
|
49
|
+
expect(subject.value).to eq(3)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'works with negative initial values' do
|
|
53
|
+
counter = described_class.new(-5)
|
|
54
|
+
counter.increment
|
|
55
|
+
expect(counter.value).to eq(-4)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
describe '#decrement' do
|
|
60
|
+
it 'decrements the counter by 1' do
|
|
61
|
+
subject.increment # Start at 1
|
|
62
|
+
expect { subject.decrement }.to change { subject.value }.from(1).to(0)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'returns the new value' do
|
|
66
|
+
subject.increment # Start at 1
|
|
67
|
+
result = subject.decrement
|
|
68
|
+
expect(result).to eq(0)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it 'can go negative' do
|
|
72
|
+
subject.decrement
|
|
73
|
+
expect(subject.value).to eq(-1)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it 'can be called multiple times' do
|
|
77
|
+
3.times { subject.decrement }
|
|
78
|
+
expect(subject.value).to eq(-3)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
describe 'thread safety' do
|
|
83
|
+
it 'handles concurrent increments correctly' do
|
|
84
|
+
counter = described_class.new
|
|
85
|
+
threads = []
|
|
86
|
+
|
|
87
|
+
10.times do
|
|
88
|
+
threads << Thread.new do
|
|
89
|
+
100.times { counter.increment }
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
threads.each(&:join)
|
|
94
|
+
expect(counter.value).to eq(1000)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it 'handles concurrent decrements correctly' do
|
|
98
|
+
counter = described_class.new(1000)
|
|
99
|
+
threads = []
|
|
100
|
+
|
|
101
|
+
10.times do
|
|
102
|
+
threads << Thread.new do
|
|
103
|
+
100.times { counter.decrement }
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
threads.each(&:join)
|
|
108
|
+
expect(counter.value).to eq(0)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it 'handles mixed concurrent operations correctly' do
|
|
112
|
+
counter = described_class.new
|
|
113
|
+
threads = []
|
|
114
|
+
|
|
115
|
+
# 5 threads incrementing
|
|
116
|
+
5.times do
|
|
117
|
+
threads << Thread.new do
|
|
118
|
+
100.times { counter.increment }
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# 3 threads decrementing
|
|
123
|
+
3.times do
|
|
124
|
+
threads << Thread.new do
|
|
125
|
+
100.times { counter.decrement }
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
threads.each(&:join)
|
|
130
|
+
expect(counter.value).to eq(200) # 500 increments - 300 decrements
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it 'provides atomic read operations' do
|
|
134
|
+
counter = described_class.new
|
|
135
|
+
values_read = []
|
|
136
|
+
|
|
137
|
+
# Writer thread
|
|
138
|
+
writer = Thread.new do
|
|
139
|
+
1000.times { counter.increment }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Reader threads
|
|
143
|
+
readers = 5.times.map do
|
|
144
|
+
Thread.new do
|
|
145
|
+
100.times { values_read << counter.value }
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
[writer, *readers].each(&:join)
|
|
150
|
+
|
|
151
|
+
# All read values should be valid integers (not partial writes)
|
|
152
|
+
expect(values_read).to all(be_an(Integer))
|
|
153
|
+
expect(values_read).to all(be >= 0)
|
|
154
|
+
expect(values_read).to all(be <= 1000)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
describe 'drop-in replacement for Concurrent::AtomicFixnum' do
|
|
159
|
+
it 'provides the same basic API' do
|
|
160
|
+
# Test that our implementation has the same methods as Concurrent::AtomicFixnum
|
|
161
|
+
expect(subject).to respond_to(:value)
|
|
162
|
+
expect(subject).to respond_to(:increment)
|
|
163
|
+
expect(subject).to respond_to(:decrement)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
it 'behaves identically to Concurrent::AtomicFixnum for basic operations' do
|
|
167
|
+
# This test documents the expected behavior that matches Concurrent::AtomicFixnum
|
|
168
|
+
counter = described_class.new(5)
|
|
169
|
+
|
|
170
|
+
expect(counter.value).to eq(5)
|
|
171
|
+
expect(counter.increment).to eq(6)
|
|
172
|
+
expect(counter.value).to eq(6)
|
|
173
|
+
expect(counter.decrement).to eq(5)
|
|
174
|
+
expect(counter.value).to eq(5)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Shoryuken::Helpers::AtomicHash do
|
|
4
|
+
subject { described_class.new }
|
|
5
|
+
|
|
6
|
+
describe '#initialize' do
|
|
7
|
+
it 'creates an empty hash' do
|
|
8
|
+
hash = described_class.new
|
|
9
|
+
expect(hash.keys).to eq([])
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
describe '#[]' do
|
|
14
|
+
it 'returns nil for missing keys' do
|
|
15
|
+
expect(subject['missing']).to be_nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'returns stored values' do
|
|
19
|
+
subject['key'] = 'value'
|
|
20
|
+
expect(subject['key']).to eq('value')
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'works with different key types' do
|
|
24
|
+
subject[:symbol] = 'symbol_value'
|
|
25
|
+
subject[42] = 'number_value'
|
|
26
|
+
|
|
27
|
+
expect(subject[:symbol]).to eq('symbol_value')
|
|
28
|
+
expect(subject[42]).to eq('number_value')
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe '#[]=' do
|
|
33
|
+
it 'stores values' do
|
|
34
|
+
subject['key'] = 'value'
|
|
35
|
+
expect(subject['key']).to eq('value')
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'overwrites existing values' do
|
|
39
|
+
subject['key'] = 'old_value'
|
|
40
|
+
subject['key'] = 'new_value'
|
|
41
|
+
expect(subject['key']).to eq('new_value')
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'returns the assigned value' do
|
|
45
|
+
result = (subject['key'] = 'value')
|
|
46
|
+
expect(result).to eq('value')
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
describe '#clear' do
|
|
51
|
+
it 'removes all entries' do
|
|
52
|
+
subject['key1'] = 'value1'
|
|
53
|
+
subject['key2'] = 'value2'
|
|
54
|
+
|
|
55
|
+
expect(subject.keys.size).to eq(2)
|
|
56
|
+
|
|
57
|
+
subject.clear
|
|
58
|
+
|
|
59
|
+
expect(subject.keys).to eq([])
|
|
60
|
+
expect(subject['key1']).to be_nil
|
|
61
|
+
expect(subject['key2']).to be_nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it 'returns the hash itself' do
|
|
65
|
+
subject['key'] = 'value'
|
|
66
|
+
result = subject.clear
|
|
67
|
+
expect(result).to eq({})
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
describe '#keys' do
|
|
72
|
+
it 'returns empty array for empty hash' do
|
|
73
|
+
expect(subject.keys).to eq([])
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it 'returns all keys' do
|
|
77
|
+
subject['key1'] = 'value1'
|
|
78
|
+
subject[:key2] = 'value2'
|
|
79
|
+
subject[42] = 'value3'
|
|
80
|
+
|
|
81
|
+
keys = subject.keys
|
|
82
|
+
expect(keys).to contain_exactly('key1', :key2, 42)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it 'reflects changes after modifications' do
|
|
86
|
+
subject['key'] = 'value'
|
|
87
|
+
expect(subject.keys).to eq(['key'])
|
|
88
|
+
|
|
89
|
+
subject.clear
|
|
90
|
+
expect(subject.keys).to eq([])
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
describe '#fetch' do
|
|
95
|
+
it 'returns value for existing key' do
|
|
96
|
+
subject['key'] = 'value'
|
|
97
|
+
expect(subject.fetch('key')).to eq('value')
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it 'returns default for missing key' do
|
|
101
|
+
expect(subject.fetch('missing', 'default')).to eq('default')
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it 'returns nil as default when no default provided' do
|
|
105
|
+
expect(subject.fetch('missing')).to be_nil
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it 'works with different default types' do
|
|
109
|
+
expect(subject.fetch('missing', [])).to eq([])
|
|
110
|
+
expect(subject.fetch('missing', {})).to eq({})
|
|
111
|
+
expect(subject.fetch('missing', 42)).to eq(42)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
describe 'thread safety' do
|
|
116
|
+
it 'handles concurrent writes correctly' do
|
|
117
|
+
hash = described_class.new
|
|
118
|
+
threads = []
|
|
119
|
+
|
|
120
|
+
# 10 threads, each writing 100 different keys
|
|
121
|
+
10.times do |thread_id|
|
|
122
|
+
threads << Thread.new do
|
|
123
|
+
100.times do |i|
|
|
124
|
+
key = "thread_#{thread_id}_key_#{i}"
|
|
125
|
+
hash[key] = "value_#{i}"
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
threads.each(&:join)
|
|
131
|
+
|
|
132
|
+
# Verify all 1000 keys were written
|
|
133
|
+
expect(hash.keys.size).to eq(1000)
|
|
134
|
+
|
|
135
|
+
# Verify a sample of values
|
|
136
|
+
expect(hash['thread_0_key_0']).to eq('value_0')
|
|
137
|
+
expect(hash['thread_9_key_99']).to eq('value_99')
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
it 'handles concurrent reads correctly' do
|
|
141
|
+
hash = described_class.new
|
|
142
|
+
|
|
143
|
+
# Pre-populate hash
|
|
144
|
+
100.times { |i| hash["key_#{i}"] = "value_#{i}" }
|
|
145
|
+
|
|
146
|
+
read_results = []
|
|
147
|
+
threads = []
|
|
148
|
+
|
|
149
|
+
# 10 threads, each reading 100 times
|
|
150
|
+
10.times do
|
|
151
|
+
threads << Thread.new do
|
|
152
|
+
100.times do |i|
|
|
153
|
+
key = "key_#{i % 100}"
|
|
154
|
+
read_results << hash[key]
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
threads.each(&:join)
|
|
160
|
+
|
|
161
|
+
# All reads should succeed
|
|
162
|
+
expect(read_results.size).to eq(1000)
|
|
163
|
+
expect(read_results.compact.size).to eq(1000)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
it 'handles mixed concurrent read/write operations' do
|
|
167
|
+
hash = described_class.new
|
|
168
|
+
threads = []
|
|
169
|
+
|
|
170
|
+
# Writer threads
|
|
171
|
+
5.times do |thread_id|
|
|
172
|
+
threads << Thread.new do
|
|
173
|
+
50.times do |i|
|
|
174
|
+
hash["writer_#{thread_id}_#{i}"] = "value_#{i}"
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Reader threads
|
|
180
|
+
5.times do
|
|
181
|
+
threads << Thread.new do
|
|
182
|
+
100.times do |i|
|
|
183
|
+
# Read existing keys
|
|
184
|
+
hash["writer_0_#{i % 50}"]
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Clear operations
|
|
190
|
+
2.times do
|
|
191
|
+
threads << Thread.new do
|
|
192
|
+
sleep(0.001) # Let some writes happen first
|
|
193
|
+
hash.clear
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
threads.each(&:join)
|
|
198
|
+
|
|
199
|
+
# The hash should be valid (not corrupted)
|
|
200
|
+
# After clear operations, it might be empty or have some keys
|
|
201
|
+
expect(hash.keys).to be_an(Array)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
it 'provides thread-safe key enumeration' do
|
|
205
|
+
hash = described_class.new
|
|
206
|
+
threads = []
|
|
207
|
+
keys_snapshots = []
|
|
208
|
+
|
|
209
|
+
# Writer thread
|
|
210
|
+
writer = Thread.new do
|
|
211
|
+
100.times { |i| hash["key_#{i}"] = i }
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Reader threads taking snapshots of keys
|
|
215
|
+
5.times do
|
|
216
|
+
threads << Thread.new do
|
|
217
|
+
20.times do
|
|
218
|
+
keys_snapshots << hash.keys.dup
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
[writer, *threads].each(&:join)
|
|
224
|
+
|
|
225
|
+
# All snapshots should be valid arrays
|
|
226
|
+
keys_snapshots.each do |snapshot|
|
|
227
|
+
expect(snapshot).to be_an(Array)
|
|
228
|
+
# Keys should be valid
|
|
229
|
+
snapshot.each { |key| expect(key).to be_a(String) }
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
describe 'mutex synchronization' do
|
|
235
|
+
it 'ensures all operations are atomic' do
|
|
236
|
+
hash = described_class.new
|
|
237
|
+
|
|
238
|
+
# Verify basic operations work correctly
|
|
239
|
+
hash['key'] = 'value'
|
|
240
|
+
expect(hash['key']).to eq('value')
|
|
241
|
+
expect(hash.keys).to eq(['key'])
|
|
242
|
+
|
|
243
|
+
hash.clear
|
|
244
|
+
expect(hash.keys).to eq([])
|
|
245
|
+
expect(hash['key']).to be_nil
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
describe 'drop-in replacement for Concurrent::Hash' do
|
|
250
|
+
it 'provides the same basic API' do
|
|
251
|
+
# Test that our implementation has the same methods as Concurrent::Hash
|
|
252
|
+
expect(subject).to respond_to(:[])
|
|
253
|
+
expect(subject).to respond_to(:[]=)
|
|
254
|
+
expect(subject).to respond_to(:clear)
|
|
255
|
+
expect(subject).to respond_to(:keys)
|
|
256
|
+
expect(subject).to respond_to(:fetch)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
it 'behaves identically to Concurrent::Hash for basic operations' do
|
|
260
|
+
# This test documents the expected behavior that matches Concurrent::Hash
|
|
261
|
+
hash = described_class.new
|
|
262
|
+
|
|
263
|
+
# Test assignment and retrieval
|
|
264
|
+
hash['queue1'] = 'Worker1'
|
|
265
|
+
expect(hash['queue1']).to eq('Worker1')
|
|
266
|
+
|
|
267
|
+
# Test keys
|
|
268
|
+
expect(hash.keys).to eq(['queue1'])
|
|
269
|
+
|
|
270
|
+
# Test fetch with default
|
|
271
|
+
expect(hash.fetch('queue1')).to eq('Worker1')
|
|
272
|
+
expect(hash.fetch('missing', 'default')).to eq('default')
|
|
273
|
+
|
|
274
|
+
# Test clear
|
|
275
|
+
hash.clear
|
|
276
|
+
expect(hash.keys).to eq([])
|
|
277
|
+
expect(hash['queue1']).to be_nil
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
it 'matches DefaultWorkerRegistry usage patterns' do
|
|
281
|
+
# Test the exact patterns used in DefaultWorkerRegistry
|
|
282
|
+
hash = described_class.new
|
|
283
|
+
|
|
284
|
+
# Pattern from register_worker method
|
|
285
|
+
queue = 'test_queue'
|
|
286
|
+
clazz = 'TestWorker'
|
|
287
|
+
hash[queue] = clazz
|
|
288
|
+
|
|
289
|
+
# Pattern from batch_receive_messages? method
|
|
290
|
+
expect(hash[queue]).to eq(clazz)
|
|
291
|
+
|
|
292
|
+
# Pattern from fetch_worker method
|
|
293
|
+
expect(hash[queue]).to eq(clazz)
|
|
294
|
+
|
|
295
|
+
# Pattern from queues method
|
|
296
|
+
expect(hash.keys).to eq([queue])
|
|
297
|
+
|
|
298
|
+
# Pattern from workers method (with fetch and default)
|
|
299
|
+
expect(hash.fetch(queue, [])).to eq(clazz)
|
|
300
|
+
expect(hash.fetch('missing', [])).to eq([])
|
|
301
|
+
|
|
302
|
+
# Pattern from clear method
|
|
303
|
+
hash.clear
|
|
304
|
+
expect(hash.keys).to eq([])
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|