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.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/lib/zache.rb +30 -17
  3. data/test/test_zache.rb +137 -123
  4. data/zache.gemspec +1 -1
  5. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c247c0f146ef35562b8d56169fdb69ef0a9a02796d0e49dfb6c8ed83094dfd24
4
- data.tar.gz: d0b659e1315eee6f19b6c708da5b49073d3e0d9a5d5809a424c9b641d330e992
3
+ metadata.gz: f2c6bee2ddda59d3ddba9115b7f1eaf2037fe28cc217bec57aa1bc2dfb91b660
4
+ data.tar.gz: 3b1fafcbe3afd075992c0a36df45ca5b51228bd4d1d9c5e086c7523b462ef343
5
5
  SHA512:
6
- metadata.gz: 23c14258f92e749ee9876b4fae20274a954d71a34a95b427dafd9bef201d17c91c1731d1733345ed564f9e7d74e340c68d9325127a55ab73e5d654ac8d5cb866
7
- data.tar.gz: 9232c93c2027dbe83015974315e5c609f3392216c77a96f465cc13f0ddd309fc11d4147234ff23892037619491bb037bc466e28f0075a8be424ef60c28014327
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
- synchronized do
130
+ synchronize_one(key) do
129
131
  calc(key, lifetime, &block)
130
132
  end
131
133
  end
132
134
  placeholder
133
135
  else
134
- synchronized do
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 cache currently locked doing something?
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
- @mutex.locked?
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
- synchronized do
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
- synchronized { @hash.delete(key) { yield if block_given? } }
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
- synchronized { @hash = {} }
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
- synchronized do
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
- synchronized do
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 synchronized(&block)
288
- if @sync
289
- @mutex.synchronize(&block)
290
- else
291
- yield
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
- cache = Zache.new(sync: false)
23
- first = cache.get(:hey, lifetime: 5) { rand }
24
- second = cache.get(:hey) { rand }
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, cache.size)
26
+ assert_equal(1, z.size)
27
27
  end
28
28
 
29
29
  def test_caches_and_expires
30
- cache = Zache.new
31
- first = cache.get(:hey, lifetime: 0.01) { rand }
30
+ z = Zache.new
31
+ first = z.get(:hey, lifetime: 0.01) { rand }
32
32
  sleep 0.1
33
- second = cache.get(:hey) { rand }
33
+ second = z.get(:hey) { rand }
34
34
  refute_equal(first, second)
35
35
  end
36
36
 
37
37
  def test_calculates_age
38
- cache = Zache.new
39
- cache.get(:hey) { rand }
38
+ z = Zache.new
39
+ z.get(:hey) { rand }
40
40
  sleep 0.1
41
- assert_operator(cache.mtime(:hey), :<, Time.now - 0.05)
41
+ assert_operator(z.mtime(:hey), :<, Time.now - 0.05)
42
42
  end
43
43
 
44
44
  def test_caches_in_threads
45
- cache = Zache.new
45
+ z = Zache.new
46
46
  Threads.new(10).assert(100) do
47
- cache.get(:hey, lifetime: 0.0001) { rand }
47
+ z.get(:hey, lifetime: 0.0001) { rand }
48
48
  end
49
49
  end
50
50
 
51
51
  def test_key_exists
52
- cache = Zache.new
53
- cache.get(:hey) { rand }
54
- exists_result = cache.exists?(:hey)
55
- not_exists_result = cache.exists?(:bye)
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
- cache = Zache.new
62
- cache.put(:hey, 'hello', lifetime: 0.1)
61
+ z = Zache.new
62
+ z.put(:hey, 'hello', lifetime: 0.1)
63
63
  sleep 0.2
64
- refute(cache.exists?(:hey))
64
+ refute(z.exists?(:hey))
65
65
  end
66
66
 
67
67
  def test_remove_key
68
- cache = Zache.new
69
- cache.get(:hey) { rand }
70
- cache.get(:wey) { rand }
71
- assert(cache.exists?(:hey))
72
- assert(cache.exists?(:wey))
73
- cache.remove(:hey)
74
- refute(cache.exists?(:hey))
75
- assert(cache.exists?(:wey))
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
- cache = Zache.new
80
- cache.get('first') { rand }
81
- cache.get('second') { rand }
82
- cache.remove_by { |k| k == 'first' }
83
- refute(cache.exists?('first'))
84
- assert(cache.exists?('second'))
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
- cache = Zache.new(sync: false)
89
- cache.get(:hey) { rand }
90
- cache.get(:wey) { rand }
91
- assert(cache.exists?(:hey))
92
- assert(cache.exists?(:wey))
93
- cache.remove(:hey)
94
- refute(cache.exists?(:hey))
95
- assert(cache.exists?(:wey))
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
- cache = Zache.new
99
+ z = Zache.new
100
100
  Threads.new(300).assert(3000) do
101
- cache.get(:hey) { rand }
102
- cache.get(:bye, lifetime: 0.01) { rand }
101
+ z.get(:hey) { rand }
102
+ z.get(:bye, lifetime: 0.01) { rand }
103
103
  sleep 0.1
104
- cache.clean
104
+ z.clean
105
105
  end
