thread_cache 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +48 -0
- data/Gemfile +5 -0
- data/LICENSE +21 -0
- data/README.md +48 -0
- data/lib/thread_cache/version.rb +5 -0
- data/lib/thread_cache.rb +264 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/thread_cache_spec.rb +1240 -0
- data/thread_cache.gemspec +38 -0
- metadata +154 -0
@@ -0,0 +1,1240 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
require 'thread_cache'
|
5
|
+
|
6
|
+
RSpec.describe ThreadCache do
|
7
|
+
let(:current_unix_time) { 1577880000.0 }
|
8
|
+
let(:current_time) { Time.at(current_unix_time) }
|
9
|
+
|
10
|
+
describe '#new' do
|
11
|
+
it 'initializes the Thread.current attribute if needed' do
|
12
|
+
nillify_data_store(:custom_namespace)
|
13
|
+
|
14
|
+
described_class.new({ namespace: :custom_namespace })
|
15
|
+
|
16
|
+
expect(data_store(:custom_namespace)).not_to be_nil
|
17
|
+
expect(data_store(:custom_namespace)).to eq({})
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'does not initialize the Thread.current attribute if not needed' do
|
21
|
+
empty_data_store(:custom_namespace)
|
22
|
+
create_entry('some/key', {
|
23
|
+
value: 'some value',
|
24
|
+
version: '1',
|
25
|
+
expires_in: 180,
|
26
|
+
created_at: current_unix_time,
|
27
|
+
}, :custom_namespace)
|
28
|
+
|
29
|
+
described_class.new({ namespace: :custom_namespace })
|
30
|
+
|
31
|
+
expect(data_store(:custom_namespace)).to eq({
|
32
|
+
'some/key' => {
|
33
|
+
value: 'some value',
|
34
|
+
version: '1',
|
35
|
+
expires_in: 180,
|
36
|
+
created_at: current_unix_time,
|
37
|
+
}
|
38
|
+
})
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe '#clear' do
|
43
|
+
it 'clears all keys from the data store' do
|
44
|
+
create_entry('key1', { value: 'value1' })
|
45
|
+
create_entry('key2', { value: 'value2' })
|
46
|
+
create_entry('key3', { value: 'value3' })
|
47
|
+
|
48
|
+
described_class.new.clear
|
49
|
+
|
50
|
+
expect(data_store).to be_empty
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe '#exist?' do
|
55
|
+
it 'returns whether or not a key exists in the data store' do
|
56
|
+
empty_data_store
|
57
|
+
create_entry('some/key', { value: 'some value' })
|
58
|
+
|
59
|
+
thread_cache = described_class.new
|
60
|
+
|
61
|
+
expect(thread_cache.exist?('some/key')).to eq(true)
|
62
|
+
expect(thread_cache.exist?('some_nonexistent/key')).to eq(false)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe '#write' do
|
67
|
+
around do |spec|
|
68
|
+
travel_to(current_time, &spec)
|
69
|
+
end
|
70
|
+
|
71
|
+
before do
|
72
|
+
empty_data_store
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'adds an entry and with created_at set to the current unix time' do
|
76
|
+
described_class.new.write('some/key', 'some value')
|
77
|
+
|
78
|
+
entry = find_entry('some/key')
|
79
|
+
|
80
|
+
expect(entry[:value]).to eq('some value')
|
81
|
+
expect(entry[:created_at]).to eq(current_unix_time)
|
82
|
+
end
|
83
|
+
|
84
|
+
context 'when options version is not given' do
|
85
|
+
it 'adds an entry to the data store with version nil' do
|
86
|
+
described_class.new.write('some/key', 'some value')
|
87
|
+
|
88
|
+
entry = find_entry('some/key')
|
89
|
+
|
90
|
+
expect(entry[:value]).to eq('some value')
|
91
|
+
expect(entry[:version]).to be_nil
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
context 'when options version is given' do
|
96
|
+
it 'adds an entry to the data store with the given options version' do
|
97
|
+
described_class.new.write('some/key', 'some value', { version: '1' })
|
98
|
+
|
99
|
+
entry = find_entry('some/key')
|
100
|
+
|
101
|
+
expect(entry[:value]).to eq('some value')
|
102
|
+
expect(entry[:version]).to eq('1')
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
context 'when options expires_in is not given' do
|
107
|
+
it 'adds an entry to the data store with the default expires_in' do
|
108
|
+
described_class.new.write('some/key', 'some value')
|
109
|
+
|
110
|
+
entry = find_entry('some/key')
|
111
|
+
|
112
|
+
expect(entry[:value]).to eq('some value')
|
113
|
+
expect(entry[:expires_in]).to eq(60)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
context 'when options expires_in is given' do
|
118
|
+
it 'adds an entry to the data store with the given options expires_in' do
|
119
|
+
thread_cache = described_class.new({ expires_in: 60 })
|
120
|
+
thread_cache.write('some/key', 'some value', { expires_in: 300 })
|
121
|
+
|
122
|
+
entry = find_entry('some/key')
|
123
|
+
|
124
|
+
expect(entry[:value]).to eq('some value')
|
125
|
+
expect(entry[:expires_in]).to eq(300)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
context 'when value is nil, and options skip_nil is not given' do
|
130
|
+
it 'skips, and does not add an entry to the data store, when default skip_nil is true' do
|
131
|
+
thread_cache = described_class.new({ skip_nil: true })
|
132
|
+
thread_cache.write('some/key', nil)
|
133
|
+
|
134
|
+
entry = find_entry('some/key')
|
135
|
+
|
136
|
+
expect(entry).to be_nil
|
137
|
+
end
|
138
|
+
|
139
|
+
it 'does not skip, and adds an entry to the data store, when default skip_nil is false' do
|
140
|
+
thread_cache = described_class.new({ skip_nil: false })
|
141
|
+
thread_cache.write('some/key', nil)
|
142
|
+
|
143
|
+
entry = find_entry('some/key')
|
144
|
+
|
145
|
+
expect(entry).not_to be_nil
|
146
|
+
expect(entry[:value]).to eq(nil)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
context 'when value is nil, and options skip_nil is given' do
|
151
|
+
it 'skips, and does not add an entry to the data store, when options skip_nil is true' do
|
152
|
+
thread_cache = described_class.new({ skip_nil: false }) # It will override this default
|
153
|
+
thread_cache.write('some/key', nil, { skip_nil: true })
|
154
|
+
|
155
|
+
entry = find_entry('some/key')
|
156
|
+
|
157
|
+
expect(entry).to be_nil
|
158
|
+
end
|
159
|
+
|
160
|
+
it 'does not skip, and adds an entry to the data store, when options skip_nil is false' do
|
161
|
+
thread_cache = described_class.new({ skip_nil: true }) # It will override this default
|
162
|
+
thread_cache.write('some/key', nil, { skip_nil: false })
|
163
|
+
|
164
|
+
entry = find_entry('some/key')
|
165
|
+
|
166
|
+
expect(entry).not_to be_nil
|
167
|
+
expect(entry[:value]).to eq(nil)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
describe '#read' do
|
173
|
+
around do |spec|
|
174
|
+
travel_to(current_time, &spec)
|
175
|
+
end
|
176
|
+
|
177
|
+
before do
|
178
|
+
empty_data_store
|
179
|
+
end
|
180
|
+
|
181
|
+
context 'when an entry is not found' do
|
182
|
+
it 'returns a nil value' do
|
183
|
+
value = described_class.new.read('some_nonexistent/key')
|
184
|
+
|
185
|
+
expect(value).to be_nil
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
context 'when an entry is found, but it has expired' do
|
190
|
+
it 'returns a nil value and deletes from the data store' do
|
191
|
+
create_entry('some/key', { value: 'some value', expires_in: -1 })
|
192
|
+
|
193
|
+
value = described_class.new.read('some/key')
|
194
|
+
|
195
|
+
expect(value).to be_nil
|
196
|
+
expect(find_entry('some/key')).to be_nil
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
context 'when an entry is found, and it has not expired, but its version mismatches' do
|
201
|
+
it 'returns a nil value and deletes from the data store' do
|
202
|
+
create_entry('some/key', { value: 'some value', version: '1', expires_in: 300 })
|
203
|
+
|
204
|
+
value = described_class.new.read('some/key', version: '2')
|
205
|
+
|
206
|
+
expect(value).to be_nil
|
207
|
+
expect(find_entry('some/key')).to be_nil
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
context 'when an entry is found, and it has not expired, and its version does not mismatch' do
|
212
|
+
it 'returns the value and does not delete from the data store' do
|
213
|
+
create_entry('some/key', { value: 'some value', version: nil, expires_in: 300 })
|
214
|
+
|
215
|
+
value = described_class.new.read('some/key')
|
216
|
+
|
217
|
+
expect(value).to eq('some value')
|
218
|
+
expect(find_entry('some/key')).to eq(
|
219
|
+
build_entry({
|
220
|
+
value: 'some value',
|
221
|
+
version: nil,
|
222
|
+
expires_in: 300,
|
223
|
+
created_at: current_unix_time
|
224
|
+
})
|
225
|
+
)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
describe '#fetch' do
|
231
|
+
around do |spec|
|
232
|
+
travel_to(current_time, &spec)
|
233
|
+
end
|
234
|
+
|
235
|
+
before do
|
236
|
+
empty_data_store
|
237
|
+
end
|
238
|
+
|
239
|
+
describe 'normal fetch' do
|
240
|
+
context 'when value is not found, or it has expired, or its version mismatched' do
|
241
|
+
it 'writes and returns the value from the given block' do
|
242
|
+
[
|
243
|
+
['other/key', {}], # not found
|
244
|
+
['some/key', { value: 'some existing value', version: '1', expires_in: -1 }], # expired
|
245
|
+
['some/key', { value: 'some existing value', version: '2', expires_in: 60 }], # mismatched
|
246
|
+
].each do |key, attributes|
|
247
|
+
create_entry(key, attributes)
|
248
|
+
|
249
|
+
value = described_class.new.fetch('some/key', { version: '1', expires_in: 300 }) do
|
250
|
+
'some new value from the block'
|
251
|
+
end
|
252
|
+
|
253
|
+
expect(value).to eq('some new value from the block')
|
254
|
+
|
255
|
+
expect(find_entry('some/key')).to eq(
|
256
|
+
build_entry({
|
257
|
+
value: 'some new value from the block',
|
258
|
+
version: '1',
|
259
|
+
expires_in: 300,
|
260
|
+
create_at: current_unix_time
|
261
|
+
})
|
262
|
+
)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
context 'when value from the given block is nil, and default skip_nil is true' do
|
267
|
+
it 'does not write the new nil value, and returns nil' do
|
268
|
+
thread_cache = described_class.new({ skip_nil: true })
|
269
|
+
|
270
|
+
options = { version: '1', expires_in: 300 }
|
271
|
+
value = thread_cache.fetch('some/key', options) do
|
272
|
+
nil
|
273
|
+
end
|
274
|
+
|
275
|
+
expect(value).to eq(nil)
|
276
|
+
expect(find_entry('some/key')).to be_nil
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
context 'when value from the given block is nil, and options skip_nil is true' do
|
281
|
+
it 'does not write the new nil value, and returns nil' do
|
282
|
+
thread_cache = described_class.new({ skip_nil: false })
|
283
|
+
|
284
|
+
options = { version: '1', expires_in: 300, skip_nil: true }
|
285
|
+
value = thread_cache.fetch('some/key', options) do
|
286
|
+
nil
|
287
|
+
end
|
288
|
+
|
289
|
+
expect(value).to eq(nil)
|
290
|
+
expect(find_entry('some/key')).to be_nil
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
context 'when value is found, and it has not expired, and it does not mismatch' do
|
296
|
+
it 'does not write value from the block, and returns the existing value' do
|
297
|
+
create_entry('some/key', { value: 'some existing value', version: '1', expires_in: 60 })
|
298
|
+
|
299
|
+
value = described_class.new.fetch('some/key', { version: '1', expires_in: 300 }) do
|
300
|
+
'some new value from the block'
|
301
|
+
end
|
302
|
+
|
303
|
+
expect(value).to eq('some existing value')
|
304
|
+
|
305
|
+
expect(find_entry('some/key')).to eq(
|
306
|
+
build_entry({
|
307
|
+
value: 'some existing value',
|
308
|
+
version: '1',
|
309
|
+
expires_in: 60,
|
310
|
+
created_at: current_unix_time
|
311
|
+
})
|
312
|
+
)
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
describe 'forced fetch' do
|
318
|
+
it 'writes and returns the value from the given block' do
|
319
|
+
create_entry('some/key', { value: 'some existing value', version: '1', expires_in: 60 })
|
320
|
+
|
321
|
+
options = { force: true, version: '1', expires_in: 300 }
|
322
|
+
value = described_class.new.fetch('some/key', options) do
|
323
|
+
'some new value from the block'
|
324
|
+
end
|
325
|
+
|
326
|
+
expect(value).to eq('some new value from the block')
|
327
|
+
|
328
|
+
expect(find_entry('some/key')).to eq(
|
329
|
+
build_entry({
|
330
|
+
value: 'some new value from the block',
|
331
|
+
version: '1',
|
332
|
+
expires_in: 300,
|
333
|
+
create_at: current_unix_time
|
334
|
+
})
|
335
|
+
)
|
336
|
+
end
|
337
|
+
|
338
|
+
context 'when value from the given block is nil, and default skip_nil is true' do
|
339
|
+
it 'does not write the new nil value, and returns nil' do
|
340
|
+
thread_cache = described_class.new({ skip_nil: true })
|
341
|
+
|
342
|
+
options = { force: true, version: '1', expires_in: 300 }
|
343
|
+
value = thread_cache.fetch('some/key', options) do
|
344
|
+
nil
|
345
|
+
end
|
346
|
+
|
347
|
+
expect(value).to eq(nil)
|
348
|
+
expect(find_entry('some/key')).to be_nil
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
context 'when value from the given block is nil, and options skip_nil is true' do
|
353
|
+
it 'does not write the new nil value, and returns nil' do
|
354
|
+
thread_cache = described_class.new({ skip_nil: false })
|
355
|
+
|
356
|
+
options = { force: true, version: '1', expires_in: 300, skip_nil: true }
|
357
|
+
value = thread_cache.fetch('some/key', options) do
|
358
|
+
nil
|
359
|
+
end
|
360
|
+
|
361
|
+
expect(value).to eq(nil)
|
362
|
+
expect(find_entry('some/key')).to be_nil
|
363
|
+
end
|
364
|
+
end
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
describe '#delete' do
|
369
|
+
context 'when key exists' do
|
370
|
+
it 'deletes the key from the data store and returns true' do
|
371
|
+
create_entry('some/key', { value: 'some value' })
|
372
|
+
|
373
|
+
result = described_class.new.delete('some/key')
|
374
|
+
|
375
|
+
expect(result).to eq(true)
|
376
|
+
expect(find_entry('some/key')).to be_nil
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
context 'when key does not exist' do
|
381
|
+
it 'returns false' do
|
382
|
+
empty_data_store
|
383
|
+
|
384
|
+
result = described_class.new.delete('some/key')
|
385
|
+
|
386
|
+
expect(result).to eq(false)
|
387
|
+
end
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
describe '#write_multi' do
|
392
|
+
around do |spec|
|
393
|
+
travel_to(current_time, &spec)
|
394
|
+
end
|
395
|
+
|
396
|
+
before do
|
397
|
+
empty_data_store
|
398
|
+
end
|
399
|
+
|
400
|
+
context 'when options values are Hashes' do
|
401
|
+
it 'uses options values from Hashes' do
|
402
|
+
keys_and_values = {
|
403
|
+
'key1' => nil,
|
404
|
+
'key2' => 'value2',
|
405
|
+
'key3' => 'value3',
|
406
|
+
'key4' => 'value4',
|
407
|
+
}
|
408
|
+
options = {
|
409
|
+
version: {
|
410
|
+
'key1' => nil,
|
411
|
+
'key2' => '2',
|
412
|
+
'key3' => '3',
|
413
|
+
},
|
414
|
+
expires_in: {
|
415
|
+
'key1' => 60,
|
416
|
+
'key2' => 180,
|
417
|
+
'key3' => 240,
|
418
|
+
},
|
419
|
+
skip_nil: {
|
420
|
+
'key1' => true,
|
421
|
+
'key2' => true,
|
422
|
+
'key3' => false,
|
423
|
+
},
|
424
|
+
}
|
425
|
+
|
426
|
+
result = described_class.new.write_multi(keys_and_values, options)
|
427
|
+
|
428
|
+
expect(result).to eq(keys_and_values)
|
429
|
+
|
430
|
+
expect(data_store).to eq(
|
431
|
+
{
|
432
|
+
'key2' => build_entry({
|
433
|
+
value: 'value2',
|
434
|
+
version: '2',
|
435
|
+
expires_in: 180,
|
436
|
+
created_at: current_unix_time,
|
437
|
+
}),
|
438
|
+
'key3' => build_entry({
|
439
|
+
value: 'value3',
|
440
|
+
version: '3',
|
441
|
+
expires_in: 240,
|
442
|
+
created_at: current_unix_time,
|
443
|
+
}),
|
444
|
+
'key4' => build_entry({
|
445
|
+
value: 'value4',
|
446
|
+
version: nil,
|
447
|
+
expires_in: 60,
|
448
|
+
created_at: current_unix_time,
|
449
|
+
}),
|
450
|
+
}
|
451
|
+
)
|
452
|
+
end
|
453
|
+
end
|
454
|
+
|
455
|
+
context 'when options values are Arrays' do
|
456
|
+
it 'uses options values from Arrays' do
|
457
|
+
keys_and_values = {
|
458
|
+
'key1' => nil,
|
459
|
+
'key2' => 'value2',
|
460
|
+
'key3' => 'value3',
|
461
|
+
'key4' => 'value4',
|
462
|
+
}
|
463
|
+
options = {
|
464
|
+
version: [nil, '2', '3'],
|
465
|
+
expires_in: [60, 180, 240],
|
466
|
+
skip_nil: [true, true, false],
|
467
|
+
}
|
468
|
+
|
469
|
+
result = described_class.new.write_multi(keys_and_values, options)
|
470
|
+
|
471
|
+
expect(result).to eq(keys_and_values)
|
472
|
+
|
473
|
+
expect(data_store).to eq(
|
474
|
+
{
|
475
|
+
'key2' => build_entry({
|
476
|
+
value: 'value2',
|
477
|
+
version: '2',
|
478
|
+
expires_in: 180,
|
479
|
+
created_at: current_unix_time,
|
480
|
+
}),
|
481
|
+
'key3' => build_entry({
|
482
|
+
value: 'value3',
|
483
|
+
version: '3',
|
484
|
+
expires_in: 240,
|
485
|
+
created_at: current_unix_time,
|
486
|
+
}),
|
487
|
+
'key4' => build_entry({
|
488
|
+
value: 'value4',
|
489
|
+
version: nil,
|
490
|
+
expires_in: 60,
|
491
|
+
created_at: current_unix_time,
|
492
|
+
}),
|
493
|
+
}
|
494
|
+
)
|
495
|
+
end
|
496
|
+
end
|
497
|
+
|
498
|
+
context 'when options values are not Hashes nor Arrays' do
|
499
|
+
it 'uses the same options values for all keys' do
|
500
|
+
keys_and_values = {
|
501
|
+
'key1' => nil,
|
502
|
+
'key2' => 'value2',
|
503
|
+
'key3' => 'value3',
|
504
|
+
'key4' => 'value4',
|
505
|
+
}
|
506
|
+
options = {
|
507
|
+
version: '1',
|
508
|
+
expires_in: 180,
|
509
|
+
skip_nil: true,
|
510
|
+
}
|
511
|
+
|
512
|
+
result = described_class.new.write_multi(keys_and_values, options)
|
513
|
+
|
514
|
+
expect(result).to eq(keys_and_values)
|
515
|
+
|
516
|
+
expect(data_store).to eq(
|
517
|
+
{
|
518
|
+
'key2' => build_entry({
|
519
|
+
value: 'value2',
|
520
|
+
version: '1',
|
521
|
+
expires_in: 180,
|
522
|
+
created_at: current_unix_time,
|
523
|
+
}),
|
524
|
+
'key3' => build_entry({
|
525
|
+
value: 'value3',
|
526
|
+
version: '1',
|
527
|
+
expires_in: 180,
|
528
|
+
created_at: current_unix_time,
|
529
|
+
}),
|
530
|
+
'key4' => build_entry({
|
531
|
+
value: 'value4',
|
532
|
+
version: '1',
|
533
|
+
expires_in: 180,
|
534
|
+
created_at: current_unix_time,
|
535
|
+
}),
|
536
|
+
}
|
537
|
+
)
|
538
|
+
end
|
539
|
+
end
|
540
|
+
end
|
541
|
+
|
542
|
+
describe '#read_multi' do
|
543
|
+
around do |spec|
|
544
|
+
travel_to(current_time, &spec)
|
545
|
+
end
|
546
|
+
|
547
|
+
before do
|
548
|
+
empty_data_store
|
549
|
+
end
|
550
|
+
|
551
|
+
context 'when options version is a Hash' do
|
552
|
+
it 'uses versions from the Hash' do
|
553
|
+
create_entry('key1', { value: 'value1', version: nil, expires_in: -1 }) # expired
|
554
|
+
create_entry('key2', { value: 'value2', version: '1', expires_in: 60 })
|
555
|
+
create_entry('key3', { value: 'value3', version: '1', expires_in: 60 }) # mismatched
|
556
|
+
create_entry('key4', { value: 'value4', version: '2', expires_in: -1 }) # mismatched and expired
|
557
|
+
create_entry('key5', { value: 'value5', version: '1', expires_in: 60 })
|
558
|
+
# and 'key6' is not found
|
559
|
+
|
560
|
+
keys = [
|
561
|
+
'key1',
|
562
|
+
'key2',
|
563
|
+
'key3',
|
564
|
+
'key4',
|
565
|
+
'key5',
|
566
|
+
'key6',
|
567
|
+
]
|
568
|
+
options = {
|
569
|
+
version: {
|
570
|
+
'key1' => nil,
|
571
|
+
'key2' => '1',
|
572
|
+
'key3' => '2',
|
573
|
+
'key4' => '2',
|
574
|
+
}
|
575
|
+
}
|
576
|
+
|
577
|
+
result = described_class.new.read_multi(keys, options)
|
578
|
+
|
579
|
+
expect(result).to eq({
|
580
|
+
'key1' => nil,
|
581
|
+
'key2' => 'value2',
|
582
|
+
'key3' => nil,
|
583
|
+
'key4' => nil,
|
584
|
+
'key5' => 'value5',
|
585
|
+
'key6' => nil,
|
586
|
+
})
|
587
|
+
end
|
588
|
+
end
|
589
|
+
|
590
|
+
context 'when options version is an Array' do
|
591
|
+
it 'uses versions from the Array in order' do
|
592
|
+
create_entry('key1', { value: 'value1', version: nil, expires_in: -1 }) # expired
|
593
|
+
create_entry('key2', { value: 'value2', version: '1', expires_in: 60 })
|
594
|
+
create_entry('key3', { value: 'value3', version: '1', expires_in: 60 }) # mismatched
|
595
|
+
create_entry('key4', { value: 'value4', version: '2', expires_in: -1 }) # mismatched and expired
|
596
|
+
create_entry('key5', { value: 'value5', version: '1', expires_in: 60 })
|
597
|
+
# and 'key6' is not found
|
598
|
+
|
599
|
+
keys = [
|
600
|
+
'key1',
|
601
|
+
'key2',
|
602
|
+
'key3',
|
603
|
+
'key4',
|
604
|
+
'key5',
|
605
|
+
'key6',
|
606
|
+
]
|
607
|
+
options = {
|
608
|
+
version: [nil, '1', '2', '2']
|
609
|
+
}
|
610
|
+
|
611
|
+
result = described_class.new.read_multi(keys, options)
|
612
|
+
|
613
|
+
expect(result).to eq({
|
614
|
+
'key1' => nil,
|
615
|
+
'key2' => 'value2',
|
616
|
+
'key3' => nil,
|
617
|
+
'key4' => nil,
|
618
|
+
'key5' => 'value5',
|
619
|
+
'key6' => nil,
|
620
|
+
})
|
621
|
+
end
|
622
|
+
end
|
623
|
+
|
624
|
+
context 'when options version is not a Hash nor an Array' do
|
625
|
+
it 'uses the same version for all keys' do
|
626
|
+
create_entry('key1', { value: 'value1', version: nil, expires_in: -1 }) # expired
|
627
|
+
create_entry('key2', { value: 'value2', version: '1', expires_in: 60 }) # mismatched
|
628
|
+
create_entry('key3', { value: 'value3', version: '1', expires_in: -1 }) # mismatched and expired
|
629
|
+
create_entry('key4', { value: 'value4', version: '2', expires_in: 60 })
|
630
|
+
create_entry('key5', { value: 'value5', version: nil, expires_in: 60 })
|
631
|
+
# and 'key6' is not found
|
632
|
+
|
633
|
+
keys = [
|
634
|
+
'key1',
|
635
|
+
'key2',
|
636
|
+
'key3',
|
637
|
+
'key4',
|
638
|
+
'key5',
|
639
|
+
'key6',
|
640
|
+
]
|
641
|
+
options = {
|
642
|
+
version: '2'
|
643
|
+
}
|
644
|
+
|
645
|
+
result = described_class.new.read_multi(keys, options)
|
646
|
+
|
647
|
+
expect(result).to eq({
|
648
|
+
'key1' => nil,
|
649
|
+
'key2' => nil,
|
650
|
+
'key3' => nil,
|
651
|
+
'key4' => 'value4',
|
652
|
+
'key5' => 'value5',
|
653
|
+
'key6' => nil,
|
654
|
+
})
|
655
|
+
end
|
656
|
+
end
|
657
|
+
end
|
658
|
+
|
659
|
+
describe '#fetch_multi' do
|
660
|
+
before do
|
661
|
+
empty_data_store
|
662
|
+
end
|
663
|
+
|
664
|
+
context 'when options values are Hashes' do
|
665
|
+
it 'uses options values from Hashes' do
|
666
|
+
# expired
|
667
|
+
create_entry('key1', { value: 'value1', version: nil, expires_in: -1, created_at: current_unix_time })
|
668
|
+
# mismatched
|
669
|
+
create_entry('key2', { value: 'value2', version: '1', expires_in: 300, created_at: current_unix_time })
|
670
|
+
# expired and mismatched
|
671
|
+
create_entry('key3', { value: 'value3', version: '1', expires_in: -1, created_at: current_unix_time })
|
672
|
+
|
673
|
+
create_entry('key4', { value: 'value4', version: '2', expires_in: 300, created_at: current_unix_time })
|
674
|
+
create_entry('key5', { value: 'value5', version: nil, expires_in: 300, created_at: current_unix_time })
|
675
|
+
# and 'key6' is not found
|
676
|
+
|
677
|
+
keys = [
|
678
|
+
'key1',
|
679
|
+
'key2',
|
680
|
+
'key3',
|
681
|
+
'key4',
|
682
|
+
'key5',
|
683
|
+
'key6',
|
684
|
+
]
|
685
|
+
options = {
|
686
|
+
version: {
|
687
|
+
'key1' => nil,
|
688
|
+
'key2' => '2',
|
689
|
+
'key3' => '2',
|
690
|
+
'key4' => '2',
|
691
|
+
},
|
692
|
+
expires_in: {
|
693
|
+
'key1' => 60,
|
694
|
+
'key2' => 180,
|
695
|
+
'key3' => 240,
|
696
|
+
'key4' => 300,
|
697
|
+
},
|
698
|
+
skip_nil: {
|
699
|
+
'key1' => false,
|
700
|
+
'key2' => false,
|
701
|
+
'key3' => true,
|
702
|
+
'key4' => false,
|
703
|
+
},
|
704
|
+
}
|
705
|
+
|
706
|
+
result = {}
|
707
|
+
travel_to(current_time + 100) do
|
708
|
+
result = described_class.new.fetch_multi(keys, options) do |key|
|
709
|
+
if key == 'key3'
|
710
|
+
nil
|
711
|
+
else
|
712
|
+
"new value for #{key}"
|
713
|
+
end
|
714
|
+
end
|
715
|
+
end
|
716
|
+
|
717
|
+
expect(result).to eq({
|
718
|
+
'key1' => 'new value for key1',
|
719
|
+
'key2' => 'new value for key2',
|
720
|
+
'key3' => nil,
|
721
|
+
'key4' => 'value4',
|
722
|
+
'key5' => 'value5',
|
723
|
+
'key6' => 'new value for key6',
|
724
|
+
})
|
725
|
+
|
726
|
+
expect(data_store).to eq(
|
727
|
+
{
|
728
|
+
'key1' => build_entry({
|
729
|
+
value: 'new value for key1',
|
730
|
+
version: nil,
|
731
|
+
expires_in: 60,
|
732
|
+
created_at: current_unix_time + 100,
|
733
|
+
}),
|
734
|
+
'key2' => build_entry({
|
735
|
+
value: 'new value for key2',
|
736
|
+
version: '2',
|
737
|
+
expires_in: 180,
|
738
|
+
created_at: current_unix_time + 100,
|
739
|
+
}),
|
740
|
+
# no entry for 'key3' entry
|
741
|
+
'key4' => build_entry({
|
742
|
+
value: 'value4',
|
743
|
+
version: '2',
|
744
|
+
expires_in: 300,
|
745
|
+
created_at: current_unix_time,
|
746
|
+
}),
|
747
|
+
'key5' => build_entry({
|
748
|
+
value: 'value5',
|
749
|
+
version: nil,
|
750
|
+
expires_in: 300,
|
751
|
+
created_at: current_unix_time,
|
752
|
+
}),
|
753
|
+
'key6' => build_entry({
|
754
|
+
value: 'new value for key6',
|
755
|
+
version: nil,
|
756
|
+
expires_in: 60,
|
757
|
+
created_at: current_unix_time + 100,
|
758
|
+
}),
|
759
|
+
}
|
760
|
+
)
|
761
|
+
end
|
762
|
+
end
|
763
|
+
|
764
|
+
context 'when options values are Arrays' do
|
765
|
+
it 'uses options values from Arrays' do
|
766
|
+
# expired
|
767
|
+
create_entry('key1', { value: 'value1', version: nil, expires_in: -1, created_at: current_unix_time })
|
768
|
+
# mismatched
|
769
|
+
create_entry('key2', { value: 'value2', version: '1', expires_in: 300, created_at: current_unix_time })
|
770
|
+
# expired and mismatched
|
771
|
+
create_entry('key3', { value: 'value3', version: '1', expires_in: -1, created_at: current_unix_time })
|
772
|
+
|
773
|
+
create_entry('key4', { value: 'value4', version: '2', expires_in: 300, created_at: current_unix_time })
|
774
|
+
create_entry('key5', { value: 'value5', version: nil, expires_in: 300, created_at: current_unix_time })
|
775
|
+
# and 'key6' is not found
|
776
|
+
|
777
|
+
keys = [
|
778
|
+
'key1',
|
779
|
+
'key2',
|
780
|
+
'key3',
|
781
|
+
'key4',
|
782
|
+
'key5',
|
783
|
+
'key6',
|
784
|
+
]
|
785
|
+
options = {
|
786
|
+
version: [nil, '2', '2', '2'],
|
787
|
+
expires_in: [60, 180, 240, 300],
|
788
|
+
skip_nil: [false, false, true, false],
|
789
|
+
}
|
790
|
+
|
791
|
+
result = {}
|
792
|
+
travel_to(current_time + 100) do
|
793
|
+
result = described_class.new.fetch_multi(keys, options) do |key|
|
794
|
+
if key == 'key3'
|
795
|
+
nil
|
796
|
+
else
|
797
|
+
"new value for #{key}"
|
798
|
+
end
|
799
|
+
end
|
800
|
+
end
|
801
|
+
|
802
|
+
expect(result).to eq({
|
803
|
+
'key1' => 'new value for key1',
|
804
|
+
'key2' => 'new value for key2',
|
805
|
+
'key3' => nil,
|
806
|
+
'key4' => 'value4',
|
807
|
+
'key5' => 'value5',
|
808
|
+
'key6' => 'new value for key6',
|
809
|
+
})
|
810
|
+
|
811
|
+
expect(data_store).to eq(
|
812
|
+
{
|
813
|
+
'key1' => build_entry({
|
814
|
+
value: 'new value for key1',
|
815
|
+
version: nil,
|
816
|
+
expires_in: 60,
|
817
|
+
created_at: current_unix_time + 100,
|
818
|
+
}),
|
819
|
+
'key2' => build_entry({
|
820
|
+
value: 'new value for key2',
|
821
|
+
version: '2',
|
822
|
+
expires_in: 180,
|
823
|
+
created_at: current_unix_time + 100,
|
824
|
+
}),
|
825
|
+
# no entry for 'key3' entry
|
826
|
+
'key4' => build_entry({
|
827
|
+
value: 'value4',
|
828
|
+
version: '2',
|
829
|
+
expires_in: 300,
|
830
|
+
created_at: current_unix_time,
|
831
|
+
}),
|
832
|
+
'key5' => build_entry({
|
833
|
+
value: 'value5',
|
834
|
+
version: nil,
|
835
|
+
expires_in: 300,
|
836
|
+
created_at: current_unix_time,
|
837
|
+
}),
|
838
|
+
'key6' => build_entry({
|
839
|
+
value: 'new value for key6',
|
840
|
+
version: nil,
|
841
|
+
expires_in: 60,
|
842
|
+
created_at: current_unix_time + 100,
|
843
|
+
}),
|
844
|
+
}
|
845
|
+
)
|
846
|
+
end
|
847
|
+
end
|
848
|
+
|
849
|
+
context 'when options values are not Hashes nor Arrays' do
|
850
|
+
it 'uses the same options values for all keys' do
|
851
|
+
# expired
|
852
|
+
create_entry('key1', { value: 'value1', version: '2', expires_in: -1, created_at: current_unix_time })
|
853
|
+
# mismatched
|
854
|
+
create_entry('key2', { value: 'value2', version: '1', expires_in: 300, created_at: current_unix_time })
|
855
|
+
# expired and mismatched
|
856
|
+
create_entry('key3', { value: 'value3', version: '1', expires_in: -1, created_at: current_unix_time })
|
857
|
+
|
858
|
+
create_entry('key4', { value: 'value4', version: '2', expires_in: 300, created_at: current_unix_time })
|
859
|
+
create_entry('key5', { value: 'value5', version: nil, expires_in: 300, created_at: current_unix_time })
|
860
|
+
|
861
|
+
# and 'key6' is not found
|
862
|
+
|
863
|
+
keys = [
|
864
|
+
'key1',
|
865
|
+
'key2',
|
866
|
+
'key3',
|
867
|
+
'key4',
|
868
|
+
'key5',
|
869
|
+
'key6',
|
870
|
+
]
|
871
|
+
options = {
|
872
|
+
version: '2',
|
873
|
+
expires_in: 180,
|
874
|
+
skip_nil: true,
|
875
|
+
}
|
876
|
+
|
877
|
+
result = {}
|
878
|
+
travel_to(current_time + 100) do
|
879
|
+
result = described_class.new.fetch_multi(keys, options) do |key|
|
880
|
+
if key == 'key3'
|
881
|
+
nil
|
882
|
+
else
|
883
|
+
"new value for #{key}"
|
884
|
+
end
|
885
|
+
end
|
886
|
+
end
|
887
|
+
|
888
|
+
expect(result).to eq({
|
889
|
+
'key1' => 'new value for key1',
|
890
|
+
'key2' => 'new value for key2',
|
891
|
+
'key3' => nil,
|
892
|
+
'key4' => 'value4',
|
893
|
+
'key5' => 'value5',
|
894
|
+
'key6' => 'new value for key6',
|
895
|
+
})
|
896
|
+
|
897
|
+
expect(data_store).to eq(
|
898
|
+
{
|
899
|
+
'key1' => build_entry({
|
900
|
+
value: 'new value for key1',
|
901
|
+
version: '2',
|
902
|
+
expires_in: 180,
|
903
|
+
created_at: current_unix_time + 100,
|
904
|
+
}),
|
905
|
+
'key2' => build_entry({
|
906
|
+
value: 'new value for key2',
|
907
|
+
version: '2',
|
908
|
+
expires_in: 180,
|
909
|
+
created_at: current_unix_time + 100,
|
910
|
+
}),
|
911
|
+
# no entry for 'key3' entry
|
912
|
+
'key4' => build_entry({
|
913
|
+
value: 'value4',
|
914
|
+
version: '2',
|
915
|
+
expires_in: 300,
|
916
|
+
created_at: current_unix_time,
|
917
|
+
}),
|
918
|
+
'key5' => build_entry({
|
919
|
+
value: 'value5',
|
920
|
+
version: nil,
|
921
|
+
expires_in: 300,
|
922
|
+
created_at: current_unix_time,
|
923
|
+
}),
|
924
|
+
'key6' => build_entry({
|
925
|
+
value: 'new value for key6',
|
926
|
+
version: '2',
|
927
|
+
expires_in: 180,
|
928
|
+
created_at: current_unix_time + 100,
|
929
|
+
}),
|
930
|
+
}
|
931
|
+
)
|
932
|
+
end
|
933
|
+
end
|
934
|
+
end
|
935
|
+
|
936
|
+
describe '#delete_multi' do
|
937
|
+
it 'deletes the keys from the data store and returns an Array with true/false for each key' do
|
938
|
+
empty_data_store
|
939
|
+
|
940
|
+
create_entry('key1', { value: 'value1' })
|
941
|
+
create_entry('key2', { value: 'value2' })
|
942
|
+
|
943
|
+
result = described_class.new.delete_multi(['key1', 'some_nonexistent/key', 'key2'])
|
944
|
+
|
945
|
+
expect(result).to eq([true, false, true])
|
946
|
+
|
947
|
+
expect(find_entry('key1')).to be_nil
|
948
|
+
expect(find_entry('key2')).to be_nil
|
949
|
+
end
|
950
|
+
end
|
951
|
+
|
952
|
+
describe '#delete_matched' do
|
953
|
+
it 'deletes the keys that match the given pattern and returns a list of deleted keys' do
|
954
|
+
empty_data_store
|
955
|
+
|
956
|
+
create_entry('key1', { value: 'value1' })
|
957
|
+
create_entry('key2', { value: 'value2' })
|
958
|
+
create_entry('other/key', { value: 'other value' })
|
959
|
+
|
960
|
+
result = described_class.new.delete_matched(/key[0-9]/)
|
961
|
+
|
962
|
+
expect(result).to eq(['key1', 'key2'])
|
963
|
+
|
964
|
+
expect(find_entry('key1')).to be_nil
|
965
|
+
expect(find_entry('key2')).to be_nil
|
966
|
+
expect(find_entry('other/key')).not_to be_nil
|
967
|
+
end
|
968
|
+
end
|
969
|
+
|
970
|
+
describe '#cleanup' do
|
971
|
+
around do |spec|
|
972
|
+
travel_to(current_time, &spec)
|
973
|
+
end
|
974
|
+
|
975
|
+
it 'deletes from the data store all invalid entries and returns a list of deleted keys' do
|
976
|
+
empty_data_store
|
977
|
+
|
978
|
+
create_entry('key1', { value: 'value1', version: nil, expires_in: 300 })
|
979
|
+
create_entry('key2', { value: 'value2', version: '1', expires_in: 300 })
|
980
|
+
create_entry('key3', { value: 'value3', version: '2', expires_in: -1 })
|
981
|
+
create_entry('key4', { value: 'value4', version: '1', expires_in: -1 })
|
982
|
+
create_entry('key5', { value: 'value5', version: '2', expires_in: 300 })
|
983
|
+
|
984
|
+
result = described_class.new.cleanup({ version: '2' })
|
985
|
+
|
986
|
+
expect(result).to eq(['key2', 'key3', 'key4'])
|
987
|
+
|
988
|
+
expect(find_entry('key1')).to_not be_nil
|
989
|
+
expect(find_entry('key2')).to be_nil
|
990
|
+
expect(find_entry('key3')).to be_nil
|
991
|
+
expect(find_entry('key4')).to be_nil
|
992
|
+
expect(find_entry('key5')).to_not be_nil
|
993
|
+
end
|
994
|
+
end
|
995
|
+
|
996
|
+
describe '#increment' do
|
997
|
+
around do |spec|
|
998
|
+
travel_to(current_time, &spec)
|
999
|
+
end
|
1000
|
+
|
1001
|
+
before do
|
1002
|
+
empty_data_store
|
1003
|
+
end
|
1004
|
+
|
1005
|
+
it 'increments value by 1' do
|
1006
|
+
thread_cache = described_class.new
|
1007
|
+
|
1008
|
+
thread_cache.increment('some/key')
|
1009
|
+
expect(find_entry('some/key')[:value]).to eq(1)
|
1010
|
+
|
1011
|
+
thread_cache.increment('some/key')
|
1012
|
+
thread_cache.increment('some/key')
|
1013
|
+
expect(find_entry('some/key')[:value]).to eq(3)
|
1014
|
+
|
1015
|
+
thread_cache.increment('some/key')
|
1016
|
+
expect(find_entry('some/key')[:value]).to eq(4)
|
1017
|
+
end
|
1018
|
+
|
1019
|
+
it 'increments value by the given amount' do
|
1020
|
+
thread_cache = described_class.new
|
1021
|
+
|
1022
|
+
thread_cache.increment('some/key', 2)
|
1023
|
+
expect(find_entry('some/key')[:value]).to eq(2)
|
1024
|
+
|
1025
|
+
thread_cache.increment('some/key', 5)
|
1026
|
+
thread_cache.increment('some/key', -1)
|
1027
|
+
expect(find_entry('some/key')[:value]).to eq(6)
|
1028
|
+
|
1029
|
+
thread_cache.increment('some/key', 2)
|
1030
|
+
expect(find_entry('some/key')[:value]).to eq(8)
|
1031
|
+
end
|
1032
|
+
|
1033
|
+
it 'passes options down' do
|
1034
|
+
create_entry('some/key', { value: 5, version: '1', expires_in: 60 })
|
1035
|
+
|
1036
|
+
thread_cache = described_class.new
|
1037
|
+
|
1038
|
+
# matched
|
1039
|
+
thread_cache.increment('some/key', 5, { version: '1' })
|
1040
|
+
expect(find_entry('some/key')[:value]).to eq(10)
|
1041
|
+
|
1042
|
+
# mismatched
|
1043
|
+
thread_cache.increment('some/key', 1, { version: '2' })
|
1044
|
+
expect(find_entry('some/key')[:value]).to eq(1)
|
1045
|
+
expect(find_entry('some/key')[:version]).to eq('2')
|
1046
|
+
|
1047
|
+
# matched
|
1048
|
+
thread_cache.increment('some/key', 1, { version: '2' })
|
1049
|
+
expect(find_entry('some/key')[:value]).to eq(2)
|
1050
|
+
|
1051
|
+
# mismatched
|
1052
|
+
thread_cache.increment('some/key', 1, { version: '3', expires_in: -1 })
|
1053
|
+
expect(find_entry('some/key')[:value]).to eq(1)
|
1054
|
+
expect(find_entry('some/key')[:expires_in]).to eq(-1)
|
1055
|
+
|
1056
|
+
# matched, but it has expired; set default expires_in
|
1057
|
+
thread_cache.increment('some/key', 1, { version: '3' })
|
1058
|
+
expect(find_entry('some/key')[:value]).to eq(1)
|
1059
|
+
expect(find_entry('some/key')[:expires_in]).to eq(60)
|
1060
|
+
|
1061
|
+
# mismatched; set expires_in
|
1062
|
+
thread_cache.increment('some/key', 1, { version: '4', expires_in: 300 })
|
1063
|
+
expect(find_entry('some/key')[:value]).to eq(1)
|
1064
|
+
expect(find_entry('some/key')[:expires_in]).to eq(300)
|
1065
|
+
end
|
1066
|
+
end
|
1067
|
+
|
1068
|
+
describe '#decrement' do
|
1069
|
+
before do
|
1070
|
+
empty_data_store
|
1071
|
+
end
|
1072
|
+
|
1073
|
+
it 'decrements value by 1' do
|
1074
|
+
thread_cache = described_class.new
|
1075
|
+
|
1076
|
+
thread_cache.decrement('some/key')
|
1077
|
+
expect(find_entry('some/key')[:value]).to eq(-1)
|
1078
|
+
|
1079
|
+
thread_cache.decrement('some/key')
|
1080
|
+
thread_cache.decrement('some/key')
|
1081
|
+
expect(find_entry('some/key')[:value]).to eq(-3)
|
1082
|
+
|
1083
|
+
thread_cache.decrement('some/key')
|
1084
|
+
expect(find_entry('some/key')[:value]).to eq(-4)
|
1085
|
+
end
|
1086
|
+
|
1087
|
+
it 'decrements value by the given amount' do
|
1088
|
+
thread_cache = described_class.new
|
1089
|
+
|
1090
|
+
thread_cache.decrement('some/key', 2)
|
1091
|
+
expect(find_entry('some/key')[:value]).to eq(-2)
|
1092
|
+
|
1093
|
+
thread_cache.decrement('some/key', -5)
|
1094
|
+
thread_cache.decrement('some/key', 1)
|
1095
|
+
expect(find_entry('some/key')[:value]).to eq(2)
|
1096
|
+
|
1097
|
+
thread_cache.decrement('some/key', 2)
|
1098
|
+
expect(find_entry('some/key')[:value]).to eq(0)
|
1099
|
+
end
|
1100
|
+
|
1101
|
+
it 'passes options down' do
|
1102
|
+
create_entry('some/key', { value: 10, version: '1', expires_in: 60 })
|
1103
|
+
|
1104
|
+
thread_cache = described_class.new
|
1105
|
+
|
1106
|
+
# matched
|
1107
|
+
thread_cache.decrement('some/key', 5, { version: '1' })
|
1108
|
+
expect(find_entry('some/key')[:value]).to eq(5)
|
1109
|
+
|
1110
|
+
# mismatched
|
1111
|
+
thread_cache.decrement('some/key', 1, { version: '2' })
|
1112
|
+
expect(find_entry('some/key')[:value]).to eq(-1)
|
1113
|
+
expect(find_entry('some/key')[:version]).to eq('2')
|
1114
|
+
|
1115
|
+
# matched
|
1116
|
+
thread_cache.decrement('some/key', 1, { version: '2' })
|
1117
|
+
expect(find_entry('some/key')[:value]).to eq(-2)
|
1118
|
+
|
1119
|
+
# mismatched
|
1120
|
+
thread_cache.decrement('some/key', 1, { version: '3', expires_in: -1 })
|
1121
|
+
expect(find_entry('some/key')[:value]).to eq(-1)
|
1122
|
+
expect(find_entry('some/key')[:expires_in]).to eq(-1)
|
1123
|
+
|
1124
|
+
# matched but it has expired; set default expires_in
|
1125
|
+
thread_cache.decrement('some/key', 1, { version: '3' })
|
1126
|
+
expect(find_entry('some/key')[:value]).to eq(-1)
|
1127
|
+
expect(find_entry('some/key')[:expires_in]).to eq(60)
|
1128
|
+
|
1129
|
+
# mismatched; set expires_in
|
1130
|
+
thread_cache.decrement('some/key', 1, { version: '4', expires_in: 300 })
|
1131
|
+
expect(find_entry('some/key')[:value]).to eq(-1)
|
1132
|
+
expect(find_entry('some/key')[:expires_in]).to eq(300)
|
1133
|
+
end
|
1134
|
+
end
|
1135
|
+
|
1136
|
+
describe 'reading many times' do
|
1137
|
+
context 'with a single thread' do
|
1138
|
+
it 'behaves as expected' do
|
1139
|
+
thread_cache = described_class.new
|
1140
|
+
|
1141
|
+
travel_to(current_time) do
|
1142
|
+
thread_cache.write('key1', 'value1') # default_expires_in: 60
|
1143
|
+
thread_cache.write('key2', 'value2', { expires_in: 15 })
|
1144
|
+
|
1145
|
+
expect(thread_cache.read('key1')).to eq('value1')
|
1146
|
+
expect(thread_cache.read('key2')).to eq('value2')
|
1147
|
+
expect(thread_cache.read('some_nonexistent/key')).to eq(nil)
|
1148
|
+
end
|
1149
|
+
|
1150
|
+
travel_to(current_time + 15) do
|
1151
|
+
expect(thread_cache.read('key1')).to eq('value1')
|
1152
|
+
expect(thread_cache.read('key2')).to eq(nil) # expired
|
1153
|
+
expect(thread_cache.read('some_nonexistent/key')).to eq(nil)
|
1154
|
+
|
1155
|
+
thread_cache.write('key1', 'new value1', { expires_in: 20 })
|
1156
|
+
thread_cache.write('key2', 'new value2', { expires_in: 60, version: '1' })
|
1157
|
+
|
1158
|
+
expect(thread_cache.read('key1')).to eq('new value1')
|
1159
|
+
end
|
1160
|
+
|
1161
|
+
travel_to(current_time + 15 + 20) do
|
1162
|
+
expect(thread_cache.read('key1')).to eq(nil) # expired
|
1163
|
+
expect(thread_cache.read('key2')).to eq('new value2')
|
1164
|
+
expect(thread_cache.read('some_nonexistent/key')).to eq(nil)
|
1165
|
+
|
1166
|
+
expect(thread_cache.read('key2', { version: '2' })).to eq(nil) # mismatched
|
1167
|
+
expect(thread_cache.read('key2')).to eq(nil)
|
1168
|
+
end
|
1169
|
+
end
|
1170
|
+
end
|
1171
|
+
|
1172
|
+
context 'with multiple threads' do
|
1173
|
+
around do |spec|
|
1174
|
+
freeze_time(&spec)
|
1175
|
+
end
|
1176
|
+
|
1177
|
+
it 'behaves as expected, each thread with its own data store' do
|
1178
|
+
thread_cache = described_class.new
|
1179
|
+
thread_cache.write('some/key', 'some value for thread0')
|
1180
|
+
|
1181
|
+
threads = []
|
1182
|
+
|
1183
|
+
threads << Thread.new do
|
1184
|
+
thread_cache.write('some/key', 'some value for thread1')
|
1185
|
+
|
1186
|
+
threads << Thread.new do
|
1187
|
+
thread_cache.write('some/key', 'some value for thread2')
|
1188
|
+
|
1189
|
+
expect(thread_cache.read('some/key')).to eq('some value for thread2')
|
1190
|
+
end
|
1191
|
+
|
1192
|
+
expect(thread_cache.read('some/key')).to eq('some value for thread1')
|
1193
|
+
end
|
1194
|
+
|
1195
|
+
threads.each(&:join)
|
1196
|
+
|
1197
|
+
expect(thread_cache.read('some/key')).to eq('some value for thread0')
|
1198
|
+
end
|
1199
|
+
end
|
1200
|
+
end
|
1201
|
+
|
1202
|
+
# Helper methods
|
1203
|
+
|
1204
|
+
def nillify_data_store(namespace = 'thread_cache')
|
1205
|
+
Thread.current[namespace] = nil
|
1206
|
+
end
|
1207
|
+
|
1208
|
+
def empty_data_store(namespace = 'thread_cache')
|
1209
|
+
Thread.current[namespace] = {}
|
1210
|
+
end
|
1211
|
+
|
1212
|
+
def init_data_store(namespace = 'thread_cache')
|
1213
|
+
Thread.current[namespace] ||= {}
|
1214
|
+
end
|
1215
|
+
|
1216
|
+
def data_store(namespace = 'thread_cache')
|
1217
|
+
Thread.current[namespace]
|
1218
|
+
end
|
1219
|
+
|
1220
|
+
def build_entry(attributes = {})
|
1221
|
+
{
|
1222
|
+
value: attributes[:value],
|
1223
|
+
version: attributes[:version],
|
1224
|
+
expires_in: attributes[:expires_in],
|
1225
|
+
created_at: attributes[:created_at] || Time.now.to_f,
|
1226
|
+
}
|
1227
|
+
end
|
1228
|
+
|
1229
|
+
def create_entry(key, attributes = {}, thread_namespace = 'thread_cache')
|
1230
|
+
init_data_store(thread_namespace)
|
1231
|
+
|
1232
|
+
data_store(thread_namespace)[key] = build_entry(attributes)
|
1233
|
+
end
|
1234
|
+
|
1235
|
+
def find_entry(key, thread_namespace = 'thread_cache')
|
1236
|
+
init_data_store(thread_namespace)
|
1237
|
+
|
1238
|
+
data_store(thread_namespace)[key]
|
1239
|
+
end
|
1240
|
+
end
|