zache 0.13.2 → 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.
data/lib/zache.rb CHANGED
@@ -1,26 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # (The MIT License)
4
- #
5
- # Copyright (c) 2018-2024 Yegor Bugayenko
6
- #
7
- # Permission is hereby granted, free of charge, to any person obtaining a copy
8
- # of this software and associated documentation files (the 'Software'), to deal
9
- # in the Software without restriction, including without limitation the rights
10
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
- # copies of the Software, and to permit persons to whom the Software is
12
- # furnished to do so, subject to the following conditions:
13
- #
14
- # The above copyright notice and this permission notice shall be included in all
15
- # copies or substantial portions of the Software.
16
- #
17
- # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
- # SOFTWARE.
3
+ # SPDX-FileCopyrightText: Copyright (c) 2018-2025 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
24
5
 
25
6
  # It is a very simple thread-safe in-memory cache with an ability to expire
26
7
  # keys automatically, when their lifetime is over. Use it like this:
@@ -34,35 +15,61 @@
34
15
  # {README}[https://github.com/yegor256/zache/blob/master/README.md] file.
35
16
  #
36
17
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
37
- # Copyright:: Copyright (c) 2018-2024 Yegor Bugayenko
18
+ # Copyright:: Copyright (c) 2018-2025 Yegor Bugayenko
38
19
  # License:: MIT
39
20
  class Zache
40
21
  # Fake implementation that doesn't cache anything, but behaves like it
41
22
  # does. It implements all methods of the original class, but doesn't do
42
23
  # any caching. This is very useful for testing.
43
24
  class Fake
25
+ # Returns a fixed size of 1.
26
+ # @return [Integer] Always returns 1
44
27
  def size
45
28
  1
46
29
  end
47
30
 
31
+ # Always returns the result of the block, never caches.
32
+ # @param [Object] key Ignored
33
+ # @param [Hash] opts Ignored
34
+ # @yield Block that provides the value
35
+ # @return [Object] The result of the block
48
36
  def get(*)
49
37
  yield
50
38
  end
51
39
 
40
+ # Always returns true regardless of the key.
41
+ # @param [Object] key Ignored
42
+ # @param [Hash] opts Ignored
43
+ # @return [Boolean] Always returns true
52
44
  def exists?(*)
53
45
  true
54
46
  end
55
47
 
56
- def locked?
48
+ # Always returns false.
49
+ # @param [Object] key Ignored
50
+ # @return [Boolean] Always returns false
51
+ def locked?(_key)
57
52
  false
58
53
  end
59
54
 
55
+ # No-op method that ignores the input.
56
+ # @param [Object] key Ignored
57
+ # @param [Object] value Ignored
58
+ # @param [Hash] opts Ignored
59
+ # @return [nil] Always returns nil
60
60
  def put(*); end
61
61
 
62
+ # No-op method that ignores the key.
63
+ # @param [Object] _key Ignored
64
+ # @return [nil] Always returns nil
62
65
  def remove(_key); end
63
66
 
67
+ # No-op method.
68
+ # @return [nil] Always returns nil
64
69
  def remove_all; end
65
70
 
71
+ # No-op method.
72
+ # @return [nil] Always returns nil
66
73
  def clean; end
67
74
  end
68
75
 
@@ -73,15 +80,22 @@ class Zache
73
80
  # unless you really know what you are doing.
74
81
  #
75
82
  # If the <tt>dirty</tt> argument is set to <tt>true</tt>, a previously
76
- # calculated result will be returned if it exists and is already expired.
83
+ # calculated result will be returned if it exists, even if it is already expired.
84
+ #
85
+ # @param sync [Boolean] Whether the hash is thread-safe
86
+ # @param dirty [Boolean] Whether to return expired values
87
+ # @return [Zache] A new instance of the cache
77
88
  def initialize(sync: true, dirty: false)
78
89
  @hash = {}
79
90
  @sync = sync
80
91
  @dirty = dirty
81
92
  @mutex = Mutex.new
93
+ @locks = {}
82
94
  end
83
95
 
84
96
  # Total number of keys currently in cache.
97
+ #
98
+ # @return [Integer] Number of keys in the cache
85
99
  def size
86
100
  @hash.size
87
101
  end
@@ -90,17 +104,39 @@ class Zache
90
104
  #
91
105
  # If the value is not
92
106
  # found in the cache, it will be calculated via the provided block. If
93
- # the block is not given, an exception will be raised (unless <tt>dirty</tt>
94
- # is set to <tt>true</tt>). The lifetime
107
+ # the block is not given and the key doesn't exist or is expired, an exception will be raised
108
+ # (unless <tt>dirty</tt> is set to <tt>true</tt>). The lifetime
95
109
  # must be in seconds. The default lifetime is huge, which means that the