106
- assert(cache.exists?(:hey))
107
- refute(cache.exists?(:bye))
106
+ assert(z.exists?(:hey))
107
+ refute(z.exists?(:bye))
108
108
  end
109
109
 
110
110
  def test_clean
111
- cache = Zache.new
112
- cache.get(:hey) { rand }
113
- cache.get(:bye, lifetime: 0.01) { rand }
111
+ z = Zache.new
112
+ z.get(:hey) { rand }
113
+ z.get(:bye, lifetime: 0.01) { rand }
114
114
  sleep 0.1
115
- cache.clean
116
- assert(cache.exists?(:hey))
117
- refute(cache.exists?(:bye))
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
- cache = Zache.new
122
- cache.get(:hey, lifetime: 0.01) { rand }
121
+ z = Zache.new
122
+ z.get(:hey, lifetime: 0.01) { rand }
123
123
  sleep 0.1
124
- cache.clean
125
- assert_empty(cache)
124
+ z.clean
125
+ assert_empty(z)
126
126
  end
127
127
 
128
128
  def test_clean_with_sync_false
129
- cache = Zache.new(sync: false)
130
- cache.get(:hey) { rand }
131
- cache.get(:bye, lifetime: 0.01) { rand }
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
- cache.clean
134
- assert(cache.exists?(:hey))
135
- refute(cache.exists?(:bye))
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
- cache = Zache.new
140
- cache.remove(:hey)
139
+ z = Zache.new
140
+ z.remove(:hey)
141
141
  end
142
142
 
143
143
  def test_check_and_remove
144
- cache = Zache.new
145
- cache.get(:hey, lifetime: 0) { rand }
146
- refute(cache.exists?(:hey))
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
- cache = Zache.new
150
+ z = Zache.new
151
151
  Threads.new(10).assert(100) do |i|
152
- cache.get(:"hey#{i}") { rand }
153
- assert(cache.exists?(:"hey#{i}"))
154
- cache.remove_all
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(cache.exists?(:"hey#{i}"))
157
+ refute(z.exists?(:"hey#{i}"))
158
158
  end
159
159
  end
160
160
 
161
161
  def test_remove_all_with_sync
162
- cache = Zache.new
163
- cache.get(:hey) { rand }
164
- cache.get(:bye) { rand }
165
- cache.remove_all
166
- refute(cache.exists?(:hey))
167
- refute(cache.exists?(:bye))
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
- cache = Zache.new(sync: false)
172
- cache.get(:hey) { rand }
173
- cache.get(:bye) { rand }
174
- cache.remove_all
175
- refute(cache.exists?(:hey))
176
- refute(cache.exists?(:bye))
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
- cache = Zache.new(sync: false)
181
- cache.get(:hey) { rand }
182
- cache.put(:hey, 123)
183
- assert_equal(123, cache.get(:hey))
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
- cache = Zache.new
187
+ z = Zache.new
188
188
  assert_raises ThreadError do
189
- cache.get(:first) { cache.get(:second) { 1 } }
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
- cache = Zache.new
199
+ z = Zache.new
195
200
  long = Thread.start do
196
- cache.get(:x) do
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
- assert_predicate(cache, :locked?)
203
- cache.get(:x) { 'second' }
204
- refute_predicate(cache, :locked?)
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
- cache = Zache.new
210
- cache.get(:x) do
211
- assert_predicate(cache, :locked?)
214
+ z = Zache.new
215
+ z.get(:x) do
216
+ assert(z.locked?(:x))
212
217
  'done'
213
218
  end
214
- refute_predicate(cache, :locked?)
219
+ refute(z.locked?(:x))
215
220
  end
216
221
 
217
222
  def test_returns_dirty_result
218
- cache = Zache.new(dirty: true)
219
- cache.get(:x, lifetime: 0) { 1 }
223
+ z = Zache.new(dirty: true)
224
+ z.get(:x, lifetime: 0) { 1 }
220
225
  long = Thread.start do
221
- cache.get(:x) do
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(cache.exists?(:x))
229
- assert(cache.expired?(:x))
230
- assert_equal(1, cache.get(:x))
231
- assert_equal(1, cache.get(:x) { 2 })
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
- cache = Zache.new(dirty: true)
238
- cache.get(:x, lifetime: 0) { 1 }
239
- assert(cache.exists?(:x))
240
- assert_equal(1, cache.get(:x))
241
- assert_equal(2, cache.get(:x) { 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
- cache = Zache.new(dirty: true)
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 << cache.get(i, lifetime: 0.001) { i }
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
- cache = Zache.new
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 << cache.get(i) { i }
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
- cache = Zache::Fake.new
270
- assert_equal(1, cache.get(:x) { 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
- cache = Zache.new
279
+ z = Zache.new
275
280
  assert_raises RuntimeError do
276
- cache.get(:hey) { raise 'intentional' }
281
+ z.get(:hey) { raise 'intentional' }
277
282
  end
278
283
  end
279
284
 
280
285
  def test_returns_placeholder_in_eager_mode
281
- cache = Zache.new
282
- a = cache.get(:me, placeholder: 42, eager: true) do
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 = cache.get(:me)
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.14.0' # Version should be updated before release
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.14.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-12 00:00:00.000000000 Z
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