iknow_cache 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +25 -0
- data/Rakefile +34 -0
- data/lib/iknow_cache.rb +251 -0
- data/lib/iknow_cache/version.rb +3 -0
- metadata +90 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 14a5fbfe4eb06e835259e354c8a8fdfb9bc82c2d34bfe6ea62fc8269144ca8a5
|
4
|
+
data.tar.gz: 22da19d2f809d5d092298d47d1b703a146ff6d143d7ac83c0191a8fae98eca36
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 80d4ff3de7d25e388217e20922971d4378970591bc6a8c57672e1c4745e1451e57a2105f16a468c7845ce4103dae8b62a5f8abe5cf6ab66713a4fa9ec23aac8f
|
7
|
+
data.tar.gz: c23861a1464d8e2ea0c6d711eac3b8517253f745cda66bb9c612b4c67be11f669fff637c0e5eeb49c967600d38b050692895104c7d84500d5d8aa78a4f3c9cc0
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2017 Chris Andreae
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
[![Build Status](https://travis-ci.org/iknow/iknow_cache.svg?branch=master)](https://travis-ci.org/iknow/iknow_cache)
|
2
|
+
|
3
|
+
# IknowCache
|
4
|
+
|
5
|
+
iKnow's versioned nested cache backed by Rails cache
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
gem 'iknow_cache'
|
12
|
+
```
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
```bash
|
16
|
+
$ bundle
|
17
|
+
```
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
```bash
|
21
|
+
$ gem install iknow_cache
|
22
|
+
```
|
23
|
+
|
24
|
+
## License
|
25
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'IknowCache'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
|
21
|
+
|
22
|
+
require 'bundler/gem_tasks'
|
23
|
+
|
24
|
+
require 'rake/testtask'
|
25
|
+
|
26
|
+
Rake::TestTask.new(:test) do |t|
|
27
|
+
t.libs << 'lib'
|
28
|
+
t.libs << 'test'
|
29
|
+
t.pattern = 'test/**/*_test.rb'
|
30
|
+
t.verbose = false
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
task default: :test
|
data/lib/iknow_cache.rb
ADDED
@@ -0,0 +1,251 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class IknowCache
|
4
|
+
def self.register_group(name, key_name, default_options: nil, static_version: 1)
|
5
|
+
group = CacheGroup.new(nil, name, key_name, default_options, static_version)
|
6
|
+
yield group if block_given?
|
7
|
+
group
|
8
|
+
end
|
9
|
+
|
10
|
+
class CacheGroup
|
11
|
+
attr_reader :parent, :default_options, :name, :key_name, :key
|
12
|
+
|
13
|
+
def initialize(parent, name, key_name, default_options, static_version)
|
14
|
+
@parent = parent
|
15
|
+
@name = name
|
16
|
+
@key_name = key_name
|
17
|
+
@key = Struct.new(*parent.try { |p| p.key.members }, key_name)
|
18
|
+
@default_options = IknowCache.merge_options(parent&.default_options, default_options).try { |x| x.dup.freeze }
|
19
|
+
@static_version = static_version
|
20
|
+
|
21
|
+
@caches = []
|
22
|
+
@children = []
|
23
|
+
end
|
24
|
+
|
25
|
+
def register_child_group(name, key_name, default_options: nil, static_version: 1)
|
26
|
+
group = CacheGroup.new(self, name, key_name, default_options, static_version)
|
27
|
+
@children << group
|
28
|
+
yield group if block_given?
|
29
|
+
group
|
30
|
+
end
|
31
|
+
|
32
|
+
def register_cache(name, cache_options: nil)
|
33
|
+
c = Cache.new(self, name, cache_options)
|
34
|
+
@caches << c
|
35
|
+
c
|
36
|
+
end
|
37
|
+
|
38
|
+
# Clear this key in all Caches in this CacheGroup.
|
39
|
+
# It is an error to do this if this CacheGroup has an statically versioned child, as that child cannot be invalidated.
|
40
|
+
def delete_all(key, parent_path: nil)
|
41
|
+
@caches.each do |cache|
|
42
|
+
cache.delete(key, parent_path: parent_path)
|
43
|
+
end
|
44
|
+
|
45
|
+
@children.each do |child_group|
|
46
|
+
child_group.invalidate_cache_group(key)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Clear all keys in this cache group (for the given parent),
|
51
|
+
# invalidating all caches in it and its children
|
52
|
+
def invalidate_cache_group(parent_key = nil)
|
53
|
+
parent_path = self.parent_path(parent_key)
|
54
|
+
Rails.cache.increment(version_path_string(parent_path))
|
55
|
+
end
|
56
|
+
|
57
|
+
# Fetch the path for this cache. We allow the parent_path to be precomputed
|
58
|
+
# to save hitting the cache multiple times for its version.
|
59
|
+
def path(key, parent_path = nil)
|
60
|
+
if key.nil? || key[self.key_name].nil?
|
61
|
+
raise ArgumentError.new("Missing required key '#{self.key_name}' for cache '#{self.name}'")
|
62
|
+
end
|
63
|
+
|
64
|
+
key_value = key[self.key_name]
|
65
|
+
parent_path ||= self.parent_path(key)
|
66
|
+
version = self.version(parent_path)
|
67
|
+
path_string(parent_path, version, key_value)
|
68
|
+
end
|
69
|
+
|
70
|
+
ROOT_PATH = 'IknowCache'
|
71
|
+
|
72
|
+
def parent_path(parent_key = nil)
|
73
|
+
if parent.nil?
|
74
|
+
ROOT_PATH
|
75
|
+
else
|
76
|
+
parent.path(parent_key)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def version(parent_path)
|
81
|
+
Rails.cache.fetch(version_path_string(parent_path), raw: true) { 1 }
|
82
|
+
end
|
83
|
+
|
84
|
+
# compute multiple paths at once: returns { key => path }
|
85
|
+
def path_multi(keys)
|
86
|
+
# compute parent path for each key
|
87
|
+
parent_paths = self.parent_path_multi(keys)
|
88
|
+
|
89
|
+
# and versions for each parent path
|
90
|
+
versions = self.version_multi(parent_paths.values.uniq)
|
91
|
+
|
92
|
+
# update parent_paths with our paths
|
93
|
+
keys.each do |key|
|
94
|
+
parent_path = parent_paths[key]
|
95
|
+
version = versions[parent_path]
|
96
|
+
key_value = key[self.key_name]
|
97
|
+
|
98
|
+
unless key_value
|
99
|
+
raise ArgumentError.new("Required cache key missing: #{self.key_name}")
|
100
|
+
end
|
101
|
+
|
102
|
+
parent_paths[key] = path_string(parent_path, version, key_value)
|
103
|
+
end
|
104
|
+
|
105
|
+
parent_paths
|
106
|
+
end
|
107
|
+
|
108
|
+
# look up multiple parent paths at once, returns { key => parent_path }
|
109
|
+
def parent_path_multi(parent_keys = nil)
|
110
|
+
if parent.nil?
|
111
|
+
parent_keys.each_with_object({}) { |k, h| h[k] = ROOT_PATH }
|
112
|
+
else
|
113
|
+
parent.path_multi(parent_keys)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Look up multiple versions at once, returns { parent_path => version }
|
118
|
+
def version_multi(parent_paths)
|
119
|
+
# compute version paths
|
120
|
+
version_by_pp = parent_paths.each_with_object({}) { |pp, h| h[pp] = version_path_string(pp) }
|
121
|
+
version_paths = version_by_pp.values
|
122
|
+
|
123
|
+
# look up versions in cache
|
124
|
+
versions = Rails.cache.read_multi(*version_paths, raw: true)
|
125
|
+
|
126
|
+
version_paths.each do |vp|
|
127
|
+
next if versions.has_key?(vp)
|
128
|
+
|
129
|
+
versions[vp] = Rails.cache.fetch(vp, raw: true) { 1 }
|
130
|
+
end
|
131
|
+
|
132
|
+
# swap in the versions
|
133
|
+
parent_paths.each do |pp|
|
134
|
+
vp = version_by_pp[pp]
|
135
|
+
version = versions[vp]
|
136
|
+
version_by_pp[pp] = version
|
137
|
+
end
|
138
|
+
|
139
|
+
version_by_pp
|
140
|
+
end
|
141
|
+
|
142
|
+
private
|
143
|
+
|
144
|
+
def path_string(parent_path, version, value)
|
145
|
+
"#{parent_path}/#{name}/#{@static_version}/#{version}/#{value}"
|
146
|
+
end
|
147
|
+
|
148
|
+
def version_path_string(parent_path)
|
149
|
+
"#{parent_path}/#{name}/_version"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
class Cache
|
154
|
+
DEBUG = false
|
155
|
+
|
156
|
+
attr_reader :name, :cache_options, :cache_group
|
157
|
+
|
158
|
+
def initialize(cache_group, name, cache_options)
|
159
|
+
@cache_group = cache_group
|
160
|
+
@name = name
|
161
|
+
@cache_options = IknowCache.merge_options(cache_group.default_options, cache_options).try { |x| x.dup.freeze }
|
162
|
+
end
|
163
|
+
|
164
|
+
def fetch(key, parent_path: nil, **options, &block)
|
165
|
+
p = path(key, parent_path)
|
166
|
+
Rails.logger.debug("Cache Fetch: #{p}") if DEBUG
|
167
|
+
v = Rails.cache.fetch(p, IknowCache.merge_options(cache_options, options), &block)
|
168
|
+
Rails.logger.debug("=> #{v.inspect}") if DEBUG
|
169
|
+
v
|
170
|
+
end
|
171
|
+
|
172
|
+
def read(key, parent_path: nil, **options)
|
173
|
+
p = path(key, parent_path)
|
174
|
+
Rails.logger.debug("Cache Read: #{p}") if DEBUG
|
175
|
+
v = Rails.cache.read(p, IknowCache.merge_options(cache_options, options))
|
176
|
+
Rails.logger.debug("=> #{v.inspect}") if DEBUG
|
177
|
+
v
|
178
|
+
end
|
179
|
+
|
180
|
+
def write(key, value, parent_path: nil, **options)
|
181
|
+
p = path(key, parent_path)
|
182
|
+
if DEBUG
|
183
|
+
Rails.logger.debug("Cache Store: #{p} (#{IknowCache.merge_options(cache_options, options).inspect})")
|
184
|
+
Rails.logger.debug("<= #{value.inspect}")
|
185
|
+
end
|
186
|
+
Rails.cache.write(p, value, IknowCache.merge_options(cache_options, options))
|
187
|
+
end
|
188
|
+
|
189
|
+
def delete(key, parent_path: nil, **options)
|
190
|
+
p = path(key, parent_path)
|
191
|
+
Rails.logger.debug("Cache Delete: #{p}") if DEBUG
|
192
|
+
Rails.cache.delete(p, IknowCache.merge_options(cache_options, options))
|
193
|
+
end
|
194
|
+
|
195
|
+
def read_multi(keys)
|
196
|
+
return {} if keys.blank?
|
197
|
+
|
198
|
+
key_paths = path_multi(keys)
|
199
|
+
path_keys = key_paths.invert
|
200
|
+
|
201
|
+
Rails.logger.debug("Cache Multi-Read: #{key_paths.values.inspect}") if DEBUG
|
202
|
+
raw = Rails.cache.read_multi(*key_paths.values)
|
203
|
+
vs = raw.each_with_object({}) do |(path, value), h|
|
204
|
+
h[path_keys[path]] = value
|
205
|
+
end
|
206
|
+
Rails.logger.debug("=> #{vs.inspect}") if DEBUG
|
207
|
+
vs
|
208
|
+
end
|
209
|
+
|
210
|
+
def write_multi(entries, options = nil)
|
211
|
+
return {} if entries.blank?
|
212
|
+
|
213
|
+
key_paths = path_multi(entries.keys)
|
214
|
+
options = IknowCache.merge_options(cache_options, options)
|
215
|
+
|
216
|
+
entries.each do |key, value|
|
217
|
+
Rails.logger.debug("Cache Multi-Write: #{key_paths[key]}") if DEBUG
|
218
|
+
Rails.cache.write(key_paths[key], value, options)
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
delegate :key, to: :cache_group
|
223
|
+
|
224
|
+
private
|
225
|
+
|
226
|
+
def path(key, parent_path = nil)
|
227
|
+
group_path = @cache_group.path(key, parent_path)
|
228
|
+
path_string(group_path)
|
229
|
+
end
|
230
|
+
|
231
|
+
def path_multi(keys)
|
232
|
+
@cache_group.path_multi(keys).each_with_object({}) do |(key, group_path), h|
|
233
|
+
h[key] = path_string(group_path)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
def path_string(group_path)
|
238
|
+
"#{group_path}/#{self.name}"
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def self.merge_options(parent_options, options)
|
243
|
+
if parent_options.blank?
|
244
|
+
options
|
245
|
+
elsif options.blank?
|
246
|
+
parent_options
|
247
|
+
else
|
248
|
+
parent_options.merge(options)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
metadata
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: iknow_cache
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Chris Andreae
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-04-08 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '5.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '5.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: sqlite3
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: appraisal
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description:
|
56
|
+
email:
|
57
|
+
- chris@bibo.com.ph
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- MIT-LICENSE
|
63
|
+
- README.md
|
64
|
+
- Rakefile
|
65
|
+
- lib/iknow_cache.rb
|
66
|
+
- lib/iknow_cache/version.rb
|
67
|
+
homepage: https://github.com/iknow/iknow_cache
|
68
|
+
licenses:
|
69
|
+
- MIT
|
70
|
+
metadata: {}
|
71
|
+
post_install_message:
|
72
|
+
rdoc_options: []
|
73
|
+
require_paths:
|
74
|
+
- lib
|
75
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '0'
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '0'
|
85
|
+
requirements: []
|
86
|
+
rubygems_version: 3.0.3
|
87
|
+
signing_key:
|
88
|
+
specification_version: 4
|
89
|
+
summary: iKnow's versioned nested cache
|
90
|
+
test_files: []
|