timed_lru 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ *.gem
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "http://rubygems.org"
2
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,29 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ timed_lru (0.3.1)
5
+
6
+ GEM
7
+ remote: http://rubygems.org/
8
+ specs:
9
+ diff-lcs (1.2.1)
10
+ rake (10.0.3)
11
+ rspec (2.13.0)
12
+ rspec-core (~> 2.13.0)
13
+ rspec-expectations (~> 2.13.0)
14
+ rspec-mocks (~> 2.13.0)
15
+ rspec-core (2.13.0)
16
+ rspec-expectations (2.13.0)
17
+ diff-lcs (>= 1.1.3, < 2.0)
18
+ rspec-mocks (2.13.0)
19
+ yard (0.8.5.2)
20
+
21
+ PLATFORMS
22
+ ruby
23
+
24
+ DEPENDENCIES
25
+ bundler
26
+ rake
27
+ rspec
28
+ timed_lru!
29
+ yard
data/README.md ADDED
@@ -0,0 +1,63 @@
1
+ Timed LRU
2
+ =========
3
+
4
+ My implementation of a simple, thread-safe LRU with (optional) TTLs
5
+ and constant time operations. There are many LRUs for Ruby available but
6
+ I was unable to find one that matches all three requirements.
7
+
8
+ Install
9
+ -------
10
+
11
+ Install it via `gem`:
12
+
13
+ gem install timed_lru
14
+
15
+ Or just bundle it with your project.
16
+
17
+ Usage Example
18
+ -------------
19
+
20
+ # Initialize with a max size (default: 100) and a TTL (default: none)
21
+ lru = TimedLRU.new max_size: 3, ttl: 5
22
+
23
+ # Add values
24
+ lru["a"] = "value 1"
25
+ lru["b"] = "value 2"
26
+ lru["c"] = "value 3"
27
+ lru.keys # => ["a", "b"]
28
+
29
+ # Wait a second
30
+ sleep(1)
31
+
32
+ # Add more values
33
+ lru["d"] = "value 4"
34
+ lru.keys # => ["b", "c", "d"]
35
+
36
+ # Sleep a little longer
37
+ sleep(4)
38
+ lru["c"] # => "value 3"
39
+ lru.keys # => ["c", "d"]
40
+
41
+ Licence
42
+ -------
43
+
44
+ Copyright (c) 2013 Black Square Media Ltd
45
+
46
+ Permission is hereby granted, free of charge, to any person obtaining
47
+ a copy of this software and associated documentation files (the
48
+ "Software"), to deal in the Software without restriction, including
49
+ without limitation the rights to use, copy, modify, merge, publish,
50
+ distribute, sublicense, and/or sell copies of the Software, and to
51
+ permit persons to whom the Software is furnished to do so, subject to
52
+ the following conditions:
53
+
54
+ The above copyright notice and this permission notice shall be
55
+ included in all copies or substantial portions of the Software.
56
+
57
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
58
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
59
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
60
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
61
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
62
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
63
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require 'rake'
2
+
3
+ require 'rspec/mocks/version'
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ require 'yard'
8
+ YARD::Rake::YardocTask.new
9
+
10
+ desc 'Default: run specs.'
11
+ task :default => :spec
data/lib/timed_lru.rb ADDED
@@ -0,0 +1,136 @@
1
+ require 'monitor'
2
+ require 'forwardable'
3
+
4
+ class TimedLRU
5
+ include MonitorMixin
6
+ extend Forwardable
7
+
8
+ module ThreadUnsafe
9
+ def mon_synchronize
10
+ yield
11
+ end
12
+ end
13
+
14
+ Node = Struct.new(:key, :value, :left, :right, :expires_at)
15
+ def_delegators :@hash, :size, :keys, :each_key, :empty?
16
+
17
+ # @attr_reader [Integer] max_size
18
+ attr_reader :max_size
19
+
20
+ # @attr_reader [Integer,NilClass] ttl
21
+ attr_reader :ttl
22
+
23
+ # @param [Hash] opts options
24
+ # @option opts [Integer] max_size
25
+ # maximum allowed number of items, defaults to 100
26
+ # @option opts [Boolean] thread_safe
27
+ # true by default, set to false if you are not using threads a *really* need
28
+ # that extra bit of performance
29
+ # @option opts [Integer] ttl
30
+ # the TTL in seconds
31
+ def initialize(opts = {})
32
+ super() # MonitorMixin
33
+
34
+ @hash = {}
35
+ @max_size = Integer(opts[:max_size] || 100)
36
+ @ttl = Integer(opts[:ttl]) if opts[:ttl]
37
+
38
+ raise ArgumentError, "Option :max_size must be > 0" unless max_size > 0
39
+ raise ArgumentError, "Option :ttl must be > 0" unless ttl.nil? || ttl > 0
40
+
41
+ extend ThreadUnsafe if opts[:thread_safe] == false
42
+ end
43
+
44
+ # Stores a `value` by `key`
45
+ # @param [Object] key the storage key
46
+ # @param [Object] value the associated value
47
+ # @return [Object] the value
48
+ def store(key, value)
49
+ mon_synchronize do
50
+ node = (@hash[key] ||= Node.new(key))
51
+ node.value = value
52
+ touch(node)
53
+ compact!
54
+ node.value
55
+ end
56
+ end
57
+ alias_method :[]=, :store
58
+
59
+ # Retrieves a `value` by `key`
60
+ # @param [Object] key the storage key
61
+ # @return [Object,NilClass] value the associated value (or nil)
62
+ def fetch(key)
63
+ mon_synchronize do
64
+ node = @hash[key]
65
+ break unless node
66
+
67
+ touch(node)
68
+ node.value
69
+ end
70
+ end
71
+ alias_method :[], :fetch
72
+
73
+ # Deletes by `key`
74
+ # @param [Object] key the storage key
75
+ # @return [Object,NilClass] value the deleted value (or nil)
76
+ def delete(key)
77
+ mon_synchronize do
78
+ node = @hash[key]
79
+ remove(node).value if node
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ def compact!
86
+ while @hash.size > max_size
87
+ remove(@tail)
88
+ end
89
+
90
+ while ttl && @tail.expires_at < Time.now.to_i
91
+ remove(@tail)
92
+ end
93
+ end
94
+
95
+ def remove(node)
96
+ @hash.delete(node.key)
97
+ left, right = node.left, node.right
98
+ left.nil? ? @head = right : left.right = right
99
+ right.nil? ? @tail = left : right.left = left
100
+ node
101
+ end
102
+
103
+ def touch(node)
104
+ node.expires_at = Time.now.to_i + ttl if ttl
105
+ return if node == @head
106
+
107
+ left, right = node.left, node.right
108
+ node.left, node.right = nil, @head
109
+ @head.left = node if @head
110
+
111
+ left.right = right if left
112
+ right.left = left if right
113
+
114
+ @tail = left if @tail == node
115
+ @head = node
116
+ @tail = @head unless @tail
117
+ end
118
+
119
+ end
120
+
121
+ # right ? : @tail = left
122
+
123
+ # return if node == @head
124
+ # if @head
125
+ # @head.left = node
126
+ # @head.right = right if @head.right == node
127
+ # end
128
+
129
+ # if right
130
+ # right.left = left
131
+ # end
132
+
133
+ # if @tail == node
134
+ # left.right = nil
135
+ # @tail = left
136
+ # end
@@ -0,0 +1,4 @@
1
+ require 'bundler/setup'
2
+ require 'rspec'
3
+ require 'timed_lru'
4
+
@@ -0,0 +1,177 @@
1
+ require 'spec_helper'
2
+
3
+ describe TimedLRU do
4
+
5
+ subject { described_class.new max_size: 4 }
6
+
7
+ def full_chain
8
+ return [] unless head
9
+
10
+ res = [head]
11
+ while curr = res.last.right
12
+ curr.left.should == res.last
13
+ res << curr
14
+ end
15
+ head.left.should be_nil
16
+ tail.right.should be_nil
17
+ res.last.should == tail
18
+ res
19
+ end
20
+
21
+ def chain
22
+ full_chain.map &:key
23
+ end
24
+
25
+ def head
26
+ subject.instance_variable_get(:@head)
27
+ end
28
+
29
+ def tail
30
+ subject.instance_variable_get(:@tail)
31
+ end
32
+
33
+ describe "defaults" do
34
+ subject { described_class.new }
35
+
36
+ its(:max_size) { should be(100) }
37
+ its(:ttl) { should be_nil }
38
+ it { should be_a(MonitorMixin) }
39
+ it { should_not be_a(described_class::ThreadUnsafe) }
40
+ it { should respond_to(:empty?) }
41
+ it { should respond_to(:keys) }
42
+ it { should respond_to(:size) }
43
+ it { should respond_to(:each_key) }
44
+ end
45
+
46
+ describe "init" do
47
+ subject { described_class.new max_size: 25, ttl: 120, thread_safe: false }
48
+
49
+ its(:max_size) { should be(25) }
50
+ its(:ttl) { should be(120) }
51
+ it { should be_a(described_class::ThreadUnsafe) }
52
+
53
+ it 'should assert correct option values' do
54
+ lambda { described_class.new(max_size: "X") }.should raise_error(ArgumentError)
55
+ lambda { described_class.new(max_size: -1) }.should raise_error(ArgumentError)
56
+ lambda { described_class.new(max_size: 0) }.should raise_error(ArgumentError)
57
+
58
+ lambda { described_class.new(ttl: "X") }.should raise_error(ArgumentError)
59
+ lambda { described_class.new(ttl: true) }.should raise_error(TypeError)
60
+ lambda { described_class.new(ttl: 0) }.should raise_error(ArgumentError)
61
+ end
62
+ end
63
+
64
+ describe "storing" do
65
+
66
+ it "should set head + tail on first item" do
67
+ lambda {
68
+ subject.store("a", 1).should == 1
69
+ }.should change { chain }.from([]).to(["a"])
70
+ end
71
+
72
+ it "should shift chain when new items are added" do
73
+ subject["a"] = 1
74
+ lambda { subject["b"] = 2 }.should change { chain }.from(%w|a|).to(%w|b a|)
75
+ lambda { subject["c"] = 3 }.should change { chain }.to(%w|c b a|)
76
+ lambda { subject["d"] = 4 }.should change { chain }.to(%w|d c b a|)
77
+ end
78
+
79
+ it "should expire LRU items when chain exceeds max size" do
80
+ ("a".."d").each {|x| subject[x] = 1 }
81
+ lambda { subject["e"] = 5 }.should change { chain }.to(%w|e d c b|)
82
+ lambda { subject["f"] = 6 }.should change { chain }.to(%w|f e d c|)
83
+ end
84
+
85
+ it "should update items" do
86
+ ("a".."d").each {|x| subject[x] = 1 }
87
+ lambda { subject["d"] = 2 }.should_not change { chain }
88
+ lambda { subject["c"] = 2 }.should change { chain }.to(%w|c d b a|)
89
+ lambda { subject["b"] = 2 }.should change { chain }.to(%w|b c d a|)
90
+ lambda { subject["a"] = 2 }.should change { chain }.to(%w|a b c d|)
91
+ end
92
+
93
+ end
94
+
95
+ describe "retrieving" do
96
+
97
+ it 'should fetch values' do
98
+ subject.fetch("a").should be_nil
99
+ subject["a"].should be_nil
100
+ subject["a"] = 1
101
+ subject["a"].should == 1
102
+ end
103
+
104
+ it 'should renew membership on access' do
105
+ ("a".."d").each {|x| subject[x] = 1 }
106
+ lambda { subject["d"] }.should_not change { chain }
107
+ lambda { subject["c"] }.should change { chain }.to(%w|c d b a|)
108
+ lambda { subject["b"] }.should change { chain }.to(%w|b c d a|)
109
+ lambda { subject["a"] }.should change { chain }.to(%w|a b c d|)
110
+ lambda { subject["x"] }.should_not change { chain }
111
+ end
112
+
113
+ end
114
+
115
+ describe "deleting" do
116
+
117
+ it 'should delete an return values' do
118
+ subject.delete("a").should be_nil
119
+ subject["a"] = 1
120
+ subject.delete("a").should == 1
121
+ end
122
+
123
+ it 'should re-arrange membership chain' do
124
+ ("a".."d").each {|x| subject[x] = 1 }
125
+ lambda { subject.delete("x") }.should_not change { chain }
126
+ lambda { subject.delete("c") }.should change { chain }.to(%w|d b a|)
127
+ lambda { subject.delete("a") }.should change { chain }.to(%w|d b|)
128
+ lambda { subject.delete("d") }.should change { chain }.to(%w|b|)
129
+ lambda { subject.delete("b") }.should change { subject.size }.from(1).to(0)
130
+ end
131
+
132
+ end
133
+
134
+ describe "TTL expiration" do
135
+ subject { described_class.new max_size: 4, ttl: 60 }
136
+
137
+ def in_past(ago)
138
+ Time.stub now: (Time.now - ago)
139
+ yield
140
+ ensure
141
+ Time.unstub :now
142
+ end
143
+
144
+ it 'should expire on access' do
145
+ in_past(70) do
146
+ subject["a"] = 1
147
+ chain.should == %w|a|
148
+ end
149
+
150
+ in_past(50) do
151
+ subject["b"] = 2
152
+ chain.should == %w|b a|
153
+ end
154
+
155
+ subject["c"] = 3
156
+ chain.should == %w|c b|
157
+ end
158
+
159
+ it 'should renew expiration on access' do
160
+ in_past(70) do
161
+ subject["a"] = 1
162
+ subject["b"] = 2
163
+ chain.should == %w|b a|
164
+ end
165
+
166
+ in_past(50) do
167
+ subject["a"].should == 1
168
+ chain.should == %w|a b|
169
+ end
170
+
171
+ subject["c"] = 3
172
+ chain.should == %w|c a|
173
+ end
174
+
175
+ end
176
+
177
+ end
data/timed_lru.gemspec ADDED
@@ -0,0 +1,22 @@
1
+ Gem::Specification.new do |s|
2
+ s.required_ruby_version = '>= 1.9.1'
3
+ s.required_rubygems_version = ">= 1.8.0"
4
+
5
+ s.name = File.basename(__FILE__, '.gemspec')
6
+ s.summary = "Timed LRU"
7
+ s.description = "Thread-safe LRU implementation with (optional) TTL and constant time operations"
8
+ s.version = "0.3.1"
9
+
10
+ s.authors = ["Black Square Media"]
11
+ s.email = "info@blacksquaremedia.com"
12
+ s.homepage = "https://github.com/bsm/timed_lru"
13
+
14
+ s.require_path = 'lib'
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+
18
+ s.add_development_dependency "rake"
19
+ s.add_development_dependency "bundler"
20
+ s.add_development_dependency "rspec"
21
+ s.add_development_dependency "yard"
22
+ end
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: timed_lru
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Black Square Media
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-03-04 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rake
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
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: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: bundler
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
+ - !ruby/object:Gem::Dependency
47
+ name: rspec
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: yard
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ description: Thread-safe LRU implementation with (optional) TTL and constant time
79
+ operations
80
+ email: info@blacksquaremedia.com
81
+ executables: []
82
+ extensions: []
83
+ extra_rdoc_files: []
84
+ files:
85
+ - .gitignore
86
+ - Gemfile
87
+ - Gemfile.lock
88
+ - README.md
89
+ - Rakefile
90
+ - lib/timed_lru.rb
91
+ - spec/spec_helper.rb
92
+ - spec/timed_lru_spec.rb
93
+ - timed_lru.gemspec
94
+ homepage: https://github.com/bsm/timed_lru
95
+ licenses: []
96
+ post_install_message:
97
+ rdoc_options: []
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ none: false
102
+ requirements:
103
+ - - ! '>='
104
+ - !ruby/object:Gem::Version
105
+ version: 1.9.1
106
+ required_rubygems_version: !ruby/object:Gem::Requirement
107
+ none: false
108
+ requirements:
109
+ - - ! '>='
110
+ - !ruby/object:Gem::Version
111
+ version: 1.8.0
112
+ requirements: []
113
+ rubyforge_project:
114
+ rubygems_version: 1.8.24
115
+ signing_key:
116
+ specification_version: 3
117
+ summary: Timed LRU
118
+ test_files: []
119
+ has_rdoc: