timed_lru 0.3.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.
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: