thread_cache 1.0.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 +7 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +48 -0
- data/Gemfile +5 -0
- data/LICENSE +21 -0
- data/README.md +48 -0
- data/lib/thread_cache/version.rb +5 -0
- data/lib/thread_cache.rb +264 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/thread_cache_spec.rb +1240 -0
- data/thread_cache.gemspec +38 -0
- metadata +154 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 5c5461aafc736ae8c0b33ac3da0a1c1db3e49a28b4e7aac55dd10e2dca73b933
|
4
|
+
data.tar.gz: d9802d83b6663e6733f9e795f64cd7f1bd9b40e82397cc3168accd247eec5c18
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ec97f1f5c73528c13fe7d16b674a0e913622e529b3d06eb372a00210e36b4a54d46be42f405aba11ca018de24e0bdf72ed6b106130d590da4bfd4121abb3846b
|
7
|
+
data.tar.gz: '09f4e1ca3e71c59fd8db2ba9f276a2bfa3c4d2924e0205c6791539795b130f121c6a8edfa3201a5208d172f95e5b13a1671340a9ca6111f07231c0cf973f5683'
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require: rubocop-performance
|
2
|
+
|
3
|
+
AllCops:
|
4
|
+
TargetRubyVersion: 2.5
|
5
|
+
SuggestExtensions: false
|
6
|
+
NewCops: enable
|
7
|
+
|
8
|
+
Metrics/BlockLength:
|
9
|
+
Max: 30
|
10
|
+
Exclude:
|
11
|
+
- 'spec/**/*.rb'
|
12
|
+
Metrics/MethodLength:
|
13
|
+
Max: 30
|
14
|
+
Metrics/ClassLength:
|
15
|
+
Max: 200
|
16
|
+
Metrics/AbcSize:
|
17
|
+
Max: 20
|
18
|
+
|
19
|
+
Layout/LineLength:
|
20
|
+
Max: 120
|
21
|
+
Layout/FirstHashElementIndentation:
|
22
|
+
Enabled: false
|
23
|
+
Layout/HashAlignment:
|
24
|
+
Enabled: false
|
25
|
+
|
26
|
+
Naming/AccessorMethodName:
|
27
|
+
Enabled: false
|
28
|
+
Naming/MethodParameterName:
|
29
|
+
Enabled: false
|
30
|
+
|
31
|
+
Style/Documentation:
|
32
|
+
Enabled: false
|
33
|
+
Style/WordArray:
|
34
|
+
Enabled: false
|
35
|
+
Style/SymbolArray:
|
36
|
+
Enabled: false
|
37
|
+
Style/NumericLiterals:
|
38
|
+
Enabled: false
|
39
|
+
Style/StringLiterals:
|
40
|
+
Enabled: false
|
41
|
+
Style/TrailingCommaInArrayLiteral:
|
42
|
+
Enabled: false
|
43
|
+
Style/TrailingCommaInHashLiteral:
|
44
|
+
Enabled: false
|
45
|
+
Style/NegatedIf:
|
46
|
+
Enabled: false
|
47
|
+
Style/IfUnlessModifier:
|
48
|
+
Enabled: false
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2021 Elias Rodrigues
|
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 NONINFRINGEMENT. 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.
|
data/README.md
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# ThreadCache
|
2
|
+
|
3
|
+
A simple thread-local cache store
|
4
|
+
|
5
|
+
## Install
|
6
|
+
|
7
|
+
Install it from rubygems.org in your terminal:
|
8
|
+
|
9
|
+
```sh
|
10
|
+
gem install thread_cache
|
11
|
+
```
|
12
|
+
|
13
|
+
Or via `Gemfile` in your project:
|
14
|
+
|
15
|
+
```sh
|
16
|
+
source 'https://rubygems.org'
|
17
|
+
|
18
|
+
gem 'thread_cache', '~> 1.0'
|
19
|
+
```
|
20
|
+
|
21
|
+
Or build and install the gem locally:
|
22
|
+
|
23
|
+
```sh
|
24
|
+
gem build thread_cache.gemspec
|
25
|
+
gem install thread_cache-1.0.0.gem
|
26
|
+
```
|
27
|
+
|
28
|
+
Require it in your Ruby code and the `ThreadCache` class will be available:
|
29
|
+
|
30
|
+
```rb
|
31
|
+
require 'thread_cache'
|
32
|
+
```
|
33
|
+
|
34
|
+
## Tests
|
35
|
+
|
36
|
+
Run tests with:
|
37
|
+
|
38
|
+
```sh
|
39
|
+
bundle exec rspec
|
40
|
+
```
|
41
|
+
|
42
|
+
## Linter
|
43
|
+
|
44
|
+
Check your code with:
|
45
|
+
|
46
|
+
```sh
|
47
|
+
bundle exec rubocop
|
48
|
+
```
|
data/lib/thread_cache.rb
ADDED
@@ -0,0 +1,264 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './thread_cache/version'
|
4
|
+
|
5
|
+
# A simple thread-local cache store
|
6
|
+
#
|
7
|
+
# options:
|
8
|
+
# namespace: Thread attribute name to use as data store (String or Symbol)
|
9
|
+
# expires_in: Default number of seconds to expire values (number)
|
10
|
+
# skip_nil: Whether or not to cache nil values by default (boolean)
|
11
|
+
class ThreadCache
|
12
|
+
def initialize(options = {})
|
13
|
+
@namespace = options.fetch(:namespace, 'thread_cache')
|
14
|
+
@expires_in = options.fetch(:expires_in, 60)
|
15
|
+
@skip_nil = options.fetch(:skip_nil, false)
|
16
|
+
|
17
|
+
data_store
|
18
|
+
end
|
19
|
+
|
20
|
+
def clear
|
21
|
+
data_store.clear
|
22
|
+
end
|
23
|
+
|
24
|
+
def exist?(key)
|
25
|
+
data_store.key?(key)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Writes a key-value pair to the data store.
|
29
|
+
#
|
30
|
+
# NOTE: value is written as it is given, it does not duplicate nor serialize it.
|
31
|
+
#
|
32
|
+
# key: (String or Symbol)
|
33
|
+
# value: (any type)
|
34
|
+
# options:
|
35
|
+
# version: sets the value's version (any type)
|
36
|
+
# expires_in: overrides the default expires_in
|
37
|
+
# skip_nil: overrides the default skip_nil
|
38
|
+
def write(key, value, options = {})
|
39
|
+
perform_write(key, value, parse_options(options))
|
40
|
+
end
|
41
|
+
|
42
|
+
# Reads a value from the data store using key.
|
43
|
+
#
|
44
|
+
# options:
|
45
|
+
# version: defines the version to read the value
|
46
|
+
def read(key, options = {})
|
47
|
+
options = parse_options(options)
|
48
|
+
|
49
|
+
entry = data_store[key]
|
50
|
+
value, _error = validate(key, entry, options[:version])
|
51
|
+
value
|
52
|
+
end
|
53
|
+
|
54
|
+
# Fetches a value from the data store using key.
|
55
|
+
# If a valid value is not found, it writes the value returned from the given block.
|
56
|
+
#
|
57
|
+
# options:
|
58
|
+
# force: writes the value even when a valid value is found (boolean)
|
59
|
+
# version: sets the value's version
|
60
|
+
# expires_in: overrides the default expires_in
|
61
|
+
# skip_nil: overrides the default skip_nil
|
62
|
+
def fetch(key, options = {}, &block)
|
63
|
+
options = parse_options(options)
|
64
|
+
|
65
|
+
perform_fetch(key, options, &block)
|
66
|
+
end
|
67
|
+
|
68
|
+
def delete(key)
|
69
|
+
entry = data_store.delete(key)
|
70
|
+
|
71
|
+
!entry.nil?
|
72
|
+
end
|
73
|
+
|
74
|
+
def write_multi(keys_and_values, options = {})
|
75
|
+
options = parse_options_multi(keys_and_values.keys, options)
|
76
|
+
|
77
|
+
keys_and_values.each do |key, value|
|
78
|
+
opts = {
|
79
|
+
version: options[:version][key],
|
80
|
+
expires_in: options[:expires_in][key],
|
81
|
+
skip_nil: options[:skip_nil][key],
|
82
|
+
}
|
83
|
+
perform_write(key, value, opts)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def read_multi(keys, options = {})
|
88
|
+
options = parse_options_multi(keys, options)
|
89
|
+
|
90
|
+
entries = data_store.slice(*keys)
|
91
|
+
keys.each_with_object({}) do |key, acc|
|
92
|
+
value, _error = validate(key, entries[key], options[:version][key])
|
93
|
+
acc[key] = value
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def fetch_multi(keys, options = {}, &block)
|
98
|
+
options = parse_options_multi(keys, options)
|
99
|
+
|
100
|
+
keys.each_with_object({}) do |key, acc|
|
101
|
+
opts = {
|
102
|
+
force: options[:force][key],
|
103
|
+
version: options[:version][key],
|
104
|
+
expires_in: options[:expires_in][key],
|
105
|
+
skip_nil: options[:skip_nil][key],
|
106
|
+
}
|
107
|
+
acc[key] = perform_fetch(key, opts, &block)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def delete_multi(keys)
|
112
|
+
keys.map { |key| delete(key) }
|
113
|
+
end
|
114
|
+
|
115
|
+
def delete_matched(pattern)
|
116
|
+
data_store.keys.each_with_object([]) do |key, acc|
|
117
|
+
if key.match?(pattern)
|
118
|
+
delete(key)
|
119
|
+
acc << key
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Validates all entries, consequently deleting the ones that have expired.
|
125
|
+
#
|
126
|
+
# options:
|
127
|
+
# version: defines the version to read the values
|
128
|
+
def cleanup(options = {})
|
129
|
+
options = parse_options(options)
|
130
|
+
|
131
|
+
data_store.map.each_with_object([]) do |(key, entry), acc|
|
132
|
+
_value, error = validate(key, entry, options[:version])
|
133
|
+
acc << key if !error.nil?
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def increment(key, amount = 1, options = {})
|
138
|
+
options = parse_options(options)
|
139
|
+
|
140
|
+
perform_add(key, amount, options)
|
141
|
+
end
|
142
|
+
|
143
|
+
def decrement(key, amount = 1, options = {})
|
144
|
+
options = parse_options(options)
|
145
|
+
|
146
|
+
perform_add(key, -amount, options)
|
147
|
+
end
|
148
|
+
|
149
|
+
alias exists? exist?
|
150
|
+
|
151
|
+
alias set write
|
152
|
+
alias set_multi write_multi
|
153
|
+
|
154
|
+
alias get read
|
155
|
+
alias get_multi read_multi
|
156
|
+
|
157
|
+
alias remove delete
|
158
|
+
alias remove_multi delete_multi
|
159
|
+
alias remove_matched delete_matched
|
160
|
+
|
161
|
+
alias incr increment
|
162
|
+
alias decr decrement
|
163
|
+
|
164
|
+
private
|
165
|
+
|
166
|
+
def data_store
|
167
|
+
Thread.current[@namespace] ||= {}
|
168
|
+
end
|
169
|
+
|
170
|
+
def build_entry(value, version, expires_in)
|
171
|
+
{
|
172
|
+
value: value,
|
173
|
+
version: version,
|
174
|
+
expires_in: expires_in&.to_f,
|
175
|
+
created_at: current_unix_time,
|
176
|
+
}
|
177
|
+
end
|
178
|
+
|
179
|
+
def perform_write(key, value, options)
|
180
|
+
return if value.nil? && options[:skip_nil]
|
181
|
+
|
182
|
+
data_store[key] = build_entry(value, options[:version], options[:expires_in])
|
183
|
+
value
|
184
|
+
end
|
185
|
+
|
186
|
+
def perform_fetch(key, options)
|
187
|
+
if options[:force]
|
188
|
+
perform_write(key, yield(key), options)
|
189
|
+
else
|
190
|
+
entry = data_store[key]
|
191
|
+
value, error = validate(key, entry, options[:version])
|
192
|
+
|
193
|
+
if error.nil?
|
194
|
+
value
|
195
|
+
else
|
196
|
+
perform_write(key, yield(key), options)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def perform_add(key, amount, options = {})
|
202
|
+
entry = data_store[key]
|
203
|
+
value, _error = validate(key, entry, options[:version])
|
204
|
+
|
205
|
+
perform_write(key, value.to_i + amount, options)
|
206
|
+
end
|
207
|
+
|
208
|
+
def validate(key, entry, version)
|
209
|
+
if entry.nil?
|
210
|
+
[nil, 'not found']
|
211
|
+
elsif expired?(entry) || mismatched?(entry, version)
|
212
|
+
delete(key)
|
213
|
+
[nil, 'expired or mismatched']
|
214
|
+
else
|
215
|
+
[entry[:value], nil]
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def expired?(entry)
|
220
|
+
entry[:expires_in] && entry[:created_at] + entry[:expires_in] <= current_unix_time
|
221
|
+
end
|
222
|
+
|
223
|
+
def mismatched?(entry, version)
|
224
|
+
entry[:version] && version && entry[:version] != version
|
225
|
+
end
|
226
|
+
|
227
|
+
def current_unix_time
|
228
|
+
Time.now.to_f
|
229
|
+
end
|
230
|
+
|
231
|
+
def options_defaults
|
232
|
+
@options_defaults ||= {
|
233
|
+
force: nil,
|
234
|
+
version: nil,
|
235
|
+
expires_in: @expires_in,
|
236
|
+
skip_nil: @skip_nil,
|
237
|
+
}
|
238
|
+
end
|
239
|
+
|
240
|
+
def parse_options(options)
|
241
|
+
options_defaults.each_with_object({}) do |(option_name, default_value), acc|
|
242
|
+
acc[option_name] = options.fetch(option_name, default_value)
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
def parse_options_multi(keys, options)
|
247
|
+
options_defaults.each_with_object({}) do |(option_name, default_value), acc|
|
248
|
+
option_value = options.fetch(option_name, default_value)
|
249
|
+
|
250
|
+
acc[option_name] = option_value_for_multi(keys, option_value, default_value)
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
def option_value_for_multi(keys, option_value, fallback_value)
|
255
|
+
case option_value
|
256
|
+
when Hash
|
257
|
+
(Hash.new { fallback_value }).merge(option_value)
|
258
|
+
when Array
|
259
|
+
(Hash.new { fallback_value }).merge(keys.take(option_value.size).zip(option_value).to_h)
|
260
|
+
else
|
261
|
+
Hash.new { option_value }
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
data/spec/spec_helper.rb
ADDED