lmc-store 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +22 -0
- data/README.md +26 -0
- data/Rakefile +6 -0
- data/lib/active_support/cache/lmc_store.rb +157 -0
- data/lib/lmc-store.rb +1 -0
- data/lmc-store.gemspec +15 -0
- data/spec/active_support/cache/lmc_store_spec.rb +139 -0
- data/spec/spec_helper.rb +3 -0
- metadata +81 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 04e07e855624916fd37f2f210a6f27608e44c54bf7c2d612e7bbe6b0094337d8
|
4
|
+
data.tar.gz: 0ca77b61e18be6592be9320a5268cad3a9126ce9efb4aa8221f05f2f52382607
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b3c860a67b88eea73664d403a7c2f8d8b141cc0c0ebe0311eb5e6263d33724b8ac46bb615ab3b47a73c558fcf39a08b19542d98ce0d8ae2aaf63b2b845a3a180
|
7
|
+
data.tar.gz: fe5458a0fbd3917573c97304cc0005031b9021ac65a820df57359ee722031b4d488a97e4e08b4a06d0b97341e27f399b3e29a49510d537bf39004c288b33ba64
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2019 Robin Roestenburg
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
LMC (LocalMemCache) store
|
2
|
+
------------------
|
3
|
+
|
4
|
+
[lmc_store](http://github.com/anteo/lmc_store) is an ActiveSupport::Cache::Store implementation for
|
5
|
+
[LocalMemCache](http://localmemcache.rubyforge.org/) in Rails.
|
6
|
+
|
7
|
+
## Disclaimer
|
8
|
+
|
9
|
+
This gem was created because there are no other gems for modern Rails >= 6.
|
10
|
+
While it is in alpha stage, use it at your own risk.
|
11
|
+
|
12
|
+
## Installation
|
13
|
+
|
14
|
+
Add this line to your application's Gemfile:
|
15
|
+
|
16
|
+
gem 'lmc_store'
|
17
|
+
|
18
|
+
And then execute:
|
19
|
+
|
20
|
+
$ bundle
|
21
|
+
|
22
|
+
If using Rails, in `config/application.rb`:
|
23
|
+
|
24
|
+
config.cache_store = :lmc_store
|
25
|
+
|
26
|
+
Done!
|
data/Rakefile
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'localmemcache'
|
3
|
+
require 'concurrent/utility/monotonic_time'
|
4
|
+
require 'monitor'
|
5
|
+
require 'tmpdir'
|
6
|
+
|
7
|
+
class ActiveSupport::Cache::LmcStore < ActiveSupport::Cache::Store
|
8
|
+
DEFAULT_DIR = Dir.tmpdir
|
9
|
+
DEFAULT_NAME = 'localmemcache'
|
10
|
+
DEFAULT_SIZE = 16.megabytes # also the minimum size, as localmemcache is unreliable below this value
|
11
|
+
|
12
|
+
def initialize(options = {})
|
13
|
+
super options
|
14
|
+
|
15
|
+
directory = options.fetch(:directory, DEFAULT_DIR)
|
16
|
+
name = options.fetch(:name, DEFAULT_NAME)
|
17
|
+
size = options.fetch(:size, DEFAULT_SIZE)
|
18
|
+
|
19
|
+
data_store_options = {
|
20
|
+
filename: Pathname.new(directory).join(name).to_s,
|
21
|
+
size_mb: [DEFAULT_SIZE, size].max / 1.megabyte
|
22
|
+
}
|
23
|
+
|
24
|
+
@data = LocalMemCache.new(data_store_options)
|
25
|
+
@max_prune_time = options[:max_prune_time] || 2
|
26
|
+
@monitor = Monitor.new
|
27
|
+
@pruning = false
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.supports_cache_versioning?
|
31
|
+
true
|
32
|
+
end
|
33
|
+
|
34
|
+
def clear(options = nil)
|
35
|
+
synchronize do
|
36
|
+
@data.clear
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def cleanup(options = nil)
|
41
|
+
options = merged_options(options)
|
42
|
+
instrument(:cleanup, size: used_bytes) do
|
43
|
+
keys = synchronize { @data.keys }
|
44
|
+
keys.each do |key|
|
45
|
+
payload = @data[key]
|
46
|
+
entry = deserialize_entry(payload) if payload
|
47
|
+
delete_entry(key, **options) if entry&.expired?
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def prune(target_size, max_time = nil)
|
53
|
+
return if pruning?
|
54
|
+
@pruning = true
|
55
|
+
begin
|
56
|
+
start_time = Concurrent.monotonic_time
|
57
|
+
cleanup
|
58
|
+
instrument(:prune, target_size, from: used_bytes) do
|
59
|
+
loop do
|
60
|
+
key, _ = @data.random_pair
|
61
|
+
delete_entry(key, **options)
|
62
|
+
return if used_bytes <= target_size || (max_time && Concurrent.monotonic_time - start_time > max_time)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
ensure
|
66
|
+
@pruning = false
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def pruning?
|
71
|
+
@pruning
|
72
|
+
end
|
73
|
+
|
74
|
+
def increment(name, amount = 1, options = nil)
|
75
|
+
modify_value(name, amount, options)
|
76
|
+
end
|
77
|
+
|
78
|
+
def decrement(name, amount = 1, options = nil)
|
79
|
+
modify_value(name, -amount, options)
|
80
|
+
end
|
81
|
+
|
82
|
+
def delete_matched(matcher, options = nil)
|
83
|
+
options = merged_options(options)
|
84
|
+
instrument(:delete_matched, matcher.inspect) do
|
85
|
+
matcher = key_matcher(matcher, options)
|
86
|
+
keys = synchronize { @data.keys }
|
87
|
+
keys.each do |key|
|
88
|
+
delete_entry(key, **options) if key.match(matcher)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def inspect # :nodoc:
|
94
|
+
"#<#{self.class.name} entries=#{@data.size}, free=#{free_bytes}/#{total_bytes}, options=#{@options.inspect}>"
|
95
|
+
end
|
96
|
+
|
97
|
+
def synchronize(&block)
|
98
|
+
@monitor.synchronize(&block)
|
99
|
+
end
|
100
|
+
|
101
|
+
def total_bytes
|
102
|
+
@data.shm_status[:total_bytes]
|
103
|
+
end
|
104
|
+
|
105
|
+
def free_bytes
|
106
|
+
@data.shm_status[:free_bytes]
|
107
|
+
end
|
108
|
+
|
109
|
+
def used_bytes
|
110
|
+
total_bytes - free_bytes
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
attr_reader :data
|
116
|
+
|
117
|
+
def cached_size(key, payload)
|
118
|
+
(key.to_s.bytesize + payload.bytesize) * 2
|
119
|
+
end
|
120
|
+
|
121
|
+
def read_entry(key, **options)
|
122
|
+
entry = nil
|
123
|
+
synchronize do
|
124
|
+
payload = @data[key]
|
125
|
+
entry = deserialize_entry(payload) if payload
|
126
|
+
end
|
127
|
+
entry
|
128
|
+
end
|
129
|
+
|
130
|
+
def write_entry(key, entry, **options)
|
131
|
+
payload = serialize_entry(entry)
|
132
|
+
synchronize do
|
133
|
+
cached_size = cached_size(key, payload)
|
134
|
+
prune(total_bytes * 0.75, @max_prune_time) if free_bytes < cached_size
|
135
|
+
@data[key] = payload
|
136
|
+
true
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def delete_entry(key, **options)
|
141
|
+
synchronize do
|
142
|
+
payload = @data.delete(key)
|
143
|
+
!!payload
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def modify_value(name, amount, options)
|
148
|
+
options = merged_options(options)
|
149
|
+
synchronize do
|
150
|
+
if (num = read(name, options))
|
151
|
+
num = num.to_i + amount
|
152
|
+
write(name, num, options)
|
153
|
+
num
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
data/lib/lmc-store.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'active_support/cache/lmc_store'
|
data/lmc-store.gemspec
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "lmc-store"
|
3
|
+
s.version = "0.0.1"
|
4
|
+
s.summary = "Rails cache store implementation for LocalMemCache (a modern one)"
|
5
|
+
s.authors = ["Anton Argirov"]
|
6
|
+
s.email = "anton.argirov@gmail.com"
|
7
|
+
s.files = Dir.glob("{lib,spec}/**/*") + %w(Gemfile Rakefile LICENSE.txt lmc-store.gemspec README.md)
|
8
|
+
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
9
|
+
s.require_paths = ['lib']
|
10
|
+
s.homepage = "https://github.com/anteo/lmc-store"
|
11
|
+
s.license = "MIT"
|
12
|
+
|
13
|
+
s.add_dependency 'activesupport', ['>=0']
|
14
|
+
s.add_dependency 'localmemcache', ['~>0.4.0']
|
15
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'lmc-store'
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
describe ActiveSupport::Cache::LmcStore do
|
6
|
+
describe 'caching' do
|
7
|
+
subject { described_class.new }
|
8
|
+
before { subject.clear }
|
9
|
+
|
10
|
+
describe 'read/write/delete' do
|
11
|
+
context 'when cache is empty' do
|
12
|
+
it '#read returns nil' do
|
13
|
+
expect(subject.read('foo')).to be_nil
|
14
|
+
end
|
15
|
+
|
16
|
+
it '#write returns true' do
|
17
|
+
expect(subject.write('foo', '1337')).to be true
|
18
|
+
end
|
19
|
+
|
20
|
+
it '#delete returns false' do
|
21
|
+
expect(subject.delete('foo')).to be false
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'when cache is not empty' do
|
26
|
+
before do
|
27
|
+
subject.write('foo', '1337')
|
28
|
+
end
|
29
|
+
|
30
|
+
it '#read returns cached value' do
|
31
|
+
expect(subject.read('foo')).to eq('1337')
|
32
|
+
end
|
33
|
+
|
34
|
+
it '#write returns true' do
|
35
|
+
expect(subject.write('foo', '1338')).to be true
|
36
|
+
end
|
37
|
+
|
38
|
+
it '#delete returns true' do
|
39
|
+
expect(subject.delete('foo')).to be true
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'caches structured values' do
|
44
|
+
data = { foo: 12.34, bar: 56, qux: nil }
|
45
|
+
subject.write('foo', data)
|
46
|
+
expect(subject.read('foo')).to eq(data)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe '#fetch' do
|
51
|
+
it 'persists values' do
|
52
|
+
subject.fetch('foo') { '1337' }
|
53
|
+
result = subject.fetch('foo') { '1338' }
|
54
|
+
expect(result).to eq '1337'
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'is lazy' do
|
58
|
+
generator = double('value')
|
59
|
+
allow(generator).to receive(:value).once
|
60
|
+
|
61
|
+
2.times do
|
62
|
+
subject.fetch('foo') { generator.value }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe 'eviction' do
|
69
|
+
def blob(size_mb)
|
70
|
+
SecureRandom.random_bytes(size_mb.megabytes)
|
71
|
+
end
|
72
|
+
|
73
|
+
subject { described_class.new(size: 16.megabytes) }
|
74
|
+
before { subject.clear }
|
75
|
+
|
76
|
+
it 'evicts items' do
|
77
|
+
expect(subject).to receive(:prune).at_least(1).and_call_original
|
78
|
+
|
79
|
+
16.times do |index|
|
80
|
+
subject.write(index.to_s, blob(1))
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
describe 'persistence' do
|
86
|
+
subject { described_class.new }
|
87
|
+
|
88
|
+
before do
|
89
|
+
fork do
|
90
|
+
subject.clear
|
91
|
+
subject.write 'foo', 'bar'
|
92
|
+
exit 0
|
93
|
+
end
|
94
|
+
Process.wait
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'can read on-disk data' do
|
98
|
+
expect(subject.read('foo')).to eq 'bar'
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
describe 'concurrency' do
|
103
|
+
def cache_factory
|
104
|
+
described_class.new
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'in the same thread' do
|
108
|
+
cache1 = cache_factory
|
109
|
+
cache2 = cache_factory
|
110
|
+
|
111
|
+
cache1.write('foo', 'bar1')
|
112
|
+
cache2.write('foo', 'bar2')
|
113
|
+
|
114
|
+
expect(cache1.read('foo')).to eq 'bar2'
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'across multiple processes' do
|
118
|
+
cache_factory.clear
|
119
|
+
|
120
|
+
(0..4).each do |process_index|
|
121
|
+
fork do
|
122
|
+
cache = cache_factory
|
123
|
+
(0..99).each do |index|
|
124
|
+
cache.write((index * 5 + process_index).to_s, "cache#{process_index}")
|
125
|
+
end
|
126
|
+
exit 0
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
Process.wait
|
131
|
+
|
132
|
+
cache = cache_factory
|
133
|
+
(0..499).each do |index|
|
134
|
+
expect(cache.read(index.to_s)).to match /cache\d/
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: lmc-store
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Anton Argirov
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-04-05 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: localmemcache
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.4.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.4.0
|
41
|
+
description:
|
42
|
+
email: anton.argirov@gmail.com
|
43
|
+
executables: []
|
44
|
+
extensions: []
|
45
|
+
extra_rdoc_files: []
|
46
|
+
files:
|
47
|
+
- Gemfile
|
48
|
+
- LICENSE.txt
|
49
|
+
- README.md
|
50
|
+
- Rakefile
|
51
|
+
- lib/active_support/cache/lmc_store.rb
|
52
|
+
- lib/lmc-store.rb
|
53
|
+
- lmc-store.gemspec
|
54
|
+
- spec/active_support/cache/lmc_store_spec.rb
|
55
|
+
- spec/spec_helper.rb
|
56
|
+
homepage: https://github.com/anteo/lmc-store
|
57
|
+
licenses:
|
58
|
+
- MIT
|
59
|
+
metadata: {}
|
60
|
+
post_install_message:
|
61
|
+
rdoc_options: []
|
62
|
+
require_paths:
|
63
|
+
- lib
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '0'
|
74
|
+
requirements: []
|
75
|
+
rubygems_version: 3.1.6
|
76
|
+
signing_key:
|
77
|
+
specification_version: 4
|
78
|
+
summary: Rails cache store implementation for LocalMemCache (a modern one)
|
79
|
+
test_files:
|
80
|
+
- spec/active_support/cache/lmc_store_spec.rb
|
81
|
+
- spec/spec_helper.rb
|