thread_cache 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,2 @@
1
+ coverage/
2
+ *.gem
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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
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
+ ```
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ThreadCache
4
+ VERSION = '1.0.0'
5
+ end
@@ -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
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'simplecov'
4
+ SimpleCov.start
5
+
6
+ require 'rspec'
7
+ require 'active_support/testing/time_helpers'
8
+ require 'pry-byebug'
9
+
10
+ RSpec.configure do |config|
11
+ config.include ActiveSupport::Testing::TimeHelpers
12
+ end