96
110
  # key will never be expired.
97
111
  #
98
112
  # If the <tt>dirty</tt> argument is set to <tt>true</tt>, a previously
99
- # calculated result will be returned if it exists and is already expired.
100
- def get(key, lifetime: 2**32, dirty: false, &block)
113
+ # calculated result will be returned if it exists, even if it is already expired.
114
+ #
115
+ # @param key [Object] The key to retrieve from the cache
116
+ # @param lifetime [Integer] Time in seconds until the key expires
117
+ # @param dirty [Boolean] Whether to return expired values
118
+ # @param eager [Boolean] Whether to return placeholder while working?
119
+ # @param placeholder [Object] The placeholder to return in eager mode
120
+ # @yield Block to calculate the value if not in cache
121
+ # @yieldreturn [Object] The value to cache
122
+ # @return [Object] The cached value
123
+ def get(key, lifetime: 2**32, dirty: false, placeholder: nil, eager: false, &block)
101
124
  if block_given?
102
- return @hash[key][:value] if (dirty || @dirty) && locked? && expired?(key) && @hash.key?(key)
103
- synchronized { calc(key, lifetime, &block) }
125
+ return @hash[key][:value] if (dirty || @dirty) && locked?(key) && expired?(key) && @hash.key?(key)
126
+ if eager
127
+ return @hash[key][:value] if @hash.key?(key)
128
+ put(key, placeholder, lifetime: 0)
129
+ Thread.new do
130
+ synchronize_one(key) do
131
+ calc(key, lifetime, &block)
132
+ end
133
+ end
134
+ placeholder
135
+ else
136
+ synchronize_one(key) do
137
+ calc(key, lifetime, &block)
138
+ end
139
+ end
104
140
  else
105
141
  rec = @hash[key]
106
142
  if expired?(key)
@@ -114,8 +150,12 @@ class Zache
114
150
  end
115
151
 
116
152
  # Checks whether the value exists in the cache by the provided key. Returns
117
- # TRUE if the value is here. If the key is already expired in the hash,
153
+ # TRUE if the value is here. If the key is already expired in the cache,
118
154
  # it will be removed by this method and the result will be FALSE.
155
+ #
156
+ # @param key [Object] The key to check in the cache
157
+ # @param dirty [Boolean] Whether to consider expired values as existing
158
+ # @return [Boolean] True if the key exists and is not expired (unless dirty is true)
119
159
  def exists?(key, dirty: false)
120
160
  rec = @hash[key]
121
161
  if expired?(key) && !dirty && !@dirty
@@ -127,6 +167,9 @@ class Zache
127
167
 
128
168
  # Checks whether the key exists in the cache and is expired. If the
129
169
  # key is absent FALSE is returned.
170
+ #
171
+ # @param key [Object] The key to check in the cache
172
+ # @return [Boolean] True if the key exists and is expired
130
173
  def expired?(key)
131
174
  rec = @hash[key]
132
175
  !rec.nil? && rec[:start] < Time.now - rec[:lifetime]
@@ -134,19 +177,30 @@ class Zache
134
177
 
135
178
  # Returns the modification time of the key, if it exists.
136
179
  # If not, current time is returned.
180
+ #
181
+ # @param key [Object] The key to get the modification time for
182
+ # @return [Time] The modification time of the key or current time if key doesn't exist
137
183
  def mtime(key)
138
184
  rec = @hash[key]
139
185
  rec.nil? ? Time.now : rec[:start]
140
186
  end
141
187
 
142
- # Is cache currently locked doing something?
143
- def locked?
144
- @mutex.locked?
188
+ # Is key currently locked doing something?
189
+ #
190
+ # @param [Object] key The key to check
191
+ # @return [Boolean] True if the cache is locked
192
+ def locked?(key)
193
+ @locks[key]&.locked?
145
194
  end
146
195
 
147
196
  # Put a value into the cache.
197
+ #
198
+ # @param key [Object] The key to store the value under
199
+ # @param value [Object] The value to store in the cache
200
+ # @param lifetime [Integer] Time in seconds until the key expires (default: never expires)
201
+ # @return [Object] The value stored
148
202
  def put(key, value, lifetime: 2**32)
