ring_cache 1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ring_cache.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Alvaro Redondo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # RingCache
2
+
3
+ RingCache is an in-memory cache that emulates a ring buffer, in which older elements are evicted to make room for new ones. It is mostly useful in situations in which it is not worth it, or possible, to keep all accessed data in memory, and some elements are more frequently accessed than others.
4
+
5
+ As a ring buffer, it can work with a fix capacity. In addition, it allows the possibility of specifying a target hit rate, above which it will evict the elements that were accessed last. This should make it easier to optimize the amount of memory used when the hit rate becomes insensitive to the capacity over a given threshold.
6
+
7
+ ## Installation
8
+
9
+ RingCache does not have any dependency apart from Ruby.
10
+
11
+ To install with Bundle, add the following line to the Gemfile:
12
+
13
+ gem 'ring_cache'
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Otherwise, it can be installed with Rubygems as follows:
20
+
21
+ $ gem install ring_cache
22
+
23
+ ## Cache Initialization
24
+
25
+ All options accepted by RingCache are set when initializing the cache.
26
+
27
+ The options related to the cache size are:
28
+
29
+ * `capacity`: The maximum number of elements that the cache will hold. By default, the capacity is unlimited.
30
+
31
+ * `target_hit_rate`: The cache will keep on growing in size until this target is achieved. Then, it will evict elements to make room for new ones to maintain its current size—as long as the hit rate is kept over this threshold. If the hit rate falls below the threshold, the cache will increase its size again. Size is always limited by the `capacity` option. By default, there is no target hit rate.
32
+
33
+ The following options allow some control over the stored data:
34
+
35
+ * `duplicate_on_store` : Store a duplicate of the element obtained with `dup` rather than the element provided to the cache.
36
+ * `duplicate_on_retrieve`: Return a duplicate of the accessed data rather than the accessed data itself.
37
+ * `execute_on_retrieve`: Method or array of methods that should be executed on accessed data before returning it. This could be used, for example, to ensure that returned data is fresh.
38
+
39
+ Initialization example:
40
+
41
+ ```ruby
42
+ cache = RingCache.new(
43
+ capacity: 2_000,
44
+ target_hit_rate: 0.9,
45
+ execute_on_retrieve: :reload
46
+ )
47
+ ```
48
+
49
+ ## Usage
50
+
51
+ To access data, use the `read` and `write` methods:
52
+
53
+ ```ruby
54
+ cache.write(:example, 'Lorem ipsum')
55
+ test = cache.read(:example)
56
+ # => "Lorem ipsum"
57
+ ```
58
+
59
+ Both keys and values can be any data type.
60
+
61
+ Use `fetch` as a shortcut to provide missing, more-costly-to-load data in a block:
62
+
63
+ ```ruby
64
+ cache.fetch(:example) do
65
+ 'Lorem ipsum'
66
+ end
67
+ # => "Lorem ipsum"
68
+ ```
69
+
70
+ Use `evict` to remove an element from the cache:
71
+
72
+ ```ruby
73
+ cache.evict(:example)
74
+ # => true
75
+ ```
76
+
77
+ And `reset` to completely erase the cache contents—while maintaining initialization options.
78
+
79
+ There are other methods that just return information:
80
+
81
+ * `has_key?(key)`: Returns true if the cache contains an element indexed by this key. Otherwise, false.
82
+ * `hit_rate`: Current hit rate of the cache. This is a number between 0 and 1.
83
+ * `last_access(key)`: Time when the element indexed by this key was last accessed.
84
+ * `size`: Number of elements currently stored in the cache.
85
+
86
+ ## Contributing
87
+
88
+ Please, fork the repository, make your changes, and submit a pull request. Thanks!
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs << 'test'
6
+ t.test_files = FileList['test/test*.rb']
7
+ t.verbose = true
8
+ end
@@ -0,0 +1,3 @@
1
+ class RingCache
2
+ VERSION = '1.0'
3
+ end
data/lib/ring_cache.rb ADDED
@@ -0,0 +1,120 @@
1
+ lib = File.dirname(__FILE__)
2
+ $LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib)
3
+
4
+ require 'ring_cache/version'
5
+ require 'set'
6
+
7
+ class RingCache
8
+ attr_reader :capacity, :target_hit_rate
9
+
10
+ def initialize(options = {})
11
+ @duplicate_on_store = options.fetch(:duplicate_on_store, false)
12
+ @duplicate_on_retrieve = options.fetch(:duplicate_on_retrieve, false)
13
+
14
+ execute_on_retrieve = options.fetch(:execute_on_retrieve, [])
15
+ @execute_on_retrieve = execute_on_retrieve.kind_of?(Array) ? execute_on_retrieve : [execute_on_retrieve]
16
+
17
+ @capacity = options.fetch(:capacity, nil)
18
+ @target_hit_rate = options.fetch(:target_hit_rate, nil)
19
+ unless @target_hit_rate.nil? or (@target_hit_rate > 0.0 and @target_hit_rate < 1.0)
20
+ raise ArgumentError, 'Invalid target_hit_rate'
21
+ end
22
+
23
+ reset
24
+ end
25
+
26
+ def evict(key)
27
+ if @cache.has_key?(key)
28
+ @access_time_index.delete([@cache[key][:last_accessed_at], key])
29
+ @cache.delete(key)
30
+ true
31
+ else
32
+ false
33
+ end
34
+ end
35
+
36
+ def fetch(key, &block)
37
+ unless (data = read(key))
38
+ data = block.call
39
+ write(key, data)
40
+ end
41
+ data
42
+ end
43
+
44
+ def has_key?(key)
45
+ @cache.has_key?(key)
46
+ end
47
+
48
+ def hit_rate
49
+ (@access_count > 0) ? (@hit_count / @access_count.to_f) : 0.0
50
+ end
51
+
52
+ def last_access(key)
53
+ has_key?(key) ? @cache[key][:last_accessed_at] : nil
54
+ end
55
+
56
+ def read(key)
57
+ @access_count += 1
58
+
59
+ if @cache.has_key?(key)
60
+ access_time = Time.now
61
+ @access_time_index.delete([@cache[key][:last_accessed_at], key])
62
+ @access_time_index << [access_time, key]
63
+ @cache[key][:last_accessed_at] = access_time
64
+
65
+ @hit_count += 1
66
+
67
+ data = @cache[key][:data]
68
+ data = data.dup if @duplicate_on_retrieve and !data.nil?
69
+
70
+ unless @execute_on_retrieve.empty? or data.nil?
71
+ @execute_on_retrieve.each do |method|
72
+ method = method.to_sym
73
+ if data.respond_to?(method)
74
+ data.send(method)
75
+ elsif data.kind_of?(Enumerable) and data.all? { |d| d.respond_to?(method) }
76
+ data.each { |d| d.send(method) }
77
+ else
78
+ raise RuntimeError, "Retrieved data does not respond to #{method.inspect}"
79
+ end
80
+ end
81
+ end
82
+
83
+ data
84
+ else
85
+ nil
86
+ end
87
+ end
88
+
89
+ def reset
90
+ @cache = {}
91
+ @access_time_index = SortedSet.new
92
+ @access_count = 0
93
+ @hit_count = 0
94
+ true
95
+ end
96
+
97
+ def size
98
+ @cache.size
99
+ end
100
+
101
+ def write(key, data)
102
+ evict_oldest if must_evict?
103
+ data = data.dup if @duplicate_on_store and !data.nil?
104
+ access_time = Time.now
105
+ @cache[key] = { last_accessed_at: access_time, data: data }
106
+ @access_time_index << [access_time, key]
107
+ end
108
+
109
+ private
110
+
111
+ def evict_oldest
112
+ access_time_index_entry = @access_time_index.first
113
+ @cache.delete(access_time_index_entry[1])
114
+ @access_time_index.delete(access_time_index_entry)
115
+ end
116
+
117
+ def must_evict?
118
+ (capacity and size >= capacity) or (target_hit_rate and hit_rate >= target_hit_rate)
119
+ end
120
+ end
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'ring_cache/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'ring_cache'
8
+ spec.version = RingCache::VERSION
9
+ spec.authors = ['Alvaro Redondo']
10
+ spec.email = ['alvaro@redondo.name']
11
+ spec.summary = %q{In-memory cache that emulates a ring buffer.}
12
+ spec.description = %q{RingCache is an in-memory cache that emulates a ring buffer, in which older elements are evicted to make room for new ones.}
13
+ spec.homepage = 'https://github.com/aredondo/ring_cache'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_development_dependency 'bundler', '~> 1.6'
22
+ spec.add_development_dependency 'rake'
23
+ end
@@ -0,0 +1,16 @@
1
+ module RandomDataGenerator
2
+ private
3
+
4
+ def random_data(element_count, key_length)
5
+ data = []
6
+ letters = ('a' .. 'z').to_a
7
+
8
+ while data.size < element_count
9
+ key = letters.sample(key_length).join
10
+ content = letters.sample(1)
11
+ data << { key: key, content: content }
12
+ end
13
+
14
+ data
15
+ end
16
+ end
@@ -0,0 +1,10 @@
1
+ lib = File.expand_path('../lib', File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib)
3
+
4
+ require 'ring_cache'
5
+ require 'minitest'
6
+ require 'minitest/autorun'
7
+ require 'minitest/reporters'
8
+ require File.expand_path('random_data_generator', File.dirname(__FILE__))
9
+
10
+ Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path('test_helper', File.dirname(__FILE__))
4
+
5
+ class TestRingCache < Minitest::Test
6
+ include RandomDataGenerator
7
+
8
+ def test_generic_checks
9
+ c = RingCache.new(capacity: 3)
10
+ c.write(:a, 1)
11
+ assert_equal 0.0, c.hit_rate
12
+ assert_equal 1, c.read(:a)
13
+ assert_equal 1.0, c.hit_rate
14
+ assert c.has_key?(:a)
15
+ assert_kind_of Time, c.last_access(:a)
16
+ assert_equal 1, c.size
17
+ assert_equal 1, c.instance_variable_get(:@access_time_index).size
18
+
19
+ c.write(:b, 2)
20
+ assert_equal 2, c.read(:b)
21
+ assert_equal 1.0, c.hit_rate
22
+ assert c.has_key?(:b)
23
+ assert_kind_of Time, c.last_access(:b)
24
+ assert_equal 2, c.size
25
+ assert_equal 2, c.instance_variable_get(:@access_time_index).size
26
+
27
+ c.write(:c, 3)
28
+ assert_equal 3, c.read(:c)
29
+ assert_equal 1.0, c.hit_rate
30
+ assert c.has_key?(:c)
31
+ assert_kind_of Time, c.last_access(:c)
32
+ assert_equal 3, c.size
33
+ assert_equal 3, c.instance_variable_get(:@access_time_index).size
34
+
35
+ c.write(:d, 4)
36
+ assert_equal 4, c.read(:d)
37
+ assert_equal 1.0, c.hit_rate
38
+ assert c.has_key?(:d)
39
+ assert_kind_of Time, c.last_access(:d)
40
+ assert_equal 3, c.size
41
+ assert_equal 3, c.instance_variable_get(:@access_time_index).size
42
+ refute c.has_key?(:a)
43
+
44
+ value = c.fetch(:b) do
45
+ 4
46
+ end
47
+ assert_equal 2, value
48
+ assert_equal 1.0, c.hit_rate
49
+
50
+ value = c.fetch(:e) do
51
+ 5
52
+ end
53
+ assert_equal 5, value
54
+ assert_equal 3, c.size
55
+ assert_equal 3, c.instance_variable_get(:@access_time_index).size
56
+ refute c.has_key?(:c)
57
+ assert_equal 5 / 6.0, c.hit_rate
58
+
59
+ assert c.last_access(:b) > c.last_access(:d)
60
+ assert c.last_access(:d) < c.last_access(:e)
61
+
62
+ c.evict(:d)
63
+ refute c.has_key?(:d)
64
+ assert_equal 2, c.size
65
+ assert_equal 2, c.instance_variable_get(:@access_time_index).size
66
+
67
+ value = c.read(:another)
68
+ assert_equal 5 / 7.0, c.hit_rate
69
+
70
+ c.reset
71
+ assert_equal 0, c.size
72
+ assert_equal 0.0, c.hit_rate
73
+ end
74
+
75
+ def test_execute_method_on_retrieve
76
+ test_class = Class.new
77
+ test_class.class_eval do
78
+ attr_reader :reloaded
79
+ define_method(:inititalize) { @reloaded = false }
80
+ define_method(:reload) { @reloaded = true }
81
+ end
82
+
83
+ cache = RingCache.new(execute_on_retrieve: :reload)
84
+
85
+ data = test_class.new
86
+ refute data.reloaded
87
+ cache.write(:d1, data)
88
+ retrieved_data = cache.read(:d1)
89
+ assert retrieved_data.reloaded, retrieved_data.reloaded.inspect
90
+
91
+ data = [
92
+ test_class.new,
93
+ test_class.new,
94
+ test_class.new
95
+ ]
96
+ refute data.any? { |d| d.reloaded }
97
+ cache.write(:d2, data)
98
+ retrieved_data = cache.read(:d2)
99
+ assert retrieved_data.all? { |e| e.reloaded }, retrieved_data.inspect
100
+
101
+ data = Object.new
102
+ refute_respond_to data, :reloaded
103
+ cache.write(:d3, data)
104
+ assert_raises RuntimeError do
105
+ retrieved_data = cache.read(:d3)
106
+ end
107
+ end
108
+
109
+ def test_object_duplication_on_retrieve
110
+ c = RingCache.new(duplicate_on_retrieve: true)
111
+ data = {a: 1, b: 2, c: 3}
112
+ c.write(:d, data)
113
+ assert c.has_key?(:d)
114
+ retrieved_data = c.read(:d)
115
+ assert data == retrieved_data
116
+ refute data.equal?(retrieved_data)
117
+ end
118
+
119
+ def test_object_duplication_on_store
120
+ c = RingCache.new(duplicate_on_store: true)
121
+ data = {a: 1, b: 2, c: 3}
122
+ c.write(:d, data)
123
+ assert c.has_key?(:d)
124
+ retrieved_data = c.read(:d)
125
+ assert data == retrieved_data
126
+ refute data.equal?(retrieved_data)
127
+ end
128
+
129
+ def test_random_data
130
+ cache = RingCache.new(capacity: 1_000)
131
+ data = random_data(2_000, 10)
132
+
133
+ data.each do |element|
134
+ cache.fetch(element[:key]) do
135
+ element[:content]
136
+ end
137
+ end
138
+
139
+ assert_equal 1_000, cache.size
140
+ access_time_index = cache.instance_variable_get(:@access_time_index)
141
+ cache_contents = cache.instance_variable_get(:@cache)
142
+ assert_equal cache_contents.size, access_time_index.size
143
+
144
+ access_time_index.each do |access_time_index_entry|
145
+ assert cache_contents.has_key?(access_time_index_entry[1])
146
+ assert_equal access_time_index_entry[0],
147
+ cache_contents[access_time_index_entry[1]][:last_accessed_at]
148
+ end
149
+ end
150
+
151
+ def test_target_hit_rate
152
+ cache = RingCache.new(capacity: 4, target_hit_rate: 0.5)
153
+
154
+ cache.fetch(:a) { 1 }
155
+ cache.fetch(:b) { 2 }
156
+ cache.read(:a)
157
+ cache.read(:b)
158
+
159
+ assert_equal 0.5, cache.hit_rate
160
+ assert_equal 2, cache.size
161
+
162
+ cache.read(:a)
163
+ cache.read(:b)
164
+ cache.fetch(:c) { 3 }
165
+
166
+ assert_equal 4 / 7.0, cache.hit_rate
167
+ assert_equal 2, cache.size
168
+ assert cache.has_key?(:c)
169
+
170
+ cache.fetch(:d) { 4 }
171
+ assert_equal 4 / 8.0, cache.hit_rate
172
+ assert_equal 2, cache.size
173
+ assert cache.has_key?(:d)
174
+ end
175
+
176
+ def test_works_with_nil
177
+ cache = RingCache.new(
178
+ duplicate_on_store: true,
179
+ duplicate_on_retrieve: true,
180
+ execute_on_retrieve: :reload
181
+ )
182
+ data = nil
183
+ cache.write(:d, data)
184
+ assert_nil cache.read(:d)
185
+ end
186
+ end
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path('test_helper', File.dirname(__FILE__))
4
+ require 'minitest/benchmark'
5
+
6
+ class TestRingCachePerformance < Minitest::Benchmark
7
+ include RandomDataGenerator
8
+
9
+ def self.bench_range
10
+ bench_exp(100, 100_000, 10)
11
+ end
12
+
13
+ def setup
14
+ @data = random_data(1_000, 10)
15
+ end
16
+
17
+ # Computer-dependant -- but should be a reasonable baseline
18
+ def bench_performance
19
+ if ENV['BENCH']
20
+ validation = lambda { |ranges, times|
21
+ count_per_second = ranges.last / times.last.to_f
22
+ assert count_per_second > 100_000, 'Count per second: %.2f' % count_per_second
23
+ }
24
+
25
+ assert_performance validation do |n|
26
+ run_cache(n)
27
+ end
28
+ end
29
+ end
30
+
31
+ def bench_performance_linear
32
+ if ENV['BENCH']
33
+ assert_performance_linear do |n|
34
+ run_cache(n)
35
+ end
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def run_cache(times, capacity = 1_000)
42
+ cache = RingCache.new(capacity: capacity)
43
+ count = 0
44
+ while count < times
45
+ element = @data.sample
46
+ cache.fetch(element[:key]) do
47
+ element[:content]
48
+ end
49
+ count += 1
50
+ end
51
+ end
52
+ end
metadata ADDED
@@ -0,0 +1,95 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ring_cache
3
+ version: !ruby/object:Gem::Version
4
+ version: '1.0'
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Alvaro Redondo
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-10-18 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.6'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '1.6'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description: RingCache is an in-memory cache that emulates a ring buffer, in which
47
+ older elements are evicted to make room for new ones.
48
+ email:
49
+ - alvaro@redondo.name
50
+ executables: []
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - .gitignore
55
+ - Gemfile
56
+ - LICENSE.txt
57
+ - README.md
58
+ - Rakefile
59
+ - lib/ring_cache.rb
60
+ - lib/ring_cache/version.rb
61
+ - ring_cache.gemspec
62
+ - test/random_data_generator.rb
63
+ - test/test_helper.rb
64
+ - test/test_ring_cache.rb
65
+ - test/test_ring_cache_performance.rb
66
+ homepage: https://github.com/aredondo/ring_cache
67
+ licenses:
68
+ - MIT
69
+ post_install_message:
70
+ rdoc_options: []
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ! '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ none: false
81
+ requirements:
82
+ - - ! '>='
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ requirements: []
86
+ rubyforge_project:
87
+ rubygems_version: 1.8.29
88
+ signing_key:
89
+ specification_version: 3
90
+ summary: In-memory cache that emulates a ring buffer.
91
+ test_files:
92
+ - test/random_data_generator.rb
93
+ - test/test_helper.rb
94
+ - test/test_ring_cache.rb
95
+ - test/test_ring_cache_performance.rb