activesupport_cache_database 0.1.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/.editorconfig +9 -0
- data/.gitignore +3 -0
- data/.rubocop.yml +7 -0
- data/.travis.yml +15 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +78 -0
- data/LICENSE +13 -0
- data/README.md +44 -0
- data/Rakefile +9 -0
- data/activesupport_cache_database.gemspec +26 -0
- data/lib/active_support/cache/database_store.rb +111 -0
- data/lib/active_support/cache/database_store/migration.rb +18 -0
- data/lib/active_support/cache/database_store/model.rb +24 -0
- data/lib/activesupport_cache_database.rb +1 -0
- data/spec/active_support/cache/database_store_spec.rb +328 -0
- data/spec/spec_helper.rb +30 -0
- metadata +187 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: '029071110552b3cd5a259762e7b3a38b57c67cb30e19daca6d33b90c79963b38'
|
4
|
+
data.tar.gz: 96686b4c427b8eac4cb7a5c0474e8b2d5b867bf06654379594628620fbf7e27e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 02a282d23cf3b0c48022ffa23585e876d9c8a4fd64da959f9eedc1cbb86248e2463755dfb0de9f7b8f85ffa8b50461354a97809d37246a69b66d9bbe2345b288
|
7
|
+
data.tar.gz: 93b25ebd6d14d93cc3cc794fb785a94d61c25b10c56e8df08b33e13543b536ea6c93c9bd05b617eed793a7c6fcc5568195bdee85093ba30158e2760043ae764b
|
data/.editorconfig
ADDED
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
language: ruby
|
2
|
+
rvm:
|
3
|
+
- 2.6
|
4
|
+
- 2.5
|
5
|
+
before_install:
|
6
|
+
- gem install bundler
|
7
|
+
- mysql -e 'CREATE DATABASE ci_test;'
|
8
|
+
- psql -c 'CREATE DATABASE ci_test;' -U postgres
|
9
|
+
services:
|
10
|
+
- postgresql
|
11
|
+
- mysql
|
12
|
+
env:
|
13
|
+
- DATABASE_URL=sqlite3:///tmp/ci_test.sqlite3
|
14
|
+
- DATABASE_URL=mysql2://travis@127.0.0.1/ci_test
|
15
|
+
- DATABASE_URL=postgresql://127.0.0.1/ci_test
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
activesupport_cache_database (0.1.0)
|
5
|
+
activerecord (>= 5.0)
|
6
|
+
activesupport (>= 5.0)
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: https://rubygems.org/
|
10
|
+
specs:
|
11
|
+
activemodel (6.0.0)
|
12
|
+
activesupport (= 6.0.0)
|
13
|
+
activerecord (6.0.0)
|
14
|
+
activemodel (= 6.0.0)
|
15
|
+
activesupport (= 6.0.0)
|
16
|
+
activesupport (6.0.0)
|
17
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
18
|
+
i18n (>= 0.7, < 2)
|
19
|
+
minitest (~> 5.1)
|
20
|
+
tzinfo (~> 1.1)
|
21
|
+
zeitwerk (~> 2.1, >= 2.1.8)
|
22
|
+
ast (2.4.0)
|
23
|
+
concurrent-ruby (1.1.5)
|
24
|
+
diff-lcs (1.3)
|
25
|
+
i18n (1.6.0)
|
26
|
+
concurrent-ruby (~> 1.0)
|
27
|
+
jaro_winkler (1.5.3)
|
28
|
+
minitest (5.11.3)
|
29
|
+
mysql2 (0.5.2)
|
30
|
+
parallel (1.17.0)
|
31
|
+
parser (2.6.4.0)
|
32
|
+
ast (~> 2.4.0)
|
33
|
+
pg (1.1.4)
|
34
|
+
rainbow (3.0.0)
|
35
|
+
rake (12.3.3)
|
36
|
+
rspec (3.8.0)
|
37
|
+
rspec-core (~> 3.8.0)
|
38
|
+
rspec-expectations (~> 3.8.0)
|
39
|
+
rspec-mocks (~> 3.8.0)
|
40
|
+
rspec-core (3.8.2)
|
41
|
+
rspec-support (~> 3.8.0)
|
42
|
+
rspec-expectations (3.8.4)
|
43
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
44
|
+
rspec-support (~> 3.8.0)
|
45
|
+
rspec-mocks (3.8.1)
|
46
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
47
|
+
rspec-support (~> 3.8.0)
|
48
|
+
rspec-support (3.8.2)
|
49
|
+
rubocop (0.74.0)
|
50
|
+
jaro_winkler (~> 1.5.1)
|
51
|
+
parallel (~> 1.10)
|
52
|
+
parser (>= 2.6)
|
53
|
+
rainbow (>= 2.2.2, < 4.0)
|
54
|
+
ruby-progressbar (~> 1.7)
|
55
|
+
unicode-display_width (>= 1.4.0, < 1.7)
|
56
|
+
ruby-progressbar (1.10.1)
|
57
|
+
sqlite3 (1.4.1)
|
58
|
+
thread_safe (0.3.6)
|
59
|
+
tzinfo (1.2.5)
|
60
|
+
thread_safe (~> 0.1)
|
61
|
+
unicode-display_width (1.6.0)
|
62
|
+
zeitwerk (2.1.10)
|
63
|
+
|
64
|
+
PLATFORMS
|
65
|
+
ruby
|
66
|
+
|
67
|
+
DEPENDENCIES
|
68
|
+
activesupport_cache_database!
|
69
|
+
bundler
|
70
|
+
mysql2
|
71
|
+
pg
|
72
|
+
rake
|
73
|
+
rspec
|
74
|
+
rubocop
|
75
|
+
sqlite3
|
76
|
+
|
77
|
+
BUNDLED WITH
|
78
|
+
2.0.2
|
data/LICENSE
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright 2019 Black Square Media Ltd
|
2
|
+
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
you may not use this file except in compliance with the License.
|
5
|
+
You may obtain a copy of the License at
|
6
|
+
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
See the License for the specific language governing permissions and
|
13
|
+
limitations under the License.
|
data/README.md
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# ActiveSupport::Cache::DatabaseStore
|
2
|
+
|
3
|
+
[](https://travis-ci.org/bsm/activesupport-cache-database)
|
4
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
5
|
+
|
6
|
+
ActiveSupport::Cache::Store implementation backed by a database via ActiveRecord.
|
7
|
+
|
8
|
+
Tested with:
|
9
|
+
|
10
|
+
- PostgreSQL
|
11
|
+
- SQlite3
|
12
|
+
- MySQL/MariaDB
|
13
|
+
|
14
|
+
## Usage
|
15
|
+
|
16
|
+
Include the data migration:
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
# db/migrate/20190908102030_create_activesupport_cache_entries.rb
|
20
|
+
require 'active_support/cache/database_store/migration'
|
21
|
+
|
22
|
+
class CreateActivesupportCacheEntries < ActiveRecord::Migration[5.2]
|
23
|
+
def up
|
24
|
+
ActiveSupport::Cache::DatabaseStore::Migration.migrate(:up)
|
25
|
+
end
|
26
|
+
|
27
|
+
def down
|
28
|
+
ActiveSupport::Cache::DatabaseStore::Migration.migrate(:down)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
```
|
32
|
+
|
33
|
+
Open and use the new cache instance:
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
cache = ActiveSupport::Cache::DatabaseStore.new namespace: 'my-scope'
|
37
|
+
value = cache.fetch('some-key') { 'default' }
|
38
|
+
```
|
39
|
+
|
40
|
+
To use as a Rails cache store (not recommended!), simply use a new instance.
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
config.cache_store = ActiveSupport::Cache::DatabaseStore.new
|
44
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'activesupport_cache_database'
|
3
|
+
s.version = '0.1.0'
|
4
|
+
s.authors = ['Black Square Media Ltd']
|
5
|
+
s.email = ['info@blacksquaremedia.com']
|
6
|
+
s.summary = %(ActiveSupport::Cache::Store implementation backed by ActiveRecord.)
|
7
|
+
s.description = %(Use your DB as a cache store)
|
8
|
+
s.homepage = 'https://github.com/bsm/record-cache-store'
|
9
|
+
s.license = 'Apache-2.0'
|
10
|
+
|
11
|
+
s.files = `git ls-files -z`.split("\x0").reject {|f| f.match(%r{^spec/}) }
|
12
|
+
s.test_files = `git ls-files -z -- spec/*`.split("\x0")
|
13
|
+
s.require_paths = ['lib']
|
14
|
+
s.required_ruby_version = '>= 2.5'
|
15
|
+
|
16
|
+
s.add_dependency 'activerecord', '>= 5.0'
|
17
|
+
s.add_dependency 'activesupport', '>= 5.0'
|
18
|
+
|
19
|
+
s.add_development_dependency 'bundler'
|
20
|
+
s.add_development_dependency 'mysql2'
|
21
|
+
s.add_development_dependency 'pg'
|
22
|
+
s.add_development_dependency 'rake'
|
23
|
+
s.add_development_dependency 'rspec'
|
24
|
+
s.add_development_dependency 'rubocop'
|
25
|
+
s.add_development_dependency 'sqlite3'
|
26
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'active_support/cache'
|
2
|
+
require 'active_record'
|
3
|
+
|
4
|
+
module ActiveSupport
|
5
|
+
module Cache
|
6
|
+
# A cache store implementation which stores everything in the database, using ActiveRecord as the backend.
|
7
|
+
#
|
8
|
+
# DatabaseStore implements the Strategy::LocalCache strategy which implements
|
9
|
+
# an in-memory cache inside of a block.
|
10
|
+
class DatabaseStore < Store
|
11
|
+
prepend Strategy::LocalCache
|
12
|
+
|
13
|
+
autoload :Model, 'active_support/cache/database_store/model'
|
14
|
+
autoload :Migration, 'active_support/cache/database_store/migration'
|
15
|
+
|
16
|
+
# param [Hash] options options
|
17
|
+
# option options [Class] :model model class. Default: ActiveSupport::Cache::DatabaseStore::Model
|
18
|
+
def initialize(options=nil)
|
19
|
+
@model = (options || {}).delete(:model) || Model
|
20
|
+
super(options)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Preemptively iterates through all stored keys and removes the ones which have expired.
|
24
|
+
def cleanup(options=nil)
|
25
|
+
options = merged_options(options)
|
26
|
+
scope = @model.expired
|
27
|
+
if (namespace = options[:namespace])
|
28
|
+
scope = scope.namespaced(namespace)
|
29
|
+
end
|
30
|
+
scope.delete_all
|
31
|
+
end
|
32
|
+
|
33
|
+
# Clears the entire cache. Be careful with this method.
|
34
|
+
def clear(options=nil)
|
35
|
+
options = merged_options(options)
|
36
|
+
if (namespace = options[:namespace])
|
37
|
+
@model.namespaced(namespace).delete_all
|
38
|
+
else
|
39
|
+
@model.truncate!
|
40
|
+
end
|
41
|
+
true
|
42
|
+
end
|
43
|
+
|
44
|
+
# Calculates the number of entries in the cache.
|
45
|
+
def count(options=nil)
|
46
|
+
options = merged_options(options)
|
47
|
+
scope = @model.all
|
48
|
+
if (namespace = options[:namespace])
|
49
|
+
scope = scope.namespaced(namespace)
|
50
|
+
end
|
51
|
+
scope = scope.fresh unless options[:all]
|
52
|
+
scope.count
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def normalize_key(name, options=nil)
|
58
|
+
key = super.to_s
|
59
|
+
raise ArgumentError, 'Namespaced key exceeds the length limit' if key && key.bytesize > 255
|
60
|
+
|
61
|
+
key
|
62
|
+
end
|
63
|
+
|
64
|
+
def read_entry(key, _options=nil)
|
65
|
+
from_record @model.where(key: key).first
|
66
|
+
end
|
67
|
+
|
68
|
+
def write_entry(key, entry, _options=nil)
|
69
|
+
record = @model.where(key: key).first_or_initialize
|
70
|
+
expires_at = Time.zone.at(entry.expires_at) if entry.expires_at
|
71
|
+
record.update! value: Marshal.dump(entry.value), version: entry.version.presence, expires_at: expires_at
|
72
|
+
end
|
73
|
+
|
74
|
+
def delete_entry(key, _options=nil)
|
75
|
+
@model.where(key: key).destroy_all
|
76
|
+
end
|
77
|
+
|
78
|
+
def read_multi_entries(names, options)
|
79
|
+
keyed = {}
|
80
|
+
names.each do |name|
|
81
|
+
version = normalize_version(name, options)
|
82
|
+
keyed[normalize_key(name, options)] = { name: name, version: version }
|
83
|
+
end
|
84
|
+
|
85
|
+
results = {}
|
86
|
+
@model.where(key: keyed.keys).find_each do |rec|
|
87
|
+
name, version = keyed[rec.key].values_at(:name, :version)
|
88
|
+
entry = from_record(rec)
|
89
|
+
next if entry.nil?
|
90
|
+
|
91
|
+
if entry.expired?
|
92
|
+
delete_entry(rec.key, options)
|
93
|
+
elsif entry.mismatched?(version)
|
94
|
+
# Skip mismatched versions
|
95
|
+
else
|
96
|
+
results[name] = entry.value
|
97
|
+
end
|
98
|
+
end
|
99
|
+
results
|
100
|
+
end
|
101
|
+
|
102
|
+
def from_record(record)
|
103
|
+
return unless record
|
104
|
+
|
105
|
+
entry = Entry.new Marshal.load(record.value), version: record.version
|
106
|
+
entry.expires_at = record.expires_at
|
107
|
+
entry
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'active_support/cache/database_store'
|
2
|
+
|
3
|
+
module ActiveSupport
|
4
|
+
module Cache
|
5
|
+
class DatabaseStore < Store
|
6
|
+
class Migration < ::ActiveRecord::Migration[5.2]
|
7
|
+
def change
|
8
|
+
create_table :activesupport_cache_entries, primary_key: 'key', id: :binary, limit: 255 do |t|
|
9
|
+
t.binary :value, null: false
|
10
|
+
t.string :version, index: true
|
11
|
+
t.timestamp :created_at, null: false, index: true
|
12
|
+
t.timestamp :expires_at, index: true
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'active_support/cache/database_store'
|
2
|
+
|
3
|
+
module ActiveSupport
|
4
|
+
module Cache
|
5
|
+
class DatabaseStore < Store
|
6
|
+
class Model < ActiveRecord::Base
|
7
|
+
self.table_name = 'activesupport_cache_entries'
|
8
|
+
|
9
|
+
def self.truncate!
|
10
|
+
connection.truncate(table_name)
|
11
|
+
end
|
12
|
+
|
13
|
+
scope :fresh, -> { where(arel_table[:expires_at].gt(Time.zone.now)) }
|
14
|
+
scope :expired, -> { where(arel_table[:expires_at].lteq(Time.zone.now)) }
|
15
|
+
|
16
|
+
def self.namespaced(namespace)
|
17
|
+
prefix = "#{namespace}:"
|
18
|
+
clause = ::Arel::Nodes::NamedFunction.new('SUBSTR', [arel_table[:key], 1, prefix.bytesize])
|
19
|
+
where clause.eq(prefix)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'active_support/cache/database_store'
|
@@ -0,0 +1,328 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe ActiveSupport::Cache::DatabaseStore do
|
4
|
+
subject do
|
5
|
+
described_class.new expires_in: 60
|
6
|
+
end
|
7
|
+
|
8
|
+
it 'should read and write strings' do
|
9
|
+
expect(subject.write('foo', 'bar')).to be_truthy
|
10
|
+
expect(subject.read('foo')).to eq('bar')
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'should read and write hash' do
|
14
|
+
expect(subject.write('foo', a: 'b')).to be_truthy
|
15
|
+
expect(subject.read('foo')).to eq(a: 'b')
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'should read and write integer' do
|
19
|
+
expect(subject.write('foo', 1)).to be_truthy
|
20
|
+
expect(subject.read('foo')).to eq(1)
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'should read and write nil' do
|
24
|
+
expect(subject.write('foo', nil)).to be_truthy
|
25
|
+
expect(subject.read('foo')).to eq(nil)
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'should read and write false' do
|
29
|
+
expect(subject.write('foo', false)).to be_truthy
|
30
|
+
expect(subject.read('foo')).to eq(false)
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'should overwrite' do
|
34
|
+
expect(subject.write('foo', 'bar')).to be_truthy
|
35
|
+
expect(subject.write('foo', 'baz')).to be_truthy
|
36
|
+
expect(subject.read('foo')).to eq('baz')
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'should support exist?' do
|
40
|
+
subject.write('foo', 'bar')
|
41
|
+
expect(subject.exist?('foo')).to be_truthy
|
42
|
+
expect(subject.exist?('bar')).to be_falsey
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'should support nil exist?' do
|
46
|
+
subject.write('foo', nil)
|
47
|
+
expect(subject.exist?('foo')).to be_truthy
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'should support delete' do
|
51
|
+
subject.write('foo', 'bar')
|
52
|
+
expect(subject.exist?('foo')).to be_truthy
|
53
|
+
expect(subject.delete('foo')).to be_truthy
|
54
|
+
expect(subject.exist?('foo')).to be_falsey
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'should support expires_in' do
|
58
|
+
time = Time.local(2008, 4, 24)
|
59
|
+
allow(Time).to receive(:now).and_return(time)
|
60
|
+
|
61
|
+
subject.write('foo', 'bar')
|
62
|
+
expect(subject.read('foo')).to eq('bar')
|
63
|
+
|
64
|
+
allow(Time).to receive(:now).and_return(time + 30)
|
65
|
+
expect(subject.read('foo')).to eq('bar')
|
66
|
+
|
67
|
+
allow(Time).to receive(:now).and_return(time + 61)
|
68
|
+
expect(subject.read('foo')).to be_nil
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'should support long keys' do
|
72
|
+
key = 'x' * 255
|
73
|
+
expect(subject.write(key, 'bar')).to be_truthy
|
74
|
+
expect(subject.read(key)).to eq('bar')
|
75
|
+
expect(subject.fetch(key)).to eq('bar')
|
76
|
+
expect(subject.read(key[0..-2])).to be_nil
|
77
|
+
expect(subject.read_multi(key)).to eq(key => 'bar')
|
78
|
+
expect(subject.delete(key)).to be_truthy
|
79
|
+
|
80
|
+
expect { subject.write("#{key}x", 'bar') }.to raise_error(ArgumentError, /exceeds the length limit/)
|
81
|
+
expect { subject.read("#{key}x") }.to raise_error(ArgumentError, /exceeds the length limit/)
|
82
|
+
end
|
83
|
+
|
84
|
+
describe '#cleanup' do
|
85
|
+
it 'should delete expired' do
|
86
|
+
time = Time.now
|
87
|
+
subject.write('foo', 'bar', expires_in: 10)
|
88
|
+
subject.write('fud', 'biz', expires_in: 20)
|
89
|
+
|
90
|
+
allow(Time).to receive(:now).and_return(time + 9)
|
91
|
+
expect(subject.cleanup).to eq(0)
|
92
|
+
|
93
|
+
allow(Time).to receive(:now).and_return(time + 19)
|
94
|
+
expect(subject.cleanup).to eq(1)
|
95
|
+
expect(subject.read('foo')).to be_nil
|
96
|
+
expect(subject.read('fud')).to eq('biz')
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'should support namespace' do
|
100
|
+
time = Time.now
|
101
|
+
subject.write('foo', 'bar', expires_in: 10, namespace: 'x')
|
102
|
+
subject.write('foo', 'biz', expires_in: 10, namespace: 'y')
|
103
|
+
|
104
|
+
allow(Time).to receive(:now).and_return(time + 11)
|
105
|
+
expect(subject.count).to eq(0)
|
106
|
+
expect(subject.count(all: true)).to eq(2)
|
107
|
+
|
108
|
+
expect(subject.cleanup(namespace: 'x')).to eq(1)
|
109
|
+
expect(subject.count).to eq(0)
|
110
|
+
expect(subject.count(all: true)).to eq(1)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
describe '#clear' do
|
115
|
+
it 'should remove all entries' do
|
116
|
+
subject.write('foo', 'bar')
|
117
|
+
subject.write('fud', 'biz')
|
118
|
+
expect(subject.clear).to be_truthy
|
119
|
+
expect(subject.read('foo')).to be_nil
|
120
|
+
expect(subject.read('fud')).to be_nil
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'should support namespace' do
|
124
|
+
subject.write('foo', 'bar', namespace: 'x')
|
125
|
+
subject.write('foo', 'biz', namespace: 'y')
|
126
|
+
expect(subject.count).to eq(2)
|
127
|
+
|
128
|
+
expect(subject.clear(namespace: 'x')).to be_truthy
|
129
|
+
expect(subject.count).to eq(1)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
describe '#fetch' do
|
134
|
+
it 'should support cache hit' do
|
135
|
+
subject.write('foo', 'bar')
|
136
|
+
expect(subject).not_to receive(:write)
|
137
|
+
|
138
|
+
expect(subject.fetch('foo') { 'baz' }).to eq('bar')
|
139
|
+
end
|
140
|
+
|
141
|
+
it 'should support cache miss' do
|
142
|
+
expect(subject).to receive(:write).with('foo', 'baz', instance_of(Hash))
|
143
|
+
expect(subject.fetch('foo') { 'baz' }).to eq('baz')
|
144
|
+
end
|
145
|
+
|
146
|
+
it 'should pass key to block on cache miss' do
|
147
|
+
cache_miss = false
|
148
|
+
expect(subject.fetch('foo') {|key| cache_miss = true; key.length }).to eq(3)
|
149
|
+
expect(cache_miss).to be_truthy
|
150
|
+
|
151
|
+
cache_miss = false
|
152
|
+
expect(subject.fetch('foo') {|key| cache_miss = true; key.length }).to eq(3)
|
153
|
+
expect(cache_miss).to be_falsey
|
154
|
+
end
|
155
|
+
|
156
|
+
it 'should support forced cache miss' do
|
157
|
+
subject.write('foo', 'bar')
|
158
|
+
expect(subject).not_to receive(:read)
|
159
|
+
|
160
|
+
expect(subject.fetch('foo', force: true) { 'baz' }).to eq('baz')
|
161
|
+
end
|
162
|
+
|
163
|
+
it 'should support nil values' do
|
164
|
+
subject.write('foo', nil)
|
165
|
+
expect(subject).not_to receive(:write)
|
166
|
+
|
167
|
+
expect(subject.fetch('foo') { 'baz' }).to be_nil
|
168
|
+
end
|
169
|
+
|
170
|
+
it 'should support skip_nil option' do
|
171
|
+
expect(subject).not_to receive(:write)
|
172
|
+
expect(subject.fetch('foo', skip_nil: true) { nil }).to be_nil
|
173
|
+
expect(subject.exist?('foo')).to be_falsey
|
174
|
+
end
|
175
|
+
|
176
|
+
it 'should support forced cache miss with block' do
|
177
|
+
subject.write('foo', 'bar')
|
178
|
+
expect(subject.fetch('foo', force: true) { 'baz' }).to eq('baz')
|
179
|
+
end
|
180
|
+
|
181
|
+
it 'should support forced cache miss without block' do
|
182
|
+
subject.write('foo', 'bar')
|
183
|
+
expect { subject.fetch('foo', force: true) }.to raise_error(ArgumentError)
|
184
|
+
expect(subject.read('foo')).to eq('bar')
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
describe '#read_multi' do
|
189
|
+
it 'should support read_multi' do
|
190
|
+
subject.write('foo', 'bar')
|
191
|
+
subject.write('fu', 'baz')
|
192
|
+
subject.write('fud', 'biz')
|
193
|
+
expect(subject.read_multi('foo', 'fu')).to eq('foo' => 'bar', 'fu' => 'baz')
|
194
|
+
end
|
195
|
+
|
196
|
+
it 'should support expires' do
|
197
|
+
time = Time.now
|
198
|
+
subject.write('foo', 'bar', expires_in: 10)
|
199
|
+
subject.write('fu', 'baz')
|
200
|
+
subject.write('fud', 'biz')
|
201
|
+
|
202
|
+
allow(Time).to receive(:now).and_return(time + 11)
|
203
|
+
expect(subject.read_multi('foo', 'fu')).to eq('fu' => 'baz')
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
describe '#fetch_multi' do
|
208
|
+
it 'should support fetch_multi' do
|
209
|
+
subject.write('foo', 'bar')
|
210
|
+
subject.write('fud', 'biz')
|
211
|
+
values = subject.fetch_multi('foo', 'fu', 'fud') {|v| v * 2 }
|
212
|
+
|
213
|
+
expect(values).to eq('foo' => 'bar', 'fu' => 'fufu', 'fud' => 'biz')
|
214
|
+
expect(subject.read('fu')).to eq('fufu')
|
215
|
+
end
|
216
|
+
|
217
|
+
it 'should support without expires_in' do
|
218
|
+
subject.write('foo', 'bar')
|
219
|
+
subject.write('fud', 'biz')
|
220
|
+
values = subject.fetch_multi('foo', 'fu', 'fud', expires_in: nil) {|v| v * 2 }
|
221
|
+
|
222
|
+
expect(values).to eq('foo' => 'bar', 'fu' => 'fufu', 'fud' => 'biz')
|
223
|
+
expect(subject.read('fu')).to eq('fufu')
|
224
|
+
end
|
225
|
+
|
226
|
+
it 'should support with objects' do
|
227
|
+
cache_struct = Struct.new(:cache_key, :title)
|
228
|
+
foo = cache_struct.new('foo', 'FOO!')
|
229
|
+
bar = cache_struct.new('bar')
|
230
|
+
|
231
|
+
subject.write('bar', 'BAM!')
|
232
|
+
values = subject.fetch_multi(foo, bar, &:title)
|
233
|
+
expect(values).to eq(foo => 'FOO!', bar => 'BAM!')
|
234
|
+
end
|
235
|
+
|
236
|
+
it 'should support ordered names' do
|
237
|
+
subject.write('bam', 'BAM')
|
238
|
+
values = subject.fetch_multi('foo', 'bar', 'bam', &:upcase)
|
239
|
+
expect(values.keys).to eq(%w[foo bar bam])
|
240
|
+
end
|
241
|
+
|
242
|
+
it 'should raise without block' do
|
243
|
+
expect { subject.fetch_multi('foo') }.to raise_error(ArgumentError)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
describe 'cache key' do
|
248
|
+
it 'should support cache keys' do
|
249
|
+
obj = Object.new
|
250
|
+
def obj.cache_key
|
251
|
+
:foo
|
252
|
+
end
|
253
|
+
subject.write(obj, 'bar')
|
254
|
+
expect(subject.read('foo')).to eq('bar')
|
255
|
+
end
|
256
|
+
|
257
|
+
it 'should support to_param keys' do
|
258
|
+
obj = Object.new
|
259
|
+
def obj.to_param
|
260
|
+
:foo
|
261
|
+
end
|
262
|
+
subject.write(obj, 'bar')
|
263
|
+
expect(subject.read('foo')).to eq('bar')
|
264
|
+
end
|
265
|
+
|
266
|
+
it 'should support unversioned keys' do
|
267
|
+
obj = Object.new
|
268
|
+
def obj.cache_key
|
269
|
+
:foo
|
270
|
+
end
|
271
|
+
|
272
|
+
def obj.cache_key_with_version
|
273
|
+
'foo-v1'
|
274
|
+
end
|
275
|
+
subject.write(obj, 'bar')
|
276
|
+
expect(subject.read('foo')).to eq('bar')
|
277
|
+
end
|
278
|
+
|
279
|
+
it 'should support array keys' do
|
280
|
+
subject.write([:fu, 'foo'], 'bar')
|
281
|
+
expect(subject.read('fu/foo')).to eq('bar')
|
282
|
+
end
|
283
|
+
|
284
|
+
it 'should support hash keys' do
|
285
|
+
subject.write({ foo: 1, fu: 2 }, 'bar')
|
286
|
+
expect(subject.read('foo=1/fu=2')).to eq('bar')
|
287
|
+
end
|
288
|
+
|
289
|
+
it 'should be case sensitive' do
|
290
|
+
subject.write('foo', 'bar')
|
291
|
+
expect(subject.read('FOO')).to be_nil
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
describe 'with version' do
|
296
|
+
it 'should support fetch/read' do
|
297
|
+
subject.fetch('foo', version: 1) { 'bar' }
|
298
|
+
expect(subject.read('foo', version: 1)).to eq('bar')
|
299
|
+
expect(subject.read('foo', version: 2)).to be_nil
|
300
|
+
end
|
301
|
+
|
302
|
+
it 'should support write/read' do
|
303
|
+
subject.write('foo', 'bar', version: 1)
|
304
|
+
expect(subject.read('foo', version: 1)).to eq('bar')
|
305
|
+
expect(subject.read('foo', version: 2)).to be_nil
|
306
|
+
end
|
307
|
+
|
308
|
+
it 'should support exists' do
|
309
|
+
subject.write('foo', 'bar', version: 1)
|
310
|
+
expect(subject.exist?('foo', version: 1)).to be_truthy
|
311
|
+
expect(subject.exist?('foo', version: 2)).to be_falsey
|
312
|
+
end
|
313
|
+
|
314
|
+
it 'should cache/version keys' do
|
315
|
+
m1v1 = ModelWithKeyAndVersion.new('model/1', 1)
|
316
|
+
m1v2 = ModelWithKeyAndVersion.new('model/1', 2)
|
317
|
+
|
318
|
+
subject.write(m1v1, 'bar')
|
319
|
+
expect(subject.read(m1v1)).to eq('bar')
|
320
|
+
expect(subject.read(m1v2)).to be_nil
|
321
|
+
end
|
322
|
+
|
323
|
+
it 'should normalise' do
|
324
|
+
subject.write('foo', 'bar', version: 1)
|
325
|
+
expect(subject.read('foo', version: '1')).to eq('bar')
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
ENV['RACK_ENV'] ||= 'test'
|
2
|
+
|
3
|
+
require 'rspec'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'activesupport_cache_database'
|
6
|
+
|
7
|
+
Time.zone_default = Time.find_zone!('UTC')
|
8
|
+
|
9
|
+
database_url = ENV.fetch('DATABASE_URL') do
|
10
|
+
path = File.expand_path('./test.sqlite3', __dir__)
|
11
|
+
FileUtils.rm_f(path)
|
12
|
+
"sqlite3://#{path}"
|
13
|
+
end
|
14
|
+
ActiveRecord::Base.configurations = { 'test' => { 'url' => database_url, 'pool' => 20 } }
|
15
|
+
|
16
|
+
ActiveRecord::Base.establish_connection :test
|
17
|
+
ActiveRecord::Base.connection.instance_eval do
|
18
|
+
drop_table 'activesupport_cache_entries', if_exists: true
|
19
|
+
end
|
20
|
+
ActiveRecord::Migration.suppress_messages do
|
21
|
+
ActiveSupport::Cache::DatabaseStore::Migration.migrate(:up)
|
22
|
+
end
|
23
|
+
|
24
|
+
RSpec.configure do |c|
|
25
|
+
c.after :each do
|
26
|
+
ActiveSupport::Cache::DatabaseStore::Model.truncate!
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
ModelWithKeyAndVersion = Struct.new(:cache_key, :cache_version)
|
metadata
ADDED
@@ -0,0 +1,187 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activesupport_cache_database
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Black Square Media Ltd
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-09-11 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
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: activesupport
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '5.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '5.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
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
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: mysql2
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: pg
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rake
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rspec
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rubocop
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: sqlite3
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
description: Use your DB as a cache store
|
140
|
+
email:
|
141
|
+
- info@blacksquaremedia.com
|
142
|
+
executables: []
|
143
|
+
extensions: []
|
144
|
+
extra_rdoc_files: []
|
145
|
+
files:
|
146
|
+
- ".editorconfig"
|
147
|
+
- ".gitignore"
|
148
|
+
- ".rubocop.yml"
|
149
|
+
- ".travis.yml"
|
150
|
+
- Gemfile
|
151
|
+
- Gemfile.lock
|
152
|
+
- LICENSE
|
153
|
+
- README.md
|
154
|
+
- Rakefile
|
155
|
+
- activesupport_cache_database.gemspec
|
156
|
+
- lib/active_support/cache/database_store.rb
|
157
|
+
- lib/active_support/cache/database_store/migration.rb
|
158
|
+
- lib/active_support/cache/database_store/model.rb
|
159
|
+
- lib/activesupport_cache_database.rb
|
160
|
+
- spec/active_support/cache/database_store_spec.rb
|
161
|
+
- spec/spec_helper.rb
|
162
|
+
homepage: https://github.com/bsm/record-cache-store
|
163
|
+
licenses:
|
164
|
+
- Apache-2.0
|
165
|
+
metadata: {}
|
166
|
+
post_install_message:
|
167
|
+
rdoc_options: []
|
168
|
+
require_paths:
|
169
|
+
- lib
|
170
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
171
|
+
requirements:
|
172
|
+
- - ">="
|
173
|
+
- !ruby/object:Gem::Version
|
174
|
+
version: '2.5'
|
175
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
176
|
+
requirements:
|
177
|
+
- - ">="
|
178
|
+
- !ruby/object:Gem::Version
|
179
|
+
version: '0'
|
180
|
+
requirements: []
|
181
|
+
rubygems_version: 3.0.3
|
182
|
+
signing_key:
|
183
|
+
specification_version: 4
|
184
|
+
summary: ActiveSupport::Cache::Store implementation backed by ActiveRecord.
|
185
|
+
test_files:
|
186
|
+
- spec/active_support/cache/database_store_spec.rb
|
187
|
+
- spec/spec_helper.rb
|