shoryuken 7.0.0.alpha1 → 7.0.0.alpha2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/push.yml +3 -3
- data/.github/workflows/specs.yml +6 -9
- data/.github/workflows/verify-action-pins.yml +1 -1
- data/.rspec +2 -1
- data/.ruby-version +1 -1
- data/Appraisals +0 -6
- data/CHANGELOG.md +186 -142
- data/Gemfile +1 -0
- data/README.md +12 -13
- data/bin/cli/base.rb +1 -2
- data/bin/cli/sqs.rb +5 -4
- data/bin/shoryuken +2 -1
- data/gemfiles/rails_7_0.gemfile +10 -10
- data/gemfiles/rails_7_1.gemfile +10 -9
- data/gemfiles/rails_7_2.gemfile +10 -9
- data/gemfiles/rails_8_0.gemfile +10 -9
- 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 +8 -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/inline_message.rb +22 -0
- data/lib/shoryuken/launcher.rb +2 -0
- data/lib/shoryuken/logging.rb +19 -5
- data/lib/shoryuken/manager.rb +6 -4
- data/lib/shoryuken/message.rb +2 -0
- data/lib/shoryuken/middleware/chain.rb +2 -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 +10 -10
- 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 +9 -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 +2 -0
- data/lib/shoryuken/worker_registry.rb +2 -0
- data/lib/shoryuken.rb +8 -28
- data/shoryuken.gemspec +6 -6
- 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_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_integration_spec.rb +96 -0
- data/spec/shoryuken/inline_message_spec.rb +196 -0
- data/spec/shoryuken/launcher_spec.rb +1 -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 +60 -27
- data/.github/FUNDING.yml +0 -12
- data/gemfiles/rails_6_1.gemfile +0 -18
- 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
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
require 'shoryuken/manager'
|
|
3
|
-
require 'shoryuken/fetcher'
|
|
1
|
+
# frozen_string_literal: true
|
|
4
2
|
|
|
5
|
-
# rubocop:disable Metrics/BlockLength
|
|
6
3
|
RSpec.describe Shoryuken::Fetcher do
|
|
7
4
|
let(:queue) { instance_double('Shoryuken::Queue', fifo?: false) }
|
|
8
5
|
let(:queue_name) { 'default' }
|
|
@@ -29,17 +26,17 @@ RSpec.describe Shoryuken::Fetcher do
|
|
|
29
26
|
Shoryuken.sqs_client_receive_message_opts[group] = { wait_time_seconds: 10 }
|
|
30
27
|
|
|
31
28
|
expect(queue).to receive(:receive_messages).with({
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
wait_time_seconds: 10,
|
|
30
|
+
max_number_of_messages: limit,
|
|
31
|
+
message_attribute_names: ['All'],
|
|
32
|
+
attribute_names: ['All']
|
|
33
|
+
}).and_return([])
|
|
37
34
|
|
|
38
35
|
subject.fetch(queue_config, limit)
|
|
39
36
|
end
|
|
40
37
|
|
|
41
38
|
it 'logs debug only' do
|
|
42
|
-
# See https://github.com/
|
|
39
|
+
# See https://github.com/ruby-shoryuken/shoryuken/issues/435
|
|
43
40
|
logger = double 'logger'
|
|
44
41
|
|
|
45
42
|
allow(subject).to receive(:logger).and_return(logger)
|
|
@@ -63,10 +60,10 @@ RSpec.describe Shoryuken::Fetcher do
|
|
|
63
60
|
Shoryuken.sqs_client_receive_message_opts[queue_name] = { max_number_of_messages: 1 }
|
|
64
61
|
|
|
65
62
|
expect(queue).to receive(:receive_messages).with({
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
63
|
+
max_number_of_messages: 1,
|
|
64
|
+
message_attribute_names: ['All'],
|
|
65
|
+
attribute_names: ['All']
|
|
66
|
+
}).and_return([])
|
|
70
67
|
|
|
71
68
|
subject.fetch(queue_config, limit)
|
|
72
69
|
end
|
|
@@ -79,10 +76,10 @@ RSpec.describe Shoryuken::Fetcher do
|
|
|
79
76
|
Shoryuken.sqs_client_receive_message_opts[queue_name] = { max_number_of_messages: 20 }
|
|
80
77
|
|
|
81
78
|
expect(queue).to receive(:receive_messages).with({
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
79
|
+
max_number_of_messages: limit,
|
|
80
|
+
message_attribute_names: ['All'],
|
|
81
|
+
attribute_names: ['All']
|
|
82
|
+
}).and_return([])
|
|
86
83
|
|
|
87
84
|
subject.fetch(queue_config, limit)
|
|
88
85
|
end
|
|
@@ -94,8 +91,8 @@ RSpec.describe Shoryuken::Fetcher do
|
|
|
94
91
|
specify do
|
|
95
92
|
allow(Shoryuken::Client).to receive(:queues).with(queue_name).and_return(queue)
|
|
96
93
|
expect(queue).to receive(:receive_messages).with({
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
max_number_of_messages: described_class::FETCH_LIMIT, attribute_names: ['All'], message_attribute_names: ['All']
|
|
95
|
+
}).and_return([])
|
|
99
96
|
|
|
100
97
|
subject.fetch(queue_config, limit)
|
|
101
98
|
end
|
|
@@ -106,26 +103,26 @@ RSpec.describe Shoryuken::Fetcher do
|
|
|
106
103
|
let(:queue) { instance_double('Shoryuken::Queue', fifo?: true, name: queue_name) }
|
|
107
104
|
|
|
108
105
|
it 'polls one message at a time' do
|
|
109
|
-
# see https://github.com/
|
|
106
|
+
# see https://github.com/ruby-shoryuken/shoryuken/pull/530
|
|
110
107
|
|
|
111
108
|
allow(Shoryuken::Client).to receive(:queues).with(queue_name).and_return(queue)
|
|
112
109
|
expect(queue).to receive(:receive_messages).with({
|
|
113
|
-
|
|
114
|
-
|
|
110
|
+
max_number_of_messages: 1, attribute_names: ['All'], message_attribute_names: ['All']
|
|
111
|
+
}).and_return([])
|
|
115
112
|
|
|
116
113
|
subject.fetch(queue_config, limit)
|
|
117
114
|
end
|
|
118
115
|
|
|
119
116
|
context 'with batch=true' do
|
|
120
117
|
it 'polls the provided limit' do
|
|
121
|
-
# see https://github.com/
|
|
118
|
+
# see https://github.com/ruby-shoryuken/shoryuken/pull/530
|
|
122
119
|
|
|
123
120
|
allow(Shoryuken::Client).to receive(:queues).with(queue_name).and_return(queue)
|
|
124
121
|
allow(Shoryuken.worker_registry).to receive(:batch_receive_messages?).with(queue.name).and_return(true)
|
|
125
122
|
|
|
126
123
|
expect(queue).to receive(:receive_messages).with({
|
|
127
|
-
|
|
128
|
-
|
|
124
|
+
max_number_of_messages: limit, attribute_names: ['All'], message_attribute_names: ['All']
|
|
125
|
+
}).and_return([])
|
|
129
126
|
|
|
130
127
|
subject.fetch(queue_config, limit)
|
|
131
128
|
end
|
|
@@ -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
|