zache 0.14.0 → 0.15.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 +4 -4
- data/lib/zache.rb +30 -17
- data/test/test_zache.rb +137 -123
- data/zache.gemspec +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f2c6bee2ddda59d3ddba9115b7f1eaf2037fe28cc217bec57aa1bc2dfb91b660
|
4
|
+
data.tar.gz: 3b1fafcbe3afd075992c0a36df45ca5b51228bd4d1d9c5e086c7523b462ef343
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 24f22827fe4b5ea546ea2e121b8aaf35c2646ecbcc4ec7134b0d27693bfa4345d29829d29db1d417ab7759c556f3c21c8cea151f267fdaa282d45d69b8e53a57
|
7
|
+
data.tar.gz: 2a8a4c661c2424a126c8b309825d8d558206db695359793191caf5c1b3e348a147381149258418e7e7469a3e9bfdab30f0164eedfe59383012ed8173fae5fbdf
|
data/lib/zache.rb
CHANGED
@@ -46,8 +46,9 @@ class Zache
|
|
46
46
|
end
|
47
47
|
|
48
48
|
# Always returns false.
|
49
|
+
# @param [Object] key Ignored
|
49
50
|
# @return [Boolean] Always returns false
|
50
|
-
def locked?
|
51
|
+
def locked?(_key)
|
51
52
|
false
|
52
53
|
end
|
53
54
|
|
@@ -89,6 +90,7 @@ class Zache
|
|
89
90
|
@sync = sync
|
90
91
|
@dirty = dirty
|
91
92
|
@mutex = Mutex.new
|
93
|
+
@locks = {}
|
92
94
|
end
|
93
95
|
|
94
96
|
# Total number of keys currently in cache.
|
@@ -120,18 +122,18 @@ class Zache
|
|
120
122
|
# @return [Object] The cached value
|
121
123
|
def get(key, lifetime: 2**32, dirty: false, placeholder: nil, eager: false, &block)
|
122
124
|
if block_given?
|
123
|
-
return @hash[key][:value] if (dirty || @dirty) && locked? && expired?(key) && @hash.key?(key)
|
125
|
+
return @hash[key][:value] if (dirty || @dirty) && locked?(key) && expired?(key) && @hash.key?(key)
|
124
126
|
if eager
|
125
127
|
return @hash[key][:value] if @hash.key?(key)
|
126
128
|
put(key, placeholder, lifetime: 0)
|
127
129
|
Thread.new do
|
128
|
-
|
130
|
+
synchronize_one(key) do
|
129
131
|
calc(key, lifetime, &block)
|
130
132
|
end
|
131
133
|
end
|
132
134
|
placeholder
|
133
135
|
else
|
134
|
-
|
136
|
+
synchronize_one(key) do
|
135
137
|
calc(key, lifetime, &block)
|
136
138
|
end
|
137
139
|
end
|
@@ -183,11 +185,12 @@ class Zache
|
|
183
185
|
rec.nil? ? Time.now : rec[:start]
|
184
186
|
end
|
185
187
|
|
186
|
-
# Is
|
188
|
+
# Is key currently locked doing something?
|
187
189
|
#
|
190
|
+
# @param [Object] key The key to check
|
188
191
|
# @return [Boolean] True if the cache is locked
|
189
|
-
def locked?
|
190
|
-
@
|
192
|
+
def locked?(key)
|
193
|
+
@locks[key]&.locked?
|
191
194
|
end
|
192
195
|
|
193
196
|
# Put a value into the cache.
|
@@ -197,7 +200,7 @@ class Zache
|
|
197
200
|
# @param lifetime [Integer] Time in seconds until the key expires (default: never expires)
|
198
201
|
# @return [Object] The value stored
|
199
202
|
def put(key, value, lifetime: 2**32)
|
200
|
-
|
203
|
+
synchronize_one(key) do
|
201
204
|
@hash[key] = {
|
202
205
|
value: value,
|
203
206
|
start: Time.now,
|
@@ -213,14 +216,14 @@ class Zache
|
|
213
216
|
# @yield Block to call if the key is not found
|
214
217
|
# @return [Object] The removed value or the result of the block
|
215
218
|
def remove(key)
|
216
|
-
|
219
|
+
synchronize_one(key) { @hash.delete(key) { yield if block_given? } }
|
217
220
|
end
|
218
221
|
|
219
222
|
# Remove all keys from the cache.
|
220
223
|
#
|
221
224
|
# @return [Hash] Empty hash
|
222
225
|
def remove_all
|
223
|
-
|
226
|
+
synchronize_all { @hash = {} }
|
224
227
|
end
|
225
228
|
|
226
229
|
# Remove all keys that match the block.
|
@@ -229,7 +232,7 @@ class Zache
|
|
229
232
|
# @yieldparam key [Object] The cache key to evaluate
|
230
233
|
# @return [Integer] Number of keys removed
|
231
234
|
def remove_by
|
232
|
-
|
235
|
+
synchronize_all do
|
233
236
|
count = 0
|
234
237
|
@hash.each_key do |k|
|
235
238
|
if yield(k)
|
@@ -246,7 +249,7 @@ class Zache
|
|
246
249
|
#
|
247
250
|
# @return [Integer] Number of keys removed
|
248
251
|
def clean
|
249
|
-
|
252
|
+
synchronize_all do
|
250
253
|
size_before = @hash.size
|
251
254
|
@hash.delete_if { |key, _value| expired?(key) }
|
252
255
|
size_before - @hash.size
|
@@ -284,11 +287,21 @@ class Zache
|
|
284
287
|
# @param block [Proc] The block to execute
|
285
288
|
# @yield The block to execute in a synchronized context
|
286
289
|
# @return [Object] The result of the block
|
287
|
-
def
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
290
|
+
def synchronize_all(&block)
|
291
|
+
return yield unless @sync
|
292
|
+
@mutex.synchronize(&block)
|
293
|
+
end
|
294
|
+
|
295
|
+
# Executes a block within a synchronized context if sync is enabled.
|
296
|
+
# @param key [Object] The object to sync
|
297
|
+
# @param block [Proc] The block to execute
|
298
|
+
# @yield The block to execute in a synchronized context
|
299
|
+
# @return [Object] The result of the block
|
300
|
+
def synchronize_one(key, &block)
|
301
|
+
return yield unless @sync
|
302
|
+
@mutex.synchronize do
|
303
|
+
@locks[key] ||= Mutex.new
|
292
304
|
end
|
305
|
+
@locks[key].synchronize(&block)
|
293
306
|
end
|
294
307
|
end
|
data/test/test_zache.rb
CHANGED
@@ -19,276 +19,290 @@ Thread.report_on_exception = true
|
|
19
19
|
# License:: MIT
|
20
20
|
class ZacheTest < Minitest::Test
|
21
21
|
def test_caches
|
22
|
-
|
23
|
-
first =
|
24
|
-
second =
|
22
|
+
z = Zache.new(sync: false)
|
23
|
+
first = z.get(:hey, lifetime: 5) { rand }
|
24
|
+
second = z.get(:hey) { rand }
|
25
25
|
assert_equal(first, second)
|
26
|
-
assert_equal(1,
|
26
|
+
assert_equal(1, z.size)
|
27
27
|
end
|
28
28
|
|
29
29
|
def test_caches_and_expires
|
30
|
-
|
31
|
-
first =
|
30
|
+
z = Zache.new
|
31
|
+
first = z.get(:hey, lifetime: 0.01) { rand }
|
32
32
|
sleep 0.1
|
33
|
-
second =
|
33
|
+
second = z.get(:hey) { rand }
|
34
34
|
refute_equal(first, second)
|
35
35
|
end
|
36
36
|
|
37
37
|
def test_calculates_age
|
38
|
-
|
39
|
-
|
38
|
+
z = Zache.new
|
39
|
+
z.get(:hey) { rand }
|
40
40
|
sleep 0.1
|
41
|
-
assert_operator(
|
41
|
+
assert_operator(z.mtime(:hey), :<, Time.now - 0.05)
|
42
42
|
end
|
43
43
|
|
44
44
|
def test_caches_in_threads
|
45
|
-
|
45
|
+
z = Zache.new
|
46
46
|
Threads.new(10).assert(100) do
|
47
|
-
|
47
|
+
z.get(:hey, lifetime: 0.0001) { rand }
|
48
48
|
end
|
49
49
|
end
|
50
50
|
|
51
51
|
def test_key_exists
|
52
|
-
|
53
|
-
|
54
|
-
exists_result =
|
55
|
-
not_exists_result =
|
52
|
+
z = Zache.new
|
53
|
+
z.get(:hey) { rand }
|
54
|
+
exists_result = z.exists?(:hey)
|
55
|
+
not_exists_result = z.exists?(:bye)
|
56
56
|
assert(exists_result)
|
57
57
|
refute(not_exists_result)
|
58
58
|
end
|
59
59
|
|
60
60
|
def test_put_and_exists
|
61
|
-
|
62
|
-
|
61
|
+
z = Zache.new
|
62
|
+
z.put(:hey, 'hello', lifetime: 0.1)
|
63
63
|
sleep 0.2
|
64
|
-
refute(
|
64
|
+
refute(z.exists?(:hey))
|
65
65
|
end
|
66
66
|
|
67
67
|
def test_remove_key
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
assert(
|
72
|
-
assert(
|
73
|
-
|
74
|
-
refute(
|
75
|
-
assert(
|
68
|
+
z = Zache.new
|
69
|
+
z.get(:hey) { rand }
|
70
|
+
z.get(:wey) { rand }
|
71
|
+
assert(z.exists?(:hey))
|
72
|
+
assert(z.exists?(:wey))
|
73
|
+
z.remove(:hey)
|
74
|
+
refute(z.exists?(:hey))
|
75
|
+
assert(z.exists?(:wey))
|
76
76
|
end
|
77
77
|
|
78
78
|
def test_remove_by_block
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
refute(
|
84
|
-
assert(
|
79
|
+
z = Zache.new
|
80
|
+
z.get('first') { rand }
|
81
|
+
z.get('second') { rand }
|
82
|
+
z.remove_by { |k| k == 'first' }
|
83
|
+
refute(z.exists?('first'))
|
84
|
+
assert(z.exists?('second'))
|
85
85
|
end
|
86
86
|
|
87
87
|
def test_remove_key_with_sync_false
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
assert(
|
92
|
-
assert(
|
93
|
-
|
94
|
-
refute(
|
95
|
-
assert(
|
88
|
+
z = Zache.new(sync: false)
|
89
|
+
z.get(:hey) { rand }
|
90
|
+
z.get(:wey) { rand }
|
91
|
+
assert(z.exists?(:hey))
|
92
|
+
assert(z.exists?(:wey))
|
93
|
+
z.remove(:hey)
|
94
|
+
refute(z.exists?(:hey))
|
95
|
+
assert(z.exists?(:wey))
|
96
96
|
end
|
97
97
|
|
98
98
|
def test_clean_with_threads
|
99
|
-
|
99
|
+
z = Zache.new
|
100
100
|
Threads.new(300).assert(3000) do
|
101
|
-
|
102
|
-
|
101
|
+
z.get(:hey) { rand }
|
102
|
+
z.get(:bye, lifetime: 0.01) { rand }
|
103
103
|
sleep 0.1
|
104
|
-
|
104
|
+
z.clean
|
105
105
|
end
|
106
|
-
assert(
|
107
|
-
refute(
|
106
|
+
assert(z.exists?(:hey))
|
107
|
+
refute(z.exists?(:bye))
|
108
108
|
end
|
109
109
|
|
110
110
|
def test_clean
|
111
|
-
|
112
|
-
|
113
|
-
|
111
|
+
z = Zache.new
|
112
|
+
z.get(:hey) { rand }
|
113
|
+
z.get(:bye, lifetime: 0.01) { rand }
|
114
114
|
sleep 0.1
|
115
|
-
|
116
|
-
assert(
|
117
|
-
refute(
|
115
|
+
z.clean
|
116
|
+
assert(z.exists?(:hey))
|
117
|
+
refute(z.exists?(:bye))
|
118
118
|
end
|
119
119
|
|
120
120
|
def test_clean_size
|
121
|
-
|
122
|
-
|
121
|
+
z = Zache.new
|
122
|
+
z.get(:hey, lifetime: 0.01) { rand }
|
123
123
|
sleep 0.1
|
124
|
-
|
125
|
-
assert_empty(
|
124
|
+
z.clean
|
125
|
+
assert_empty(z)
|
126
126
|
end
|
127
127
|
|
128
128
|
def test_clean_with_sync_false
|
129
|
-
|
130
|
-
|
131
|
-
|
129
|
+
z = Zache.new(sync: false)
|
130
|
+
z.get(:hey) { rand }
|
131
|
+
z.get(:bye, lifetime: 0.01) { rand }
|
132
132
|
sleep 0.1
|
133
|
-
|
134
|
-
assert(
|
135
|
-
refute(
|
133
|
+
z.clean
|
134
|
+
assert(z.exists?(:hey))
|
135
|
+
refute(z.exists?(:bye))
|
136
136
|
end
|
137
137
|
|
138
138
|
def test_remove_absent_key
|
139
|
-
|
140
|
-
|
139
|
+
z = Zache.new
|
140
|
+
z.remove(:hey)
|
141
141
|
end
|
142
142
|
|
143
143
|
def test_check_and_remove
|
144
|
-
|
145
|
-
|
146
|
-
refute(
|
144
|
+
z = Zache.new
|
145
|
+
z.get(:hey, lifetime: 0) { rand }
|
146
|
+
refute(z.exists?(:hey))
|
147
147
|
end
|
148
148
|
|
149
149
|
def test_remove_all_with_threads
|
150
|
-
|
150
|
+
z = Zache.new
|
151
151
|
Threads.new(10).assert(100) do |i|
|
152
|
-
|
153
|
-
assert(
|
154
|
-
|
152
|
+
z.get(:"hey#{i}") { rand }
|
153
|
+
assert(z.exists?(:"hey#{i}"))
|
154
|
+
z.remove_all
|
155
155
|
end
|
156
156
|
10.times do |i|
|
157
|
-
refute(
|
157
|
+
refute(z.exists?(:"hey#{i}"))
|
158
158
|
end
|
159
159
|
end
|
160
160
|
|
161
161
|
def test_remove_all_with_sync
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
refute(
|
167
|
-
refute(
|
162
|
+
z = Zache.new
|
163
|
+
z.get(:hey) { rand }
|
164
|
+
z.get(:bye) { rand }
|
165
|
+
z.remove_all
|
166
|
+
refute(z.exists?(:hey))
|
167
|
+
refute(z.exists?(:bye))
|
168
168
|
end
|
169
169
|
|
170
170
|
def test_remove_all_without_sync
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
refute(
|
176
|
-
refute(
|
171
|
+
z = Zache.new(sync: false)
|
172
|
+
z.get(:hey) { rand }
|
173
|
+
z.get(:bye) { rand }
|
174
|
+
z.remove_all
|
175
|
+
refute(z.exists?(:hey))
|
176
|
+
refute(z.exists?(:bye))
|
177
177
|
end
|
178
178
|
|
179
179
|
def test_puts_something_in
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
assert_equal(123,
|
180
|
+
z = Zache.new(sync: false)
|
181
|
+
z.get(:hey) { rand }
|
182
|
+
z.put(:hey, 123)
|
183
|
+
assert_equal(123, z.get(:hey))
|
184
184
|
end
|
185
185
|
|
186
186
|
def test_sync_zache_is_not_reentrant
|
187
|
-
|
187
|
+
z = Zache.new
|
188
188
|
assert_raises ThreadError do
|
189
|
-
|
189
|
+
z.get(:first) { z.get(:first) { 1 } }
|
190
190
|
end
|
191
191
|
end
|
192
192
|
|
193
|
+
def test_sync_zache_is_reentrant_for_different_keys
|
194
|
+
z = Zache.new
|
195
|
+
z.get(:first) { z.get(:second) { 1 } }
|
196
|
+
end
|
197
|
+
|
193
198
|
def test_calculates_only_once
|
194
|
-
|
199
|
+
z = Zache.new
|
195
200
|
long = Thread.start do
|
196
|
-
|
201
|
+
z.get(:x) do
|
197
202
|
sleep 0.5
|
198
203
|
'first'
|
199
204
|
end
|
200
205
|
end
|
201
206
|
sleep 0.1
|
202
|
-
|
203
|
-
|
204
|
-
|
207
|
+
assert(z.locked?(:x))
|
208
|
+
z.get(:x) { 'second' }
|
209
|
+
refute(z.locked?(:x))
|
205
210
|
long.kill
|
206
211
|
end
|
207
212
|
|
208
213
|
def test_checks_locked_status_from_inside
|
209
|
-
|
210
|
-
|
211
|
-
|
214
|
+
z = Zache.new
|
215
|
+
z.get(:x) do
|
216
|
+
assert(z.locked?(:x))
|
212
217
|
'done'
|
213
218
|
end
|
214
|
-
|
219
|
+
refute(z.locked?(:x))
|
215
220
|
end
|
216
221
|
|
217
222
|
def test_returns_dirty_result
|
218
|
-
|
219
|
-
|
223
|
+
z = Zache.new(dirty: true)
|
224
|
+
z.get(:x, lifetime: 0) { 1 }
|
220
225
|
long = Thread.start do
|
221
|
-
|
226
|
+
z.get(:x) do
|
222
227
|
sleep 1000
|
223
228
|
2
|
224
229
|
end
|
225
230
|
end
|
226
231
|
sleep 0.1
|
227
232
|
Timeout.timeout(1) do
|
228
|
-
assert(
|
229
|
-
assert(
|
230
|
-
assert_equal(1,
|
231
|
-
assert_equal(1,
|
233
|
+
assert(z.exists?(:x))
|
234
|
+
assert(z.expired?(:x))
|
235
|
+
assert_equal(1, z.get(:x))
|
236
|
+
assert_equal(1, z.get(:x) { 2 })
|
232
237
|
end
|
233
238
|
long.kill
|
234
239
|
end
|
235
240
|
|
236
241
|
def test_returns_dirty_result_when_not_locked
|
237
|
-
|
238
|
-
|
239
|
-
assert(
|
240
|
-
assert_equal(1,
|
241
|
-
assert_equal(2,
|
242
|
+
z = Zache.new(dirty: true)
|
243
|
+
z.get(:x, lifetime: 0) { 1 }
|
244
|
+
assert(z.exists?(:x))
|
245
|
+
assert_equal(1, z.get(:x))
|
246
|
+
assert_equal(2, z.get(:x) { 2 })
|
242
247
|
end
|
243
248
|
|
244
249
|
def test_fetches_multiple_keys_in_many_threads_in_dirty_mode
|
245
|
-
|
250
|
+
z = Zache.new(dirty: true)
|
246
251
|
set = Concurrent::Set.new
|
247
252
|
threads = 50
|
248
253
|
barrier = Concurrent::CyclicBarrier.new(threads)
|
249
254
|
Threads.new(threads).assert(threads * 2) do |i|
|
250
255
|
barrier.wait if i < threads
|
251
|
-
set <<
|
256
|
+
set << z.get(i, lifetime: 0.001) { i }
|
252
257
|
end
|
253
258
|
assert_equal(threads, set.size)
|
254
259
|
end
|
255
260
|
|
256
261
|
def test_fetches_multiple_keys_in_many_threads
|
257
|
-
|
262
|
+
z = Zache.new
|
258
263
|
set = Concurrent::Set.new
|
259
264
|
threads = 50
|
260
265
|
barrier = Concurrent::CyclicBarrier.new(threads)
|
261
266
|
Threads.new(threads).assert(threads * 2) do |i|
|
262
267
|
barrier.wait if i < threads
|
263
|
-
set <<
|
268
|
+
set << z.get(i) { i }
|
264
269
|
end
|
265
270
|
assert_equal(threads, set.size)
|
266
271
|
end
|
267
272
|
|
268
273
|
def test_fake_class_works
|
269
|
-
|
270
|
-
assert_equal(1,
|
274
|
+
z = Zache::Fake.new
|
275
|
+
assert_equal(1, z.get(:x) { 1 })
|
271
276
|
end
|
272
277
|
|
273
278
|
def test_rethrows
|
274
|
-
|
279
|
+
z = Zache.new
|
275
280
|
assert_raises RuntimeError do
|
276
|
-
|
281
|
+
z.get(:hey) { raise 'intentional' }
|
277
282
|
end
|
278
283
|
end
|
279
284
|
|
280
285
|
def test_returns_placeholder_in_eager_mode
|
281
|
-
|
282
|
-
a =
|
286
|
+
z = Zache.new
|
287
|
+
a = z.get(:me, placeholder: 42, eager: true) do
|
283
288
|
sleep 0.1
|
284
289
|
43
|
285
290
|
end
|
286
291
|
assert_equal(42, a)
|
287
292
|
sleep 0.2
|
288
|
-
b =
|
293
|
+
b = z.get(:me)
|
289
294
|
assert_equal(43, b)
|
290
295
|
end
|
291
296
|
|
297
|
+
def test_returns_placeholder_and_releases_lock
|
298
|
+
z = Zache.new
|
299
|
+
z.get(:slow, placeholder: 42, eager: true) do
|
300
|
+
sleep 9999
|
301
|
+
end
|
302
|
+
sleep 0.1
|
303
|
+
assert_equal(555, z.get(:fast) { 555 })
|
304
|
+
end
|
305
|
+
|
292
306
|
private
|
293
307
|
|
294
308
|
def rand
|
data/zache.gemspec
CHANGED
@@ -8,7 +8,7 @@ Gem::Specification.new do |s|
|
|
8
8
|
s.required_rubygems_version = Gem::Requirement.new('>= 0') if s.respond_to? :required_rubygems_version=
|
9
9
|
s.required_ruby_version = '>= 2.5'
|
10
10
|
s.name = 'zache'
|
11
|
-
s.version = '0.
|
11
|
+
s.version = '0.15.0' # Version should be updated before release
|
12
12
|
s.license = 'MIT'
|
13
13
|
s.summary = 'In-memory Cache'
|
14
14
|
s.description = 'Zero-footprint in-memory thread-safe cache'
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: zache
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.15.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Yegor Bugayenko
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-04-
|
10
|
+
date: 2025-04-15 00:00:00.000000000 Z
|
11
11
|
dependencies: []
|
12
12
|
description: Zero-footprint in-memory thread-safe cache
|
13
13
|
email: yegor256@gmail.com
|