zache 0.15.0 → 0.15.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f2c6bee2ddda59d3ddba9115b7f1eaf2037fe28cc217bec57aa1bc2dfb91b660
4
- data.tar.gz: 3b1fafcbe3afd075992c0a36df45ca5b51228bd4d1d9c5e086c7523b462ef343
3
+ metadata.gz: e9cc0b6c8fece285753b4a7bc12bc296f9cab08b7adacee09dd888f556263e4c
4
+ data.tar.gz: 722f973b74565b70225b44f1de8f6c63ddccf18bbb534ba31a9b1c4c24bbb60d
5
5
  SHA512:
6
- metadata.gz: 24f22827fe4b5ea546ea2e121b8aaf35c2646ecbcc4ec7134b0d27693bfa4345d29829d29db1d417ab7759c556f3c21c8cea151f267fdaa282d45d69b8e53a57
7
- data.tar.gz: 2a8a4c661c2424a126c8b309825d8d558206db695359793191caf5c1b3e348a147381149258418e7e7469a3e9bfdab30f0164eedfe59383012ed8173fae5fbdf
6
+ metadata.gz: 9ff5b92b44b011b8cb689538e7d4eade3d7d216a89a94b0b85da769bece6610ba9aa27e143f766bbac0b736de65bd7d7faba803b1af4c93876e02913843b819a
7
+ data.tar.gz: dfb50539962ba04cf80db2ceff76dc01abfa3596fd3dc46a21e7d23cc03219e52098a2c77525d0e1ed35b4ea6b31fed6a5f548c228f1105639d66852c4594ac8
data/Gemfile CHANGED
@@ -12,11 +12,12 @@ gem 'minitest-reporters', '~>1.7', require: false
12
12
  gem 'os', '>0', require: false
13
13
  gem 'qbash', '>0', require: false
14
14
  gem 'rake', '~>13.2', require: false
15
+ gem 'rdoc', '~>6.15', require: false
15
16
  gem 'rubocop', '~>1.75', require: false
16
- gem 'rubocop-minitest', '>0', require: false
17
- gem 'rubocop-performance', '>0', require: false
18
- gem 'rubocop-rake', '>0', require: false
17
+ gem 'rubocop-minitest', '~>0.38', require: false
18
+ gem 'rubocop-performance', '~>1.25', require: false
19
+ gem 'rubocop-rake', '~>0.7', require: false
19
20
  gem 'simplecov', '~>0.22', require: false
20
- gem 'simplecov-cobertura', '~>2.1', require: false
21
+ gem 'simplecov-cobertura', '~>3.0', require: false
21
22
  gem 'threads', '~>0.4', require: false
22
23
  gem 'yard', '~>0.9', require: false
data/Gemfile.lock CHANGED
@@ -8,40 +8,51 @@ GEM
8
8
  specs:
9
9
  ansi (1.5.0)
10
10
  ast (2.4.3)
11
- backtrace (0.4.0)
11
+ backtrace (0.4.1)
12
12
  builder (3.3.0)
13
13
  concurrent-ruby (1.3.5)
14
+ date (3.5.0)
14
15
  docile (1.4.1)
15
- elapsed (0.0.1)
16
- loog (> 0)
17
- tago (> 0)
18
- json (2.10.2)
19
- language_server-protocol (3.17.0.4)
16
+ elapsed (0.2.0)
17
+ loog (~> 0.6)
18
+ tago (~> 0.1)
19
+ erb (6.0.0)
20
+ json (2.16.0)
21
+ language_server-protocol (3.17.0.5)
20
22
  lint_roller (1.1.0)
21
- loog (0.6.0)
22
- minitest (5.25.5)
23
+ logger (1.7.0)
24
+ loog (0.6.1)
25
+ logger (~> 1.0)
26
+ minitest (5.26.2)
23
27
  minitest-reporters (1.7.1)
24
28
  ansi
25
29
  builder
26
30
  minitest (>= 5.0)
27
31
  ruby-progressbar
