sin_lru_redux 2.0.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.
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler'
4
+ require 'benchmark'
5
+ require 'fast_cache'
6
+
7
+ Bundler.require
8
+
9
+ # FastCache
10
+ fast_cache = FastCache::Cache.new(1_000, 5 * 60)
11
+
12
+ # LruRedux
13
+ redux_ttl = LruRedux::TTL::Cache.new(1_000, 5 * 60)
14
+ redux_ttl_thread_safe = LruRedux::TTL::ThreadSafeCache.new(1_000, 5 * 60)
15
+ redux_ttl_disabled = LruRedux::TTL::Cache.new(1_000, :none)
16
+
17
+ puts '** TTL Benchmarks **'
18
+
19
+ Benchmark.bmbm do |bm|
20
+ bm.report 'FastCache' do
21
+ 1_000_000.times { fast_cache.fetch(rand(2_000)) { :value } } # rubocop:disable Style/RedundantFetchBlock
22
+ end
23
+
24
+ bm.report 'LruRedux::TTL::Cache' do
25
+ 1_000_000.times { redux_ttl.getset(rand(2_000)) { :value } }
26
+ end
27
+
28
+ bm.report 'LruRedux::TTL::ThreadSafeCache' do
29
+ 1_000_000.times { redux_ttl_thread_safe.getset(rand(2_000)) { :value } }
30
+ end
31
+
32
+ bm.report 'LruRedux::TTL::Cache (TTL disabled)' do
33
+ 1_000_000.times { redux_ttl_disabled.getset(rand(2_000)) { :value } }
34
+ end
35
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LruRedux::Cache
4
+ def initialize(*args)
5
+ max_size, ignore_nil, _ = args
6
+
7
+ ignore_nil ||= false
8
+
9
+ raise ArgumentError.new(:max_size) unless valid_max_size?(max_size)
10
+ raise ArgumentError.new(:ignore_nil) unless valid_ignore_nil?(ignore_nil)
11
+
12
+ @max_size = max_size
13
+ @ignore_nil = ignore_nil
14
+ @data = {}
15
+ end
16
+
17
+ def max_size=(max_size)
18
+ max_size ||= @max_size
19
+
20
+ raise ArgumentError.new(:max_size) unless valid_max_size?(max_size)
21
+
22
+ @max_size = max_size
23
+
24
+ @data.shift while @data.size > @max_size
25
+ end
26
+
27
+ def ttl=(_)
28
+ nil
29
+ end
30
+
31
+ def ignore_nil=(ignore_nil)
32
+ ignore_nil ||= @ignore_nil
33
+ raise ArgumentError.new(:ignore_nil) unless valid_ignore_nil?(ignore_nil)
34
+
35
+ @ignore_nil = ignore_nil
36
+ end
37
+
38
+ def getset(key)
39
+ found = true
40
+ value = @data.delete(key) { found = false }
41
+ if found
42
+ @data[key] = value
43
+ else
44
+ result = @data[key] = yield
45
+ @data.shift if @data.length > @max_size
46
+ result
47
+ end
48
+ end
49
+
50
+ def fetch(key)
51
+ found = true
52
+ value = @data.delete(key) { found = false }
53
+ if found
54
+ @data[key] = value
55
+ else
56
+ yield if block_given? # rubocop:disable Style/IfInsideElse
57
+ end
58
+ end
59
+
60
+ def [](key)
61
+ found = true
62
+ value = @data.delete(key) { found = false }
63
+ @data[key] = value if found
64
+ end
65
+
66
+ def []=(key, val)
67
+ @data.delete(key)
68
+ @data[key] = val
69
+ @data.shift if @data.length > @max_size
70
+ val # rubocop:disable Lint/Void
71
+ end
72
+
73
+ def each(&block)
74
+ array = @data.to_a
75
+ array.reverse!.each(&block)
76
+ end
77
+
78
+ # used further up the chain, non thread safe each
79
+ alias_method :each_unsafe, :each
80
+
81
+ def to_a
82
+ array = @data.to_a
83
+ array.reverse!
84
+ end
85
+
86
+ def values
87
+ vals = @data.values
88
+ vals.reverse!
89
+ end
90
+
91
+ def delete(key)
92
+ @data.delete(key)
93
+ end
94
+
95
+ alias_method :evict, :delete
96
+
97
+ def key?(key)
98
+ @data.key?(key)
99
+ end
100
+
101
+ alias_method :has_key?, :key?
102
+
103
+ def clear
104
+ @data.clear
105
+ end
106
+
107
+ def count
108
+ @data.size
109
+ end
110
+
111
+ protected
112
+
113
+ def valid_max_size?(max_size)
114
+ return true if max_size.is_a?(Integer) && max_size >= 1
115
+
116
+ false
117
+ end
118
+
119
+ def valid_ignore_nil?(ignore_nil)
120
+ return true if [true, false].include?(ignore_nil)
121
+
122
+ false
123
+ end
124
+
125
+ # for cache validation only, ensures all is sound
126
+ def valid?
127
+ true
128
+ end
129
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LruRedux::ThreadSafeCache < LruRedux::Cache
4
+ include LruRedux::Util::SafeSync
5
+ end
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LruRedux
4
+ module TTL
5
+ class Cache
6
+ attr_reader :max_size, :ttl, :ignore_nil
7
+
8
+ def initialize(*args)
9
+ max_size, ttl, ignore_nil = args
10
+
11
+ ttl ||= :none
12
+ ignore_nil ||= false
13
+
14
+ raise ArgumentError.new(:max_size) unless valid_max_size?(max_size)
15
+ raise ArgumentError.new(:ttl) unless valid_ttl?(ttl)
16
+ raise ArgumentError.new(:ignore_nil) unless valid_ignore_nil?(ignore_nil)
17
+
18
+ @max_size = max_size
19
+ @ttl = ttl
20
+ @ignore_nil = ignore_nil
21
+ @data_lru = {}
22
+ @data_ttl = {}
23
+ end
24
+
25
+ def max_size=(max_size)
26
+ max_size ||= @max_size
27
+
28
+ raise ArgumentError.new(:max_size) unless valid_max_size?(max_size)
29
+
30
+ @max_size = max_size
31
+
32
+ resize
33
+ end
34
+
35
+ def ttl=(ttl)
36
+ ttl ||= @ttl
37
+ raise ArgumentError.new(:ttl) unless valid_ttl?(ttl)
38
+
39
+ @ttl = ttl
40
+
41
+ ttl_evict
42
+ end
43
+
44
+ def ignore_nil=(ignore_nil)
45
+ ignore_nil ||= @ignore_nil
46
+ raise ArgumentError.new(:ignore_nil) unless valid_ignore_nil?(ignore_nil)
47
+
48
+ @ignore_nil = ignore_nil
49
+ end
50
+
51
+ def getset(key)
52
+ ttl_evict
53
+
54
+ found = true
55
+ value = @data_lru.delete(key) { found = false }
56
+ if found
57
+ @data_lru[key] = value
58
+ else
59
+ result = yield
60
+
61
+ if !result.nil? || !@ignore_nil
62
+ @data_lru[key] = result
63
+ @data_ttl[key] = Time.now.to_f
64
+
65
+ if @data_lru.size > @max_size
66
+ key, _ = @data_lru.first
67
+
68
+ @data_ttl.delete(key)
69
+ @data_lru.delete(key)
70
+ end
71
+ end
72
+
73
+ result
74
+ end
75
+ end
76
+
77
+ def fetch(key)
78
+ ttl_evict
79
+
80
+ found = true
81
+ value = @data_lru.delete(key) { found = false }
82
+ if found
83
+ @data_lru[key] = value
84
+ else
85
+ yield if block_given? # rubocop:disable Style/IfInsideElse
86
+ end
87
+ end
88
+
89
+ def [](key)
90
+ ttl_evict
91
+
92
+ found = true
93
+ value = @data_lru.delete(key) { found = false }
94
+ @data_lru[key] = value if found
95
+ end
96
+
97
+ def []=(key, val)
98
+ ttl_evict
99
+
100
+ @data_lru.delete(key)
101
+ @data_ttl.delete(key)
102
+
103
+ @data_lru[key] = val
104
+ @data_ttl[key] = Time.now.to_f
105
+
106
+ if @data_lru.size > @max_size
107
+ key, _ = @data_lru.first
108
+
109
+ @data_ttl.delete(key)
110
+ @data_lru.delete(key)
111
+ end
112
+
113
+ val # rubocop:disable Lint/Void
114
+ end
115
+
116
+ def each(&block)
117
+ ttl_evict
118
+
119
+ array = @data_lru.to_a
120
+ array.reverse!.each(&block)
121
+ end
122
+
123
+ # used further up the chain, non thread safe each
124
+ alias_method :each_unsafe, :each
125
+
126
+ def to_a
127
+ ttl_evict
128
+
129
+ array = @data_lru.to_a
130
+ array.reverse!
131
+ end
132
+
133
+ def values
134
+ ttl_evict
135
+
136
+ vals = @data_lru.values
137
+ vals.reverse!
138
+ end
139
+
140
+ def delete(key)
141
+ ttl_evict
142
+
143
+ @data_ttl.delete(key)
144
+ @data_lru.delete(key)
145
+ end
146
+
147
+ alias_method :evict, :delete
148
+
149
+ def key?(key)
150
+ ttl_evict
151
+
152
+ @data_lru.key?(key)
153
+ end
154
+
155
+ alias_method :has_key?, :key?
156
+
157
+ def clear
158
+ @data_ttl.clear
159
+ @data_lru.clear
160
+ end
161
+
162
+ def expire
163
+ ttl_evict
164
+ end
165
+
166
+ def count
167
+ @data_lru.size
168
+ end
169
+
170
+ protected
171
+
172
+ def valid_max_size?(max_size)
173
+ return true if max_size.is_a?(Integer) && max_size >= 1
174
+
175
+ false
176
+ end
177
+
178
+ def valid_ttl?(ttl)
179
+ return true if ttl == :none
180
+ return true if ttl.is_a?(Numeric) && ttl >= 0
181
+
182
+ false
183
+ end
184
+
185
+ def valid_ignore_nil?(ignore_nil)
186
+ return true if [true, false].include?(ignore_nil)
187
+
188
+ false
189
+ end
190
+
191
+ # for cache validation only, ensures all is sound
192
+ def valid?
193
+ @data_lru.size == @data_ttl.size
194
+ end
195
+
196
+ def ttl_evict
197
+ return if @ttl == :none
198
+
199
+ ttl_horizon = Time.now.to_f - @ttl
200
+ key, time = @data_ttl.first
201
+
202
+ until time.nil? || time > ttl_horizon
203
+ @data_ttl.delete(key)
204
+ @data_lru.delete(key)
205
+
206
+ key, time = @data_ttl.first
207
+ end
208
+ end
209
+
210
+ def resize
211
+ ttl_evict
212
+
213
+ while @data_lru.size > @max_size
214
+ key, _ = @data_lru.first
215
+
216
+ @data_ttl.delete(key)
217
+ @data_lru.delete(key)
218
+ end
219
+ end
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LruRedux::TTL::ThreadSafeCache < LruRedux::TTL::Cache
4
+ include LruRedux::Util::SafeSync
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lru_redux/util'
4
+ require 'lru_redux/ttl/cache'
5
+ require 'lru_redux/ttl/thread_safe_cache'
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'monitor'
4
+
5
+ module LruRedux
6
+ module Util
7
+ module SafeSync
8
+ include MonitorMixin
9
+
10
+ def initialize(*args)
11
+ super(*args)
12
+ end
13
+
14
+ def max_size=(max_size)
15
+ synchronize do
16
+ super(max_size)
17
+ end
18
+ end
19
+
20
+ def ttl=(ttl)
21
+ synchronize do
22
+ super(ttl)
23
+ end
24
+ end
25
+
26
+ def ignore_nil=(ignore_nil)
27
+ synchronize do
28
+ super(ignore_nil)
29
+ end
30
+ end
31
+
32
+ def getset(key)
33
+ synchronize do
34
+ super(key)
35
+ end
36
+ end
37
+
38
+ def fetch(key)
39
+ synchronize do
40
+ super(key)
41
+ end
42
+ end
43
+
44
+ def [](key)
45
+ synchronize do
46
+ super(key)
47
+ end
48
+ end
49
+
50
+ def []=(key, value)
51
+ synchronize do
52
+ super(key, value)
53
+ end
54
+ end
55
+
56
+ def each
57
+ synchronize do
58
+ super
59
+ end
60
+ end
61
+
62
+ def to_a
63
+ synchronize do
64
+ super
65
+ end
66
+ end
67
+
68
+ def values
69
+ synchronize do
70
+ super
71
+ end
72
+ end
73
+
74
+ def delete(key)
75
+ synchronize do
76
+ super(key)
77
+ end
78
+ end
79
+
80
+ def evict(key)
81
+ synchronize do
82
+ super(key)
83
+ end
84
+ end
85
+
86
+ def key?(key)
87
+ synchronize do
88
+ super(key)
89
+ end
90
+ end
91
+
92
+ def has_key?(key) # rubocop:disable Naming/PredicateName
93
+ synchronize do
94
+ super(key)
95
+ end
96
+ end
97
+
98
+ def clear
99
+ synchronize do
100
+ super
101
+ end
102
+ end
103
+
104
+ def count
105
+ synchronize do
106
+ super
107
+ end
108
+ end
109
+
110
+ def valid?
111
+ synchronize do
112
+ super
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lru_redux/util/safe_sync'
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LruRedux
4
+ VERSION = '2.0.0'
5
+ end
data/lib/lru_redux.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lru_redux/util'
4
+ require 'lru_redux/cache'
5
+ require 'lru_redux/thread_safe_cache'
6
+ require 'lru_redux/ttl'
7
+ require 'lru_redux/version'
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'lru_redux/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'sin_lru_redux'
9
+ spec.version = LruRedux::VERSION
10
+ spec.description = 'An efficient implementation of an lru cache'
11
+ spec.summary = 'An efficient implementation of an lru cache'
12
+ spec.authors = ['Masahiro']
13
+ spec.email = ['watanabe@cadenza-tech.com']
14
+ spec.license = 'MIT'
15
+
16
+ github_root_uri = 'https://github.com/cadenza-tech/sin_lru_redux'
17
+ spec.homepage = "#{github_root_uri}/tree/#{spec.version}/#{spec.name}"
18
+ spec.metadata = {
19
+ 'homepage_uri' => spec.homepage,
20
+ 'source_code_uri' => spec.homepage,
21
+ 'changelog_uri' => "#{github_root_uri}/blob/#{spec.version}#changelog",
22
+ 'bug_tracker_uri' => "#{github_root_uri}/issues",
23
+ 'documentation_uri' => "https://rubydoc.info/gems/#{spec.name}/#{spec.version}",
24
+ 'rubygems_mfa_required' => 'true'
25
+ }
26
+
27
+ spec.required_ruby_version = '>= 2.3.0'
28
+
29
+ spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
30
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
31
+ spec.require_paths = ['lib']
32
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lru_redux'
4
+ require 'minitest/autorun'
5
+ require 'minitest/pride'
6
+
7
+ class CacheTest < Minitest::Test
8
+ def setup
9
+ @c = LruRedux::Cache.new(3)
10
+ end
11
+
12
+ def teardown
13
+ assert @c.send(:valid?) # rubocop:disable Minitest/AssertionInLifecycleHook
14
+ end
15
+
16
+ def test_drops_old
17
+ @c[:a] = 1
18
+ @c[:b] = 2
19
+ @c[:c] = 3
20
+ @c[:d] = 4
21
+
22
+ assert_equal [[:d, 4], [:c, 3], [:b, 2]], @c.to_a
23
+ assert_nil @c[:a]
24
+ end
25
+
26
+ def test_fetch
27
+ @c[:a] = nil
28
+ @c[:b] = 2
29
+
30
+ assert_nil @c.fetch(:a) { 1 } # rubocop:disable Style/RedundantFetchBlock
31
+ assert_equal 3, @c.fetch(:c) { 3 } # rubocop:disable Style/RedundantFetchBlock
32
+ assert_equal [[:a, nil], [:b, 2]], @c.to_a
33
+ end
34
+
35
+ def test_getset # rubocop:disable Minitest/MultipleAssertions
36
+ assert_equal 1, @c.getset(:a) { 1 }
37
+ @c.getset(:b) { 2 }
38
+
39
+ assert_equal 1, @c.getset(:a) { 11 }
40
+ @c.getset(:c) { 3 }
41
+
42
+ assert_equal 4, @c.getset(:d) { 4 }
43
+ assert_equal [[:d, 4], [:c, 3], [:a, 1]], @c.to_a
44
+ end
45
+
46
+ def test_pushes_lru_to_back
47
+ @c[:a] = 1
48
+ @c[:b] = 2
49
+ @c[:c] = 3
50
+
51
+ @c[:a]
52
+ @c[:d] = 4
53
+
54
+ assert_equal [[:d, 4], [:a, 1], [:c, 3]], @c.to_a
55
+ assert_nil @c[:b]
56
+ end
57
+
58
+ def test_delete # rubocop:disable Minitest/MultipleAssertions
59
+ @c[:a] = 1
60
+ @c[:b] = 2
61
+ @c[:c] = 3
62
+ @c.delete(:a)
63
+
64
+ assert_equal [[:c, 3], [:b, 2]], @c.to_a
65
+ assert_nil @c[:a]
66
+
67
+ # Regression test for a bug in the legacy delete method
68
+ @c.delete(:b)
69
+ @c[:d] = 4
70
+ @c[:e] = 5
71
+ @c[:f] = 6
72
+
73
+ assert_equal [[:f, 6], [:e, 5], [:d, 4]], @c.to_a
74
+ assert_nil @c[:b]
75
+ end
76
+
77
+ def test_key?
78
+ @c[:a] = 1
79
+ @c[:b] = 2
80
+
81
+ assert @c.key?(:a)
82
+ refute @c.key?(:c)
83
+ end
84
+
85
+ def test_update
86
+ @c[:a] = 1
87
+ @c[:b] = 2
88
+ @c[:c] = 3
89
+ @c[:a] = 99
90
+
91
+ assert_equal [[:a, 99], [:c, 3], [:b, 2]], @c.to_a
92
+ end
93
+
94
+ def test_clear
95
+ @c[:a] = 1
96
+ @c[:b] = 2
97
+ @c[:c] = 3
98
+
99
+ @c.clear
100
+
101
+ assert_empty @c.to_a
102
+ end
103
+
104
+ def test_grow
105
+ @c[:a] = 1
106
+ @c[:b] = 2
107
+ @c[:c] = 3
108
+ @c.max_size = 4
109
+ @c[:d] = 4
110
+
111
+ assert_equal [[:d, 4], [:c, 3], [:b, 2], [:a, 1]], @c.to_a
112
+ end
113
+
114
+ def test_shrink
115
+ @c[:a] = 1
116
+ @c[:b] = 2
117
+ @c[:c] = 3
118
+ @c.max_size = 1
119
+
120
+ assert_equal [[:c, 3]], @c.to_a
121
+ end
122
+
123
+ def test_each
124
+ @c.max_size = 2
125
+ @c[:a] = 1
126
+ @c[:b] = 2
127
+ @c[:c] = 3
128
+
129
+ pairs = []
130
+ @c.each do |pair| # rubocop:disable Style/MapIntoArray
131
+ pairs << pair
132
+ end
133
+
134
+ assert_equal [[:c, 3], [:b, 2]], pairs
135
+ end
136
+
137
+ def test_values
138
+ @c[:a] = 1
139
+ @c[:b] = 2
140
+ @c[:c] = 3
141
+ @c[:d] = 4
142
+
143
+ assert_equal [4, 3, 2], @c.values
144
+ assert_nil @c[:a]
145
+ end
146
+ end