sin_lru_redux 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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