28
32
  os (1.1.4)
29
- parallel (1.26.3)
30
- parser (3.3.7.4)
33
+ parallel (1.27.0)
34
+ parser (3.3.10.0)
31
35
  ast (~> 2.4.1)
32
36
  racc
33
- prism (1.4.0)
34
- qbash (0.4.0)
37
+ prism (1.6.0)
38
+ psych (5.2.6)
39
+ date
40
+ stringio
41
+ qbash (0.4.8)
35
42
  backtrace (> 0)
36
43
  elapsed (> 0)
37
44
  loog (> 0)
38
45
  tago (> 0)
39
46
  racc (1.8.1)
40
47
  rainbow (3.1.1)
41
- rake (13.2.1)
42
- regexp_parser (2.10.0)
43
- rexml (3.4.1)
44
- rubocop (1.75.2)
48
+ rake (13.3.1)
49
+ rdoc (6.16.1)
50
+ erb
51
+ psych (>= 4.0.0)
52
+ tsort
53
+ regexp_parser (2.11.3)
54
+ rexml (3.4.4)
55
+ rubocop (1.81.7)
45
56
  json (~> 2.3)
46
57
  language_server-protocol (~> 3.17.0.2)
47
58
  lint_roller (~> 1.1.0)
@@ -49,20 +60,20 @@ GEM
49
60
  parser (>= 3.3.0.2)
50
61
  rainbow (>= 2.2.2, < 4.0)
51
62
  regexp_parser (>= 2.9.3, < 3.0)
52
- rubocop-ast (>= 1.44.0, < 2.0)
63
+ rubocop-ast (>= 1.47.1, < 2.0)
53
64
  ruby-progressbar (~> 1.7)
54
65
  unicode-display_width (>= 2.4.0, < 4.0)
55
- rubocop-ast (1.44.1)
66
+ rubocop-ast (1.48.0)
56
67
  parser (>= 3.3.7.2)
57
68
  prism (~> 1.4)
58
- rubocop-minitest (0.38.0)
69
+ rubocop-minitest (0.38.2)
59
70
  lint_roller (~> 1.1)
60
71
  rubocop (>= 1.75.0, < 2.0)
61
72
  rubocop-ast (>= 1.38.0, < 2.0)
62
- rubocop-performance (1.25.0)
73
+ rubocop-performance (1.26.1)
63
74
  lint_roller (~> 1.1)
64
75
  rubocop (>= 1.75.0, < 2.0)
65
- rubocop-ast (>= 1.38.0, < 2.0)
76
+ rubocop-ast (>= 1.47.1, < 2.0)
66
77
  rubocop-rake (0.7.1)
67
78
  lint_roller (~> 1.1)
68
79
  rubocop (>= 1.72.1)
@@ -71,18 +82,20 @@ GEM
71
82
  docile (~> 1.1)
72
83
  simplecov-html (~> 0.11)
73
84
  simplecov_json_formatter (~> 0.1)
74
- simplecov-cobertura (2.1.0)
85
+ simplecov-cobertura (3.1.0)
75
86
  rexml
76
87
  simplecov (~> 0.19)
77
- simplecov-html (0.13.1)
88
+ simplecov-html (0.13.2)
78
89
  simplecov_json_formatter (0.1.4)
79
- tago (0.1.0)
80
- threads (0.4.1)
90
+ stringio (3.1.8)
91
+ tago (0.4.0)
92
+ threads (0.5.0)
81
93
  backtrace (~> 0)
82
94
  concurrent-ruby (~> 1.0)
83
- unicode-display_width (3.1.4)
84
- unicode-emoji (~> 4.0, >= 4.0.4)
85
- unicode-emoji (4.0.4)
95
+ tsort (0.2.0)
96
+ unicode-display_width (3.2.0)
97
+ unicode-emoji (~> 4.1)
98
+ unicode-emoji (4.1.0)
86
99
  yard (0.9.37)
87
100
 
88
101
  PLATFORMS
@@ -101,12 +114,13 @@ DEPENDENCIES
101
114
  os (> 0)
102
115
  qbash (> 0)
103
116
  rake (~> 13.2)
117
+ rdoc (~> 6.15)
104
118
  rubocop (~> 1.75)
105
- rubocop-minitest (> 0)
106
- rubocop-performance (> 0)
107
- rubocop-rake (> 0)
119
+ rubocop-minitest (~> 0.38)
120
+ rubocop-performance (~> 1.25)
121
+ rubocop-rake (~> 0.7)
108
122
  simplecov (~> 0.22)
109
- simplecov-cobertura (~> 2.1)
123
+ simplecov-cobertura (~> 3.0)
110
124
  threads (~> 0.4)
111
125
  yard (~> 0.9)
112
126
  zache!
data/README.md CHANGED
@@ -13,8 +13,7 @@
13
13
  [![Hits-of-Code](https://hitsofcode.com/github/yegor256/zache)](https://hitsofcode.com/view/github/yegor256/zache)
14
14
 
15
15
  This is a simple Ruby gem for in-memory caching.
16
- Read [this blog post](https://www.yegor256.com/2019/02/05/zache.html)
17
- to understand what Zache is designed for.
16
+ Read this [blog post] to understand what Zache is designed for.
18
17
 
19
18
  First, install it:
20
19
 
@@ -31,22 +30,26 @@ zache = Zache.new
31
30
  v = zache.get(:count, lifetime: 5 * 60) { expensive_calculation() }
32
31
  ```
33
32
 
34
- If you omit the `lifetime` parameter, the key will never expire.
33
+ If you omit the `lifetime` parameter or set it to `nil`,
34
+ the key will never expire.
35
35
 
36
- By default `Zache` is thread-safe. It locks the entire cache on each
37
- `get` call. You can turn that off by using the `sync` argument:
36
+ By default `Zache` is thread-safe.
37
+ It locks the entire cache on each `get` call.
38
+ You can turn that off by using the `sync` argument:
38
39
 
39
40
  ```ruby
40
41
  zache = Zache.new(sync: false)
41
42
  v = zache.get(:count) { expensive_calculation() }
42
43
  ```
43
44
 
44
- You may use "dirty" mode, which will return an expired value while
45
- calculation is in progress. For example, if you have a value in the cache that's
46
- expired, and you call `get` with a long-running block, the thread waits.
47
- If another thread calls `get` again, that second thread won't wait, but will
48
- receive the expired value from the cache. This is a very convenient mode for situations
49
- where absolute data accuracy is less important than performance:
45
+ You may use "dirty" mode, which will return
46
+ an expired value while calculation is in progress.
47
+ For example, if you have a value in the cache that's expired,
48
+ and you call `get` with a long-running block, the thread waits.
49
+ If another thread calls `get` again, that second thread won't wait,
50
+ but will receive the expired value from the cache.
51
+ This is a very convenient mode for situations
52
+ where absolute data accuracy is less important than performance:
50
53
 
51
54
  ```ruby
52
55
  zache = Zache.new(dirty: true)
@@ -54,8 +57,19 @@ zache = Zache.new(dirty: true)
54
57
  value = zache.get(:key, dirty: true) { expensive_calculation() }
55
58
  ```
56
59
 
57
- The entire API is documented
58
- [here](https://www.rubydoc.info/github/yegor256/zache/master/Zache).
60
+ You may use "eager" mode with a placeholder value to return immediately
61
+ while the calculation happens in the background.
62
+ The cache returns the placeholder instantly and spawns
63
+ a thread to calculate the actual value.
64
+ This is useful when you need to avoid blocking
65
+ while expensive operations complete:
66
+
67
+ ```ruby
68
+ # Returns 0 immediately, calculates in background
69
+ value = zache.get(:key, eager: true, placeholder: 0) { expensive_calculation() }
70
+ ```
71
+
72
+ The entire API is [documented][rubydoc].
59
73
  Here are some additional useful methods:
60
74
 
61
75
  ```ruby
@@ -80,12 +94,11 @@ zache.empty?
80
94
 
81
95
  ## How to contribute
82
96
 
83
- Read
84
- [these guidelines](https://www.yegor256.com/2014/04/15/github-guidelines.html).
85
- Make sure your build is green before you contribute
86
- your pull request. You will need to have
87
- [Ruby](https://www.ruby-lang.org/en/) 2.3+ and
88
- [Bundler](https://bundler.io/) installed. Then:
97
+ Read these [guidelines].
98
+ Make sure your build is green before you contribute your pull request.
99
+ You will need to have [Ruby](https://www.ruby-lang.org/en/) 2.3+ and
100
+ [Bundler](https://bundler.io/) installed.
101
+ Then:
89
102
 
90
103
  ```bash
91
104
  bundle update
@@ -93,3 +106,7 @@ bundle exec rake
93
106
  ```
94
107
 
95
108
  If it's clean and you don't see any error messages, submit your pull request.
109
+
110
+ [blog post]: https://www.yegor256.com/2019/02/05/zache.html
111
+ [rubydoc]: https://www.rubydoc.info/github/yegor256/zache/master/Zache
112
+ [guidelines]: https://www.yegor256.com/2014/04/15/github-guidelines.html
data/REUSE.toml CHANGED
@@ -4,6 +4,18 @@
4
4
  version = 1
5
5
  [[annotations]]
6
6
  path = [
7
+ ".DS_Store",
8
+ ".gitattributes",
9
+ ".gitignore",
10
+ ".pdd",
11
+ "**.json",
12
+ "**.md",
13
+ "**.png",
14
+ "**.svg",
15
+ "**.txt",
16
+ "**/.DS_Store",
17
+ "**/.gitignore",
18
+ "**/.pdd",
7
19
  "**/*.csv",
8
20
  "**/*.jpg",
9
21
  "**/*.json",
@@ -13,15 +25,8 @@ path = [
13
25
  "**/*.svg",
14
26
  "**/*.txt",
15
27
  "**/*.vm",
16
- "**/.DS_Store",
17
- "**/.gitignore",
18
- "**/.pdd",
19
28
  "**/CNAME",
20
29
  "**/Gemfile.lock",
21
- ".DS_Store",
22
- ".gitattributes",
23
- ".gitignore",
24
- ".pdd",
25
30
  "Gemfile.lock",
26
31
  "README.md",
27
32
  "renovate.json",
data/Rakefile CHANGED
@@ -8,8 +8,9 @@ require 'qbash'
8
8
  require 'rubygems'
9
9
  require 'rake'
10
10
  require 'rake/clean'
11
+ require 'shellwords'
11
12
 
12
- CLEAN = FileList['coverage']
13
+ CLEAN.include('coverage')
13
14
 
14
15
  def name
15
16
  @name ||= File.basename(Dir['*.gemspec'].first, '.*')
@@ -27,6 +28,9 @@ Rake::TestTask.new(:test) do |test|
27
28
  test.libs << 'lib' << 'test'
28
29
  test.pattern = 'test/**/test_*.rb'
29
30
  test.verbose = false
31
+ test.options = '--verbose' if ENV['VERBOSE']
32
+ # Disable minitest plugins on Windows to avoid gem conflicts
33
+ ENV['MT_NO_PLUGINS'] = '1' if OS.windows?
30
34
  end
31
35
 
32
36
  desc 'Run them via Ruby, one by one'
@@ -43,6 +47,7 @@ require 'yard'
43
47
  desc 'Build Yard documentation'
44
48
  YARD::Rake::YardocTask.new do |t|
45
49
  t.files = ['lib/**/*.rb']
50
+ t.options = ['--fail-on-warning']
46
51
  end
47
52
 
48
53
  require 'rubocop/rake_task'
data/lib/zache.rb CHANGED
@@ -29,8 +29,6 @@ class Zache
29
29
  end
30
30
 
31
31
  # Always returns the result of the block, never caches.
32
- # @param [Object] key Ignored
33
- # @param [Hash] opts Ignored
34
32
  # @yield Block that provides the value
35
33
  # @return [Object] The result of the block
36
34
  def get(*)
@@ -38,24 +36,19 @@ class Zache
38
36
  end
39
37
 
40
38
  # Always returns true regardless of the key.
41
- # @param [Object] key Ignored
42
- # @param [Hash] opts Ignored
43
39
  # @return [Boolean] Always returns true
44
40
  def exists?(*)
45
41
  true
46
42
  end
47
43
 
48
44
  # Always returns false.
49
- # @param [Object] key Ignored
45
+ # @param [Object] _key Ignored
50
46
  # @return [Boolean] Always returns false
51
47
  def locked?(_key)
52
48
  false
53
49
  end
54
50
 
55
51
  # No-op method that ignores the input.
56
- # @param [Object] key Ignored
57
- # @param [Object] value Ignored
58
- # @param [Hash] opts Ignored
59
52
  # @return [nil] Always returns nil
60
53
  def put(*); end
61
54
 
@@ -97,7 +90,7 @@ class Zache
97
90
  #
98
91
  # @return [Integer] Number of keys in the cache
99
92
  def size
100
- @hash.size
93
+ synchronize_all { @hash.size }
101
94
  end
102
95
 
103
96
  # Gets the value from the cache by the provided key.
@@ -122,30 +115,11 @@ class Zache
122
115
  # @return [Object] The cached value
123
116
  def get(key, lifetime: 2**32, dirty: false, placeholder: nil, eager: false, &block)
124
117
  if block_given?
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
118
+ return get_dirty_value(key) if should_return_dirty?(key, dirty)
119
+ return get_eager(key, lifetime, placeholder, &block) if eager
120
+ synchronize_one(key) { calc(key, lifetime, &block) }
140
121
  else
141
- rec = @hash[key]
142
- if expired?(key)
143
- return rec[:value] if dirty || @dirty
144
- @hash.delete(key)
145
- rec = nil
146
- end
147
- raise 'The key is absent in the cache' if rec.nil?
148
- rec[:value]
122
+ get_without_block(key, dirty)
149
123
  end
150
124
  end
151
125
 
@@ -157,12 +131,14 @@ class Zache
157
131
  # @param dirty [Boolean] Whether to consider expired values as existing
158
132
  # @return [Boolean] True if the key exists and is not expired (unless dirty is true)
159
133
  def exists?(key, dirty: false)
160
- rec = @hash[key]
161
- if expired?(key) && !dirty && !@dirty
162
- @hash.delete(key)
163
- rec = nil
134
+ synchronize_all do
135
+ rec = @hash[key]
136
+ if expired_unsafe?(key) && !dirty && !@dirty
137
+ @hash.delete(key)
138
+ rec = nil
139
+ end
140
+ !rec.nil?
164
141
  end
165
- !rec.nil?
166
142
  end
167
143
 
168
144
  # Checks whether the key exists in the cache and is expired. If the
@@ -171,8 +147,7 @@ class Zache
171
147
  # @param key [Object] The key to check in the cache
172
148
  # @return [Boolean] True if the key exists and is expired
173
149
  def expired?(key)
174
- rec = @hash[key]
175
- !rec.nil? && rec[:start] < Time.now - rec[:lifetime]
150
+ synchronize_all { expired_unsafe?(key) }
176
151
  end
177
152
 
178
153
  # Returns the modification time of the key, if it exists.
@@ -181,8 +156,10 @@ class Zache
181
156
  # @param key [Object] The key to get the modification time for
182
157
  # @return [Time] The modification time of the key or current time if key doesn't exist
183
158
  def mtime(key)
184
- rec = @hash[key]
185
- rec.nil? ? Time.now : rec[:start]
159
+ synchronize_all do
160
+ rec = @hash[key]
161
+ rec.nil? ? Time.now : rec[:start]
162
+ end
186
163
  end
187
164
 
188
165
  # Is key currently locked doing something?
@@ -190,7 +167,7 @@ class Zache
190
167
  # @param [Object] key The key to check
191
168
  # @return [Boolean] True if the cache is locked
192
169
  def locked?(key)
193
- @locks[key]&.locked?
170
+ synchronize_all { @locks[key]&.locked? }
194
171
  end
195
172
 
196
173
  # Put a value into the cache.
@@ -216,14 +193,19 @@ class Zache
216
193
  # @yield Block to call if the key is not found
217
194
  # @return [Object] The removed value or the result of the block
218
195
  def remove(key)
219
- synchronize_one(key) { @hash.delete(key) { yield if block_given? } }
196
+ result = synchronize_one(key) { @hash.delete(key) { yield if block_given? } }
197
+ synchronize_all { @locks.delete(key) }
198
+ result
220
199
  end
221
200
 
222
201
  # Remove all keys from the cache.
223
202
  #
224
203
  # @return [Hash] Empty hash
225
204
  def remove_all
226
- synchronize_all { @hash = {} }
205
+ synchronize_all do
206
+ @hash = {}
207
+ @locks = {}
208
+ end
227
209
  end
228
210
 
229
211
  # Remove all keys that match the block.
@@ -235,10 +217,10 @@ class Zache
235
217
  synchronize_all do
236
218
  count = 0
237
219
  @hash.each_key do |k|
238
- if yield(k)
239
- @hash.delete(k)
240
- count += 1
241
- end
220
+ next unless yield(k)
221
+ @hash.delete(k)
222
+ @locks.delete(k)
223
+ count += 1
242
224
  end
243
225
  count
244
226
  end
@@ -251,7 +233,11 @@ class Zache
251
233
  def clean
252
234
  synchronize_all do
253
235
  size_before = @hash.size
254
- @hash.delete_if { |key, _value| expired?(key) }
236
+ @hash.delete_if do |key, _value|
237
+ expired = expired_unsafe?(key)
238
+ @locks.delete(key) if expired
239
+ expired
240
+ end
255
241
  size_before - @hash.size
256
242
  end
257
243
  end
@@ -260,11 +246,89 @@ class Zache
260
246
  #
261
247
  # @return [Boolean] True if the cache is empty
262
248
  def empty?
263
- @hash.empty?
249
+ synchronize_all { @hash.empty? }
264
250
  end
265
251
 
266
252
  private
267
253
 
254
+ # Checks if dirty value should be returned for a locked key
255
+ # @param key [Object] The key to check
256
+ # @param dirty [Boolean] Whether dirty reads are allowed
257
+ # @return [Boolean] True if dirty value should be returned
258
+ def should_return_dirty?(key, dirty)
259
+ (dirty || @dirty) && locked?(key) && expired_value?(key)
260
+ end
261
+
262
+ # Checks if key has an expired value in cache
263
+ # @param key [Object] The key to check
264
+ # @return [Boolean] True if key exists and is expired
265
+ def expired_value?(key)
266
+ synchronize_all do
267
+ rec = @hash[key]
268
+ !rec.nil? && expired_unsafe?(key)
269
+ end
270
+ end
271
+
272
+ # Gets the dirty cached value without recalculation
273
+ # @param key [Object] The key to retrieve
274
+ # @return [Object] The cached value
275
+ def get_dirty_value(key)
276
+ synchronize_all { @hash[key][:value] }
277
+ end
278
+
279
+ # Handles eager mode get operation
280
+ # @param key [Object] The key to retrieve
281
+ # @param lifetime [Integer] Time in seconds until the key expires
282
+ # @param placeholder [Object] The placeholder to return immediately
283
+ # @yield Block that provides the value
284
+ # @return [Object] The placeholder value
285
+ def get_eager(key, lifetime, placeholder, &block)
286
+ return synchronize_all { @hash[key][:value] } if synchronize_all { @hash.key?(key) }
287
+
288
+ put(key, placeholder, lifetime: 0)
289
+ spawn_calculation_thread(key, lifetime, &block)
290
+ placeholder
291
+ end
292
+
293
+ # Spawns a background thread to calculate the value
294
+ # @param key [Object] The key to calculate for
295
+ # @param lifetime [Integer] Time in seconds until the key expires
296
+ # @yield Block that provides the value
297
+ def spawn_calculation_thread(key, lifetime, &block)
298
+ Thread.new do
299
+ synchronize_one(key) { calc(key, lifetime, &block) }
300
+ rescue StandardError => e
301
+ cleanup_failed_key(key)
302
+ raise e
303
+ end
304
+ end
305
+
306
+ # Cleans up a key after calculation failure
307
+ # @param key [Object] The key to clean up
308
+ def cleanup_failed_key(key)
309
+ synchronize_all do
310
+ @hash.delete(key)
311
+ @locks.delete(key)
312
+ end
313
+ end
314
+
315
+ # Gets value without a block (retrieval only mode)
316
+ # @param key [Object] The key to retrieve
317
+ # @param dirty [Boolean] Whether to return expired values
318
+ # @return [Object] The cached value
319
+ def get_without_block(key, dirty)
320
+ synchronize_all do
321
+ rec = @hash[key]
322
+ if expired_unsafe?(key)
323
+ return rec[:value] if dirty || @dirty
324
+ @hash.delete(key)
325
+ rec = nil
326
+ end
327
+ raise 'The key is absent in the cache' if rec.nil?
328
+ rec[:value]
329
+ end
330
+ end
331
+
268
332
  # Calculates or retrieves a cached value for the given key.
269
333
  # @param key [Object] The key to store the value under
270
334
  # @param lifetime [Integer] Time in seconds until the key expires
@@ -272,15 +336,25 @@ class Zache
272
336
  # @return [Object] The cached or newly calculated value
273
337
  def calc(key, lifetime)
274
338
  rec = @hash[key]
275
- rec = nil if expired?(key)
339
+ rec = nil if expired_unsafe?(key)
276
340
  if rec.nil?
277
- @hash[key] = {
341
+ rec = {
278
342
  value: yield,
279
343
  start: Time.now,
280
344
  lifetime: lifetime
281
345
  }
346
+ @hash[key] = rec
282
347
  end
283
- @hash[key][:value]
348
+ rec[:value]
349
+ end
350
+
351
+ # Internal method that checks if a key is expired without acquiring locks.
352
+ # This should only be called from within a synchronized block.
353
+ # @param key [Object] The key to check in the cache
354
+ # @return [Boolean] True if the key exists and is expired
355
+ def expired_unsafe?(key)
356
+ rec = @hash[key]
357
+ !rec.nil? && rec[:lifetime] && rec[:start] < Time.now - rec[:lifetime]
284
358
  end
285
359
 
286
360
  # Executes a block within a synchronized context if sync is enabled.
@@ -299,9 +373,9 @@ class Zache
299
373
  # @return [Object] The result of the block
300
374
  def synchronize_one(key, &block)
301
375
  return yield unless @sync
302
- @mutex.synchronize do
376
+ mtx = @mutex.synchronize do
303
377
  @locks[key] ||= Mutex.new
304
378
  end
305
- @locks[key].synchronize(&block)
379
+ mtx.synchronize(&block)
306
380
  end
307
381
  end
data/zache.gemspec CHANGED
@@ -8,14 +8,14 @@ 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.15.0' # Version should be updated before release
11
+ s.version = '0.15.2' # 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'
15
15
  s.authors = ['Yegor Bugayenko']
16
16
  s.email = 'yegor256@gmail.com'
17
17
  s.homepage = 'https://github.com/yegor256/zache'
18
- s.files = `git ls-files`.split($RS)
18
+ s.files = `git ls-files | grep -v -E '^(test/|\\.|renovate)'`.split($RS)
19
19
  s.rdoc_options = ['--charset=UTF-8']
20
20
  s.extra_rdoc_files = ['README.md']
21
21
  s.metadata['rubygems_mfa_required'] = 'true'