lmc-store 0.0.1

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 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