149
- synchronized do
203
+ synchronize_one(key) do
150
204
  @hash[key] = {
151
205
  value: value,
152
206
  start: Time.now,
@@ -157,35 +211,65 @@ class Zache
157
211
 
158
212
  # Removes the value from the cache, by the provided key. If the key is absent
159
213
  # and the block is provided, the block will be called.
214
+ #
215
+ # @param key [Object] The key to remove from the cache
216
+ # @yield Block to call if the key is not found
217
+ # @return [Object] The removed value or the result of the block
160
218
  def remove(key)
161
- synchronized { @hash.delete(key) { yield if block_given? } }
219
+ synchronize_one(key) { @hash.delete(key) { yield if block_given? } }
162
220
  end
163
221
 
164
222
  # Remove all keys from the cache.
223
+ #
224
+ # @return [Hash] Empty hash
165
225
  def remove_all
166
- synchronized { @hash = {} }
226
+ synchronize_all { @hash = {} }
167
227
  end
168
228
 
169
229
  # Remove all keys that match the block.
230
+ #
231
+ # @yield [key] Block that should return true for keys to be removed
232
+ # @yieldparam key [Object] The cache key to evaluate
233
+ # @return [Integer] Number of keys removed
170
234
  def remove_by
171
- synchronized do
235
+ synchronize_all do
236
+ count = 0
172
237
  @hash.each_key do |k|
173
- @hash.delete(k) if yield(k)
238
+ if yield(k)
239
+ @hash.delete(k)
240
+ count += 1
241
+ end
174
242
  end
243
+ count
175
244
  end
176
245
  end
177
246
 
178
- # Remove keys that are expired.
247
+ # Remove keys that are expired. This cleans up the cache by removing all keys
248
+ # where the lifetime has been exceeded.
249
+ #
250
+ # @return [Integer] Number of keys removed
179
251
  def clean
180
- synchronized { @hash.delete_if { |key, _value| expired?(key) } }
252
+ synchronize_all do
253
+ size_before = @hash.size
254
+ @hash.delete_if { |key, _value| expired?(key) }
255
+ size_before - @hash.size
256
+ end
181
257
  end
182
258
 
259
+ # Returns TRUE if the cache is empty, FALSE otherwise.
260
+ #
261
+ # @return [Boolean] True if the cache is empty
183
262
  def empty?
184
263
  @hash.empty?
185
264
  end
186
265
 
187
266
  private
188
267
 
268
+ # Calculates or retrieves a cached value for the given key.
269
+ # @param key [Object] The key to store the value under
270
+ # @param lifetime [Integer] Time in seconds until the key expires
271
+ # @yield Block that provides the value if not cached
272
+ # @return [Object] The cached or newly calculated value
189
273
  def calc(key, lifetime)
190
274
  rec = @hash[key]
191
275
  rec = nil if expired?(key)
@@ -199,11 +283,25 @@ class Zache
199
283
  @hash[key][:value]
200
284
  end
201
285
 
202
- def synchronized(&block)
203
- if @sync
204
- @mutex.synchronize(&block)
205
- else
206
- block.call
286
+ # Executes a block within a synchronized context if sync is enabled.
287
+ # @param block [Proc] The block to execute
288
+ # @yield The block to execute in a synchronized context
289
+ # @return [Object] The result of the block
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
207
304
  end
305
+ @locks[key].synchronize(&block)
208
306
  end
209
307
  end
data/logo.svg CHANGED
@@ -16,4 +16,4 @@
16
16
  </g>
17
17
  </g>
18
18
  </g>
19
- </svg>
19
+ </svg>
data/test/test__helper.rb CHANGED
@@ -1,28 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright (c) 2018-2024 Yegor Bugayenko
4
- #
5
- # Permission is hereby granted, free of charge, to any person obtaining a copy
6
- # of this software and associated documentation files (the 'Software'), to deal
7
- # in the Software without restriction, including without limitation the rights
8
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- # copies of the Software, and to permit persons to whom the Software is
10
- # furnished to do so, subject to the following conditions:
11
- #
12
- # The above copyright notice and this permission notice shall be included in all
13
- # copies or substantial portions of the Software.
14
- #
15
- # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
18
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- # SOFTWARE.
3
+ # SPDX-FileCopyrightText: Copyright (c) 2018-2025 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
22
5
 
23
6
  $stdout.sync = true
24
7
 
25
8
  require 'simplecov'
26
- SimpleCov.start
9
+ require 'simplecov-cobertura'
10
+ unless SimpleCov.running || ENV['PICKS']
11
+ SimpleCov.command_name('test')
12
+ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new(
13
+ [
14
+ SimpleCov::Formatter::HTMLFormatter,
15
+ SimpleCov::Formatter::CoberturaFormatter
16
+ ]
17
+ )
18
+ SimpleCov.minimum_coverage 90
19
+ SimpleCov.minimum_coverage_by_file 90
20
+ SimpleCov.start do
21
+ add_filter 'test/'
22
+ add_filter 'vendor/'
23
+ add_filter 'target/'
24
+ track_files 'lib/**/*.rb'
25
+ track_files '*.rb'
26
+ end
27
+ end
28
+
27
29
  require 'minitest/autorun'
28
- require_relative '../lib/zache'
30
+ require 'minitest/reporters'
31
+ Minitest::Reporters.use! [Minitest::Reporters::SpecReporter.new]