karafka-rdkafka 0.19.1 → 0.19.2.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
- checksums.yaml.gz.sig +0 -0
- data/.github/workflows/ci.yml +12 -2
- data/.ruby-version +1 -1
- data/CHANGELOG.md +4 -0
- data/README.md +13 -12
- data/docker-compose.yml +1 -1
- data/lib/rdkafka/bindings.rb +25 -1
- data/lib/rdkafka/producer/partitions_count_cache.rb +216 -0
- data/lib/rdkafka/producer.rb +34 -29
- data/lib/rdkafka/version.rb +1 -1
- data/lib/rdkafka.rb +1 -0
- data/spec/rdkafka/admin_spec.rb +12 -10
- data/spec/rdkafka/bindings_spec.rb +0 -9
- data/spec/rdkafka/config_spec.rb +17 -15
- data/spec/rdkafka/producer/partitions_count_cache_spec.rb +359 -0
- data/spec/rdkafka/producer/partitions_count_spec.rb +359 -0
- data/spec/rdkafka/producer_spec.rb +116 -3
- data/spec/spec_helper.rb +9 -0
- data.tar.gz.sig +1 -3
- metadata +8 -3
- metadata.gz.sig +0 -0
@@ -0,0 +1,359 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe Rdkafka::Producer::PartitionsCountCache do
|
6
|
+
let(:default_ttl) { 1 } # Reduced from 30 to speed up tests
|
7
|
+
let(:custom_ttl) { 0.5 } # Half the default TTL
|
8
|
+
let(:cache) { described_class.new(default_ttl) }
|
9
|
+
let(:custom_ttl_cache) { described_class.new(custom_ttl) }
|
10
|
+
let(:topic) { "test_topic" }
|
11
|
+
let(:topic2) { "test_topic2" }
|
12
|
+
let(:partition_count) { 5 }
|
13
|
+
let(:higher_partition_count) { 10 }
|
14
|
+
let(:lower_partition_count) { 3 }
|
15
|
+
let(:even_higher_partition_count) { 15 }
|
16
|
+
|
17
|
+
describe "#initialize" do
|
18
|
+
it "creates a cache with default TTL when no TTL is specified" do
|
19
|
+
standard_cache = described_class.new
|
20
|
+
expect(standard_cache).to be_a(described_class)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "creates a cache with custom TTL when specified" do
|
24
|
+
expect(custom_ttl_cache).to be_a(described_class)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "#get" do
|
29
|
+
context "when cache is empty" do
|
30
|
+
it "yields to get the value and caches it" do
|
31
|
+
block_called = false
|
32
|
+
result = cache.get(topic) do
|
33
|
+
block_called = true
|
34
|
+
partition_count
|
35
|
+
end
|
36
|
+
|
37
|
+
expect(block_called).to be true
|
38
|
+
expect(result).to eq(partition_count)
|
39
|
+
|
40
|
+
# Verify caching by checking if block is called again
|
41
|
+
second_block_called = false
|
42
|
+
second_result = cache.get(topic) do
|
43
|
+
second_block_called = true
|
44
|
+
partition_count + 1 # Different value to ensure we get cached value
|
45
|
+
end
|
46
|
+
|
47
|
+
expect(second_block_called).to be false
|
48
|
+
expect(second_result).to eq(partition_count)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
context "when cache has a value" do
|
53
|
+
before do
|
54
|
+
# Seed the cache with a value
|
55
|
+
cache.get(topic) { partition_count }
|
56
|
+
end
|
57
|
+
|
58
|
+
it "returns cached value without yielding if not expired" do
|
59
|
+
block_called = false
|
60
|
+
result = cache.get(topic) do
|
61
|
+
block_called = true
|
62
|
+
partition_count + 1 # Different value to ensure we get cached one
|
63
|
+
end
|
64
|
+
|
65
|
+
expect(block_called).to be false
|
66
|
+
expect(result).to eq(partition_count)
|
67
|
+
end
|
68
|
+
|
69
|
+
it "yields to get new value when TTL has expired" do
|
70
|
+
# Wait for TTL to expire
|
71
|
+
sleep(default_ttl + 0.1)
|
72
|
+
|
73
|
+
block_called = false
|
74
|
+
new_count = partition_count + 1
|
75
|
+
result = cache.get(topic) do
|
76
|
+
block_called = true
|
77
|
+
new_count
|
78
|
+
end
|
79
|
+
|
80
|
+
expect(block_called).to be true
|
81
|
+
expect(result).to eq(new_count)
|
82
|
+
|
83
|
+
# Verify the new value is cached
|
84
|
+
second_block_called = false
|
85
|
+
second_result = cache.get(topic) do
|
86
|
+
second_block_called = true
|
87
|
+
new_count + 1 # Different value again
|
88
|
+
end
|
89
|
+
|
90
|
+
expect(second_block_called).to be false
|
91
|
+
expect(second_result).to eq(new_count)
|
92
|
+
end
|
93
|
+
|
94
|
+
it "respects a custom TTL" do
|
95
|
+
# Seed the custom TTL cache with a value
|
96
|
+
custom_ttl_cache.get(topic) { partition_count }
|
97
|
+
|
98
|
+
# Wait for custom TTL to expire but not default TTL
|
99
|
+
sleep(custom_ttl + 0.1)
|
100
|
+
|
101
|
+
# Custom TTL cache should refresh
|
102
|
+
custom_block_called = false
|
103
|
+
custom_result = custom_ttl_cache.get(topic) do
|
104
|
+
custom_block_called = true
|
105
|
+
higher_partition_count
|
106
|
+
end
|
107
|
+
|
108
|
+
expect(custom_block_called).to be true
|
109
|
+
expect(custom_result).to eq(higher_partition_count)
|
110
|
+
|
111
|
+
# Default TTL cache should not refresh yet
|
112
|
+
default_block_called = false
|
113
|
+
default_result = cache.get(topic) do
|
114
|
+
default_block_called = true
|
115
|
+
higher_partition_count
|
116
|
+
end
|
117
|
+
|
118
|
+
expect(default_block_called).to be false
|
119
|
+
expect(default_result).to eq(partition_count)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
context "when new value is obtained" do
|
124
|
+
before do
|
125
|
+
# Seed the cache with initial value
|
126
|
+
cache.get(topic) { partition_count }
|
127
|
+
end
|
128
|
+
|
129
|
+
it "updates cache when new value is higher than cached value" do
|
130
|
+
# Wait for TTL to expire
|
131
|
+
sleep(default_ttl + 0.1)
|
132
|
+
|
133
|
+
# Get higher value
|
134
|
+
result = cache.get(topic) { higher_partition_count }
|
135
|
+
expect(result).to eq(higher_partition_count)
|
136
|
+
|
137
|
+
# Verify it was cached
|
138
|
+
second_result = cache.get(topic) { fail "Should not be called" }
|
139
|
+
expect(second_result).to eq(higher_partition_count)
|
140
|
+
end
|
141
|
+
|
142
|
+
it "preserves higher cached value when new value is lower" do
|
143
|
+
# First update to higher value
|
144
|
+
sleep(default_ttl + 0.1)
|
145
|
+
cache.get(topic) { higher_partition_count }
|
146
|
+
|
147
|
+
# Then try to update to lower value
|
148
|
+
sleep(default_ttl + 0.1)
|
149
|
+
result = cache.get(topic) { lower_partition_count }
|
150
|
+
|
151
|
+
expect(result).to eq(higher_partition_count)
|
152
|
+
|
153
|
+
# and subsequent gets should return the previously cached higher value
|
154
|
+
second_result = cache.get(topic) { fail "Should not be called" }
|
155
|
+
expect(second_result).to eq(higher_partition_count)
|
156
|
+
end
|
157
|
+
|
158
|
+
it "handles multiple topics independently" do
|
159
|
+
# Set up both topics with different values
|
160
|
+
cache.get(topic) { partition_count }
|
161
|
+
cache.get(topic2) { higher_partition_count }
|
162
|
+
|
163
|
+
# Wait for TTL to expire
|
164
|
+
sleep(default_ttl + 0.1)
|
165
|
+
|
166
|
+
# Update first topic
|
167
|
+
first_result = cache.get(topic) { even_higher_partition_count }
|
168
|
+
expect(first_result).to eq(even_higher_partition_count)
|
169
|
+
|
170
|
+
# Update second topic independently
|
171
|
+
second_updated = higher_partition_count + 3
|
172
|
+
second_result = cache.get(topic2) { second_updated }
|
173
|
+
expect(second_result).to eq(second_updated)
|
174
|
+
|
175
|
+
# Both topics should have their updated values
|
176
|
+
expect(cache.get(topic) { fail "Should not be called" }).to eq(even_higher_partition_count)
|
177
|
+
expect(cache.get(topic2) { fail "Should not be called" }).to eq(second_updated)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
describe "#set" do
|
183
|
+
context "when cache is empty" do
|
184
|
+
it "adds a new entry to the cache" do
|
185
|
+
cache.set(topic, partition_count)
|
186
|
+
|
187
|
+
# Verify through get
|
188
|
+
result = cache.get(topic) { fail "Should not be called" }
|
189
|
+
expect(result).to eq(partition_count)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
context "when cache already has a value" do
|
194
|
+
before do
|
195
|
+
cache.set(topic, partition_count)
|
196
|
+
end
|
197
|
+
|
198
|
+
it "updates cache when new value is higher" do
|
199
|
+
cache.set(topic, higher_partition_count)
|
200
|
+
|
201
|
+
result = cache.get(topic) { fail "Should not be called" }
|
202
|
+
expect(result).to eq(higher_partition_count)
|
203
|
+
end
|
204
|
+
|
205
|
+
it "keeps original value when new value is lower" do
|
206
|
+
cache.set(topic, lower_partition_count)
|
207
|
+
|
208
|
+
result = cache.get(topic) { fail "Should not be called" }
|
209
|
+
expect(result).to eq(partition_count)
|
210
|
+
end
|
211
|
+
|
212
|
+
it "updates the timestamp even when keeping original value" do
|
213
|
+
# Set initial value
|
214
|
+
cache.set(topic, partition_count)
|
215
|
+
|
216
|
+
# Wait until close to TTL expiring
|
217
|
+
sleep(default_ttl - 0.2)
|
218
|
+
|
219
|
+
# Set lower value (should update timestamp but not value)
|
220
|
+
cache.set(topic, lower_partition_count)
|
221
|
+
|
222
|
+
# Wait a bit more, but still under the full TTL if timestamp was refreshed
|
223
|
+
sleep(0.3)
|
224
|
+
|
225
|
+
# Should still be valid due to timestamp refresh
|
226
|
+
result = cache.get(topic) { fail "Should not be called" }
|
227
|
+
expect(result).to eq(partition_count)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
context "with concurrent access" do
|
232
|
+
it "correctly handles simultaneous updates to the same topic" do
|
233
|
+
# This test focuses on the final value after concurrent updates
|
234
|
+
threads = []
|
235
|
+
|
236
|
+
# Create 5 threads that all try to update the same topic with increasing values
|
237
|
+
5.times do |i|
|
238
|
+
threads << Thread.new do
|
239
|
+
value = 10 + i # Start at 10 to ensure all are higher than initial value
|
240
|
+
cache.set(topic, value)
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
# Wait for all threads to complete
|
245
|
+
threads.each(&:join)
|
246
|
+
|
247
|
+
# The highest value (14) should be stored and accessible through get
|
248
|
+
result = cache.get(topic) { fail "Should not be called" }
|
249
|
+
expect(result).to eq(14)
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
describe "TTL behavior" do
|
255
|
+
it "treats entries as expired when they exceed TTL" do
|
256
|
+
# Set initial value
|
257
|
+
cache.get(topic) { partition_count }
|
258
|
+
|
259
|
+
# Wait just under TTL
|
260
|
+
sleep(default_ttl - 0.1)
|
261
|
+
|
262
|
+
# Value should still be cached (block should not be called)
|
263
|
+
result = cache.get(topic) { fail "Should not be called when cache is valid" }
|
264
|
+
expect(result).to eq(partition_count)
|
265
|
+
|
266
|
+
# Now wait to exceed TTL
|
267
|
+
sleep(0.2) # Total sleep is now default_ttl + 0.1
|
268
|
+
|
269
|
+
# Cache should be expired, block should be called
|
270
|
+
block_called = false
|
271
|
+
new_value = partition_count + 3
|
272
|
+
result = cache.get(topic) do
|
273
|
+
block_called = true
|
274
|
+
new_value
|
275
|
+
end
|
276
|
+
|
277
|
+
expect(block_called).to be true
|
278
|
+
expect(result).to eq(new_value)
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
describe "comprehensive scenarios" do
|
283
|
+
it "handles a full lifecycle of cache operations" do
|
284
|
+
# 1. Initial cache miss, fetch and store
|
285
|
+
result1 = cache.get(topic) { partition_count }
|
286
|
+
expect(result1).to eq(partition_count)
|
287
|
+
|
288
|
+
# 2. Cache hit
|
289
|
+
result2 = cache.get(topic) { fail "Should not be called" }
|
290
|
+
expect(result2).to eq(partition_count)
|
291
|
+
|
292
|
+
# 3. Attempt to set lower value
|
293
|
+
cache.set(topic, lower_partition_count)
|
294
|
+
result3 = cache.get(topic) { fail "Should not be called" }
|
295
|
+
# Should still return the higher original value
|
296
|
+
expect(result3).to eq(partition_count)
|
297
|
+
|
298
|
+
# 4. Set higher value
|
299
|
+
cache.set(topic, higher_partition_count)
|
300
|
+
result4 = cache.get(topic) { fail "Should not be called" }
|
301
|
+
expect(result4).to eq(higher_partition_count)
|
302
|
+
|
303
|
+
# 5. TTL expires, new value provided is lower
|
304
|
+
sleep(default_ttl + 0.1)
|
305
|
+
result5 = cache.get(topic) { lower_partition_count }
|
306
|
+
# This returns the highest value
|
307
|
+
expect(result5).to eq(higher_partition_count)
|
308
|
+
|
309
|
+
# 6. But subsequent get should return the higher cached value
|
310
|
+
result6 = cache.get(topic) { fail "Should not be called" }
|
311
|
+
expect(result6).to eq(higher_partition_count)
|
312
|
+
|
313
|
+
# 7. Set new highest value directly
|
314
|
+
even_higher = higher_partition_count + 5
|
315
|
+
cache.set(topic, even_higher)
|
316
|
+
result7 = cache.get(topic) { fail "Should not be called" }
|
317
|
+
expect(result7).to eq(even_higher)
|
318
|
+
end
|
319
|
+
|
320
|
+
it "handles multiple topics with different TTLs correctly" do
|
321
|
+
# Set up initial values
|
322
|
+
cache.get(topic) { partition_count }
|
323
|
+
custom_ttl_cache.get(topic) { partition_count }
|
324
|
+
|
325
|
+
# Wait past custom TTL but not default TTL
|
326
|
+
sleep(custom_ttl + 0.1)
|
327
|
+
|
328
|
+
# Default cache should NOT refresh (still within default TTL)
|
329
|
+
default_result = cache.get(topic) { fail "Should not be called for default cache" }
|
330
|
+
# Original value should be maintained
|
331
|
+
expect(default_result).to eq(partition_count)
|
332
|
+
|
333
|
+
# Custom TTL cache SHOULD refresh (past custom TTL)
|
334
|
+
custom_cache_value = partition_count + 8
|
335
|
+
custom_block_called = false
|
336
|
+
custom_result = custom_ttl_cache.get(topic) do
|
337
|
+
custom_block_called = true
|
338
|
+
custom_cache_value
|
339
|
+
end
|
340
|
+
|
341
|
+
expect(custom_block_called).to be true
|
342
|
+
expect(custom_result).to eq(custom_cache_value)
|
343
|
+
|
344
|
+
# Now wait past default TTL
|
345
|
+
sleep(default_ttl - custom_ttl + 0.1)
|
346
|
+
|
347
|
+
# Now default cache should also refresh
|
348
|
+
default_block_called = false
|
349
|
+
new_default_value = partition_count + 10
|
350
|
+
new_default_result = cache.get(topic) do
|
351
|
+
default_block_called = true
|
352
|
+
new_default_value
|
353
|
+
end
|
354
|
+
|
355
|
+
expect(default_block_called).to be true
|
356
|
+
expect(new_default_result).to eq(new_default_value)
|
357
|
+
end
|
358
|
+
end
|
359
|
+
end
|
@@ -0,0 +1,359 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe Rdkafka::Producer::PartitionsCountCache do
|
6
|
+
let(:default_ttl) { 1 } # Reduced from 30 to speed up tests
|
7
|
+
let(:custom_ttl) { 0.5 } # Half the default TTL
|
8
|
+
let(:cache) { described_class.new(default_ttl) }
|
9
|
+
let(:custom_ttl_cache) { described_class.new(custom_ttl) }
|
10
|
+
let(:topic) { "test_topic" }
|
11
|
+
let(:topic2) { "test_topic2" }
|
12
|
+
let(:partition_count) { 5 }
|
13
|
+
let(:higher_partition_count) { 10 }
|
14
|
+
let(:lower_partition_count) { 3 }
|
15
|
+
let(:even_higher_partition_count) { 15 }
|
16
|
+
|
17
|
+
describe "#initialize" do
|
18
|
+
it "creates a cache with default TTL when no TTL is specified" do
|
19
|
+
standard_cache = described_class.new
|
20
|
+
expect(standard_cache).to be_a(described_class)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "creates a cache with custom TTL when specified" do
|
24
|
+
expect(custom_ttl_cache).to be_a(described_class)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "#get" do
|
29
|
+
context "when cache is empty" do
|
30
|
+
it "yields to get the value and caches it" do
|
31
|
+
block_called = false
|
32
|
+
result = cache.get(topic) do
|
33
|
+
block_called = true
|
34
|
+
partition_count
|
35
|
+
end
|
36
|
+
|
37
|
+
expect(block_called).to be true
|
38
|
+
expect(result).to eq(partition_count)
|
39
|
+
|
40
|
+
# Verify caching by checking if block is called again
|
41
|
+
second_block_called = false
|
42
|
+
second_result = cache.get(topic) do
|
43
|
+
second_block_called = true
|
44
|
+
partition_count + 1 # Different value to ensure we get cached value
|
45
|
+
end
|
46
|
+
|
47
|
+
expect(second_block_called).to be false
|
48
|
+
expect(second_result).to eq(partition_count)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
context "when cache has a value" do
|
53
|
+
before do
|
54
|
+
# Seed the cache with a value
|
55
|
+
cache.get(topic) { partition_count }
|
56
|
+
end
|
57
|
+
|
58
|
+
it "returns cached value without yielding if not expired" do
|
59
|
+
block_called = false
|
60
|
+
result = cache.get(topic) do
|
61
|
+
block_called = true
|
62
|
+
partition_count + 1 # Different value to ensure we get cached one
|
63
|
+
end
|
64
|
+
|
65
|
+
expect(block_called).to be false
|
66
|
+
expect(result).to eq(partition_count)
|
67
|
+
end
|
68
|
+
|
69
|
+
it "yields to get new value when TTL has expired" do
|
70
|
+
# Wait for TTL to expire
|
71
|
+
sleep(default_ttl + 0.1)
|
72
|
+
|
73
|
+
block_called = false
|
74
|
+
new_count = partition_count + 1
|
75
|
+
result = cache.get(topic) do
|
76
|
+
block_called = true
|
77
|
+
new_count
|
78
|
+
end
|
79
|
+
|
80
|
+
expect(block_called).to be true
|
81
|
+
expect(result).to eq(new_count)
|
82
|
+
|
83
|
+
# Verify the new value is cached
|
84
|
+
second_block_called = false
|
85
|
+
second_result = cache.get(topic) do
|
86
|
+
second_block_called = true
|
87
|
+
new_count + 1 # Different value again
|
88
|
+
end
|
89
|
+
|
90
|
+
expect(second_block_called).to be false
|
91
|
+
expect(second_result).to eq(new_count)
|
92
|
+
end
|
93
|
+
|
94
|
+
it "respects a custom TTL" do
|
95
|
+
# Seed the custom TTL cache with a value
|
96
|
+
custom_ttl_cache.get(topic) { partition_count }
|
97
|
+
|
98
|
+
# Wait for custom TTL to expire but not default TTL
|
99
|
+
sleep(custom_ttl + 0.1)
|
100
|
+
|
101
|
+
# Custom TTL cache should refresh
|
102
|
+
custom_block_called = false
|
103
|
+
custom_result = custom_ttl_cache.get(topic) do
|
104
|
+
custom_block_called = true
|
105
|
+
higher_partition_count
|
106
|
+
end
|
107
|
+
|
108
|
+
expect(custom_block_called).to be true
|
109
|
+
expect(custom_result).to eq(higher_partition_count)
|
110
|
+
|
111
|
+
# Default TTL cache should not refresh yet
|
112
|
+
default_block_called = false
|
113
|
+
default_result = cache.get(topic) do
|
114
|
+
default_block_called = true
|
115
|
+
higher_partition_count
|
116
|
+
end
|
117
|
+
|
118
|
+
expect(default_block_called).to be false
|
119
|
+
expect(default_result).to eq(partition_count)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
context "when new value is obtained" do
|
124
|
+
before do
|
125
|
+
# Seed the cache with initial value
|
126
|
+
cache.get(topic) { partition_count }
|
127
|
+
end
|
128
|
+
|
129
|
+
it "updates cache when new value is higher than cached value" do
|
130
|
+
# Wait for TTL to expire
|
131
|
+
sleep(default_ttl + 0.1)
|
132
|
+
|
133
|
+
# Get higher value
|
134
|
+
result = cache.get(topic) { higher_partition_count }
|
135
|
+
expect(result).to eq(higher_partition_count)
|
136
|
+
|
137
|
+
# Verify it was cached
|
138
|
+
second_result = cache.get(topic) { fail "Should not be called" }
|
139
|
+
expect(second_result).to eq(higher_partition_count)
|
140
|
+
end
|
141
|
+
|
142
|
+
it "preserves higher cached value when new value is lower" do
|
143
|
+
# First update to higher value
|
144
|
+
sleep(default_ttl + 0.1)
|
145
|
+
cache.get(topic) { higher_partition_count }
|
146
|
+
|
147
|
+
# Then try to update to lower value
|
148
|
+
sleep(default_ttl + 0.1)
|
149
|
+
result = cache.get(topic) { lower_partition_count }
|
150
|
+
|
151
|
+
expect(result).to eq(higher_partition_count)
|
152
|
+
|
153
|
+
# and subsequent gets should return the previously cached higher value
|
154
|
+
second_result = cache.get(topic) { fail "Should not be called" }
|
155
|
+
expect(second_result).to eq(higher_partition_count)
|
156
|
+
end
|
157
|
+
|
158
|
+
it "handles multiple topics independently" do
|
159
|
+
# Set up both topics with different values
|
160
|
+
cache.get(topic) { partition_count }
|
161
|
+
cache.get(topic2) { higher_partition_count }
|
162
|
+
|
163
|
+
# Wait for TTL to expire
|
164
|
+
sleep(default_ttl + 0.1)
|
165
|
+
|
166
|
+
# Update first topic
|
167
|
+
first_result = cache.get(topic) { even_higher_partition_count }
|
168
|
+
expect(first_result).to eq(even_higher_partition_count)
|
169
|
+
|
170
|
+
# Update second topic independently
|
171
|
+
second_updated = higher_partition_count + 3
|
172
|
+
second_result = cache.get(topic2) { second_updated }
|
173
|
+
expect(second_result).to eq(second_updated)
|
174
|
+
|
175
|
+
# Both topics should have their updated values
|
176
|
+
expect(cache.get(topic) { fail "Should not be called" }).to eq(even_higher_partition_count)
|
177
|
+
expect(cache.get(topic2) { fail "Should not be called" }).to eq(second_updated)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
describe "#set" do
|
183
|
+
context "when cache is empty" do
|
184
|
+
it "adds a new entry to the cache" do
|
185
|
+
cache.set(topic, partition_count)
|
186
|
+
|
187
|
+
# Verify through get
|
188
|
+
result = cache.get(topic) { fail "Should not be called" }
|
189
|
+
expect(result).to eq(partition_count)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
context "when cache already has a value" do
|
194
|
+
before do
|
195
|
+
cache.set(topic, partition_count)
|
196
|
+
end
|
197
|
+
|
198
|
+
it "updates cache when new value is higher" do
|
199
|
+
cache.set(topic, higher_partition_count)
|
200
|
+
|
201
|
+
result = cache.get(topic) { fail "Should not be called" }
|
202
|
+
expect(result).to eq(higher_partition_count)
|
203
|
+
end
|
204
|
+
|
205
|
+
it "keeps original value when new value is lower" do
|
206
|
+
cache.set(topic, lower_partition_count)
|
207
|
+
|
208
|
+
result = cache.get(topic) { fail "Should not be called" }
|
209
|
+
expect(result).to eq(partition_count)
|
210
|
+
end
|
211
|
+
|
212
|
+
it "updates the timestamp even when keeping original value" do
|
213
|
+
# Set initial value
|
214
|
+
cache.set(topic, partition_count)
|
215
|
+
|
216
|
+
# Wait until close to TTL expiring
|
217
|
+
sleep(default_ttl - 0.2)
|
218
|
+
|
219
|
+
# Set lower value (should update timestamp but not value)
|
220
|
+
cache.set(topic, lower_partition_count)
|
221
|
+
|
222
|
+
# Wait a bit more, but still under the full TTL if timestamp was refreshed
|
223
|
+
sleep(0.3)
|
224
|
+
|
225
|
+
# Should still be valid due to timestamp refresh
|
226
|
+
result = cache.get(topic) { fail "Should not be called" }
|
227
|
+
expect(result).to eq(partition_count)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
context "with concurrent access" do
|
232
|
+
it "correctly handles simultaneous updates to the same topic" do
|
233
|
+
# This test focuses on the final value after concurrent updates
|
234
|
+
threads = []
|
235
|
+
|
236
|
+
# Create 5 threads that all try to update the same topic with increasing values
|
237
|
+
5.times do |i|
|
238
|
+
threads << Thread.new do
|
239
|
+
value = 10 + i # Start at 10 to ensure all are higher than initial value
|
240
|
+
cache.set(topic, value)
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
# Wait for all threads to complete
|
245
|
+
threads.each(&:join)
|
246
|
+
|
247
|
+
# The highest value (14) should be stored and accessible through get
|
248
|
+
result = cache.get(topic) { fail "Should not be called" }
|
249
|
+
expect(result).to eq(14)
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
describe "TTL behavior" do
|
255
|
+
it "treats entries as expired when they exceed TTL" do
|
256
|
+
# Set initial value
|
257
|
+
cache.get(topic) { partition_count }
|
258
|
+
|
259
|
+
# Wait just under TTL
|
260
|
+
sleep(default_ttl - 0.1)
|
261
|
+
|
262
|
+
# Value should still be cached (block should not be called)
|
263
|
+
result = cache.get(topic) { fail "Should not be called when cache is valid" }
|
264
|
+
expect(result).to eq(partition_count)
|
265
|
+
|
266
|
+
# Now wait to exceed TTL
|
267
|
+
sleep(0.2) # Total sleep is now default_ttl + 0.1
|
268
|
+
|
269
|
+
# Cache should be expired, block should be called
|
270
|
+
block_called = false
|
271
|
+
new_value = partition_count + 3
|
272
|
+
result = cache.get(topic) do
|
273
|
+
block_called = true
|
274
|
+
new_value
|
275
|
+
end
|
276
|
+
|
277
|
+
expect(block_called).to be true
|
278
|
+
expect(result).to eq(new_value)
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
describe "comprehensive scenarios" do
|
283
|
+
it "handles a full lifecycle of cache operations" do
|
284
|
+
# 1. Initial cache miss, fetch and store
|
285
|
+
result1 = cache.get(topic) { partition_count }
|
286
|
+
expect(result1).to eq(partition_count)
|
287
|
+
|
288
|
+
# 2. Cache hit
|
289
|
+
result2 = cache.get(topic) { fail "Should not be called" }
|
290
|
+
expect(result2).to eq(partition_count)
|
291
|
+
|
292
|
+
# 3. Attempt to set lower value
|
293
|
+
cache.set(topic, lower_partition_count)
|
294
|
+
result3 = cache.get(topic) { fail "Should not be called" }
|
295
|
+
# Should still return the higher original value
|
296
|
+
expect(result3).to eq(partition_count)
|
297
|
+
|
298
|
+
# 4. Set higher value
|
299
|
+
cache.set(topic, higher_partition_count)
|
300
|
+
result4 = cache.get(topic) { fail "Should not be called" }
|
301
|
+
expect(result4).to eq(higher_partition_count)
|
302
|
+
|
303
|
+
# 5. TTL expires, new value provided is lower
|
304
|
+
sleep(default_ttl + 0.1)
|
305
|
+
result5 = cache.get(topic) { lower_partition_count }
|
306
|
+
# This returns the highest value
|
307
|
+
expect(result5).to eq(higher_partition_count)
|
308
|
+
|
309
|
+
# 6. But subsequent get should return the higher cached value
|
310
|
+
result6 = cache.get(topic) { fail "Should not be called" }
|
311
|
+
expect(result6).to eq(higher_partition_count)
|
312
|
+
|
313
|
+
# 7. Set new highest value directly
|
314
|
+
even_higher = higher_partition_count + 5
|
315
|
+
cache.set(topic, even_higher)
|
316
|
+
result7 = cache.get(topic) { fail "Should not be called" }
|
317
|
+
expect(result7).to eq(even_higher)
|
318
|
+
end
|
319
|
+
|
320
|
+
it "handles multiple topics with different TTLs correctly" do
|
321
|
+
# Set up initial values
|
322
|
+
cache.get(topic) { partition_count }
|
323
|
+
custom_ttl_cache.get(topic) { partition_count }
|
324
|
+
|
325
|
+
# Wait past custom TTL but not default TTL
|
326
|
+
sleep(custom_ttl + 0.1)
|
327
|
+
|
328
|
+
# Default cache should NOT refresh (still within default TTL)
|
329
|
+
default_result = cache.get(topic) { fail "Should not be called for default cache" }
|
330
|
+
# Original value should be maintained
|
331
|
+
expect(default_result).to eq(partition_count)
|
332
|
+
|
333
|
+
# Custom TTL cache SHOULD refresh (past custom TTL)
|
334
|
+
custom_cache_value = partition_count + 8
|
335
|
+
custom_block_called = false
|
336
|
+
custom_result = custom_ttl_cache.get(topic) do
|
337
|
+
custom_block_called = true
|
338
|
+
custom_cache_value
|
339
|
+
end
|
340
|
+
|
341
|
+
expect(custom_block_called).to be true
|
342
|
+
expect(custom_result).to eq(custom_cache_value)
|
343
|
+
|
344
|
+
# Now wait past default TTL
|
345
|
+
sleep(default_ttl - custom_ttl + 0.1)
|
346
|
+
|
347
|
+
# Now default cache should also refresh
|
348
|
+
default_block_called = false
|
349
|
+
new_default_value = partition_count + 10
|
350
|
+
new_default_result = cache.get(topic) do
|
351
|
+
default_block_called = true
|
352
|
+
new_default_value
|
353
|
+
end
|
354
|
+
|
355
|
+
expect(default_block_called).to be true
|
356
|
+
expect(new_default_result).to eq(new_default_value)
|
357
|
+
end
|
358
|
+
end
|
359
|
+
end
|