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.
@@ -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