lmc-store 0.0.1

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: 04e07e855624916fd37f2f210a6f27608e44c54bf7c2d612e7bbe6b0094337d8
4
+ data.tar.gz: 0ca77b61e18be6592be9320a5268cad3a9126ce9efb4aa8221f05f2f52382607
5
+ SHA512:
6
+ metadata.gz: b3c860a67b88eea73664d403a7c2f8d8b141cc0c0ebe0311eb5e6263d33724b8ac46bb615ab3b47a73c558fcf39a08b19542d98ce0d8ae2aaf63b2b845a3a180
7
+ data.tar.gz: fe5458a0fbd3917573c97304cc0005031b9021ac65a820df57359ee722031b4d488a97e4e08b4a06d0b97341e27f399b3e29a49510d537bf39004c288b33ba64
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+ gemspec
3
+
4
+ gem "rake"
5
+ gem "rspec"
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,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -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
@@ -0,0 +1,3 @@
1
+ require 'active_support'
2
+
3
+ ActiveSupport.cache_format_version = 7.0
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