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.
- checksums.yaml +4 -4
- data/.0pdd.yml +2 -21
- data/.github/workflows/actionlint.yml +5 -23
- data/.github/workflows/codecov.yml +11 -26
- data/.github/workflows/copyrights.yml +6 -23
- data/.github/workflows/license.yml +5 -22
- data/.github/workflows/markdown-lint.yml +5 -22
- data/.github/workflows/pdd.yml +6 -23
- data/.github/workflows/rake.yml +8 -26
- data/.github/workflows/reuse.yml +19 -0
- data/.github/workflows/xcop.yml +5 -22
- data/.github/workflows/yamllint.yml +5 -22
- data/.gitignore +6 -2
- data/.rubocop.yml +20 -28
- data/.rultor.yml +5 -22
- data/Gemfile +16 -27
- data/Gemfile.lock +78 -40
- data/LICENSE.txt +1 -1
- data/LICENSES/MIT.txt +21 -0
- data/README.md +45 -19
- data/REUSE.toml +31 -0
- data/Rakefile +19 -34
- data/lib/zache.rb +144 -46
- data/logo.svg +1 -1
- data/test/test__helper.rb +24 -21
- data/test/test_zache.rb +162 -147
- data/zache.gemspec +5 -24
- metadata +7 -7
data/lib/zache.rb
CHANGED
@@ -1,26 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# (
|
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-
|
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
|
-
|
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
|
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
|
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
|
100
|
-
|
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
|
-
|
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
|
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
|
143
|
-
|
144
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
235
|
+
synchronize_all do
|
236
|
+
count = 0
|
172
237
|
@hash.each_key do |k|
|
173
|
-
|
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
|
-
|
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
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
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
data/test/test__helper.rb
CHANGED
@@ -1,28 +1,31 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Copyright (c) 2018-
|
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
|
-
|
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
|
-
|
30
|
+
require 'minitest/reporters'
|
31
|
+
Minitest::Reporters.use! [Minitest::Reporters::SpecReporter.new]
|