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 +4 -4
- data/Gemfile +5 -4
- data/Gemfile.lock +46 -32
- data/README.md +36 -19
- data/REUSE.toml +12 -7
- data/Rakefile +6 -1
- data/lib/zache.rb +129 -55
- data/zache.gemspec +2 -2
- metadata +3 -21
- data/.0pdd.yml +0 -12
- data/.github/workflows/actionlint.yml +0 -25
- data/.github/workflows/codecov.yml +0 -25
- data/.github/workflows/copyrights.yml +0 -15
- data/.github/workflows/license.yml +0 -42
- data/.github/workflows/markdown-lint.yml +0 -23
- data/.github/workflows/pdd.yml +0 -19
- data/.github/workflows/rake.yml +0 -28
- data/.github/workflows/reuse.yml +0 -19
- data/.github/workflows/xcop.yml +0 -21
- data/.github/workflows/yamllint.yml +0 -19
- data/.gitignore +0 -10
- data/.pdd +0 -5
- data/.rubocop.yml +0 -40
- data/.rultor.yml +0 -26
- data/renovate.json +0 -6
- data/test/test__helper.rb +0 -31
- data/test/test_zache.rb +0 -311
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e9cc0b6c8fece285753b4a7bc12bc296f9cab08b7adacee09dd888f556263e4c
|
|
4
|
+
data.tar.gz: 722f973b74565b70225b44f1de8f6c63ddccf18bbb534ba31a9b1c4c24bbb60d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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', '
|
|
17
|
-
gem 'rubocop-performance', '
|
|
18
|
-
gem 'rubocop-rake', '
|
|
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', '~>
|
|
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.
|
|
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
|
|
16
|
-
loog (
|
|
17
|
-
tago (
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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.
|
|
30
|
-
parser (3.3.
|
|
33
|
+
parallel (1.27.0)
|
|
34
|
+
parser (3.3.10.0)
|
|
31
35
|
ast (~> 2.4.1)
|
|
32
36
|
racc
|
|
33
|
-
prism (1.
|
|
34
|
-
|
|
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.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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.
|
|
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.
|
|
66
|
+
rubocop-ast (1.48.0)
|
|
56
67
|
parser (>= 3.3.7.2)
|
|
57
68
|
prism (~> 1.4)
|
|
58
|
-
rubocop-minitest (0.38.
|
|
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.
|
|
73
|
+
rubocop-performance (1.26.1)
|
|
63
74
|
lint_roller (~> 1.1)
|
|
64
75
|
rubocop (>= 1.75.0, < 2.0)
|
|
65
|
-
rubocop-ast (>= 1.
|
|
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 (
|
|
85
|
+
simplecov-cobertura (3.1.0)
|
|
75
86
|
rexml
|
|
76
87
|
simplecov (~> 0.19)
|
|
77
|
-
simplecov-html (0.13.
|
|
88
|
+
simplecov-html (0.13.2)
|
|
78
89
|
simplecov_json_formatter (0.1.4)
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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 (
|
|
106
|
-
rubocop-performance (
|
|
107
|
-
rubocop-rake (
|
|
119
|
+
rubocop-minitest (~> 0.38)
|
|
120
|
+
rubocop-performance (~> 1.25)
|
|
121
|
+
rubocop-rake (~> 0.7)
|
|
108
122
|
simplecov (~> 0.22)
|
|
109
|
-
simplecov-cobertura (~>
|
|
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
|
[](https://hitsofcode.com/view/github/yegor256/zache)
|
|
14
14
|
|
|
15
15
|
This is a simple Ruby gem for in-memory caching.
|
|
16
|
-
Read
|
|
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
|
|
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.
|
|
37
|
-
|
|
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
|
|
45
|
-
calculation is in progress.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
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]
|
|
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
|
|
126
|
-
if eager
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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
|
-
|
|
185
|
-
|
|
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
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
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
|
|
339
|
+
rec = nil if expired_unsafe?(key)
|
|
276
340
|
if rec.nil?
|
|
277
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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'
|