lrucache 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -14,8 +14,8 @@ Example
14
14
  puts cache[2] # b
15
15
  puts cache[1] # a
16
16
  puts cache[3] # c
17
- puts cache[:does_not_exist] # 42, has no effect on LRU.
18
- cache[4] = 'd' # Out of space! Throws out the least-recently used (2 => 'b').
17
+ puts cache[:does_not_exist] # 42, has no effect on LRU
18
+ cache[4] = 'd' # Out of space! Throws out the least-recently used (2 => 'b')
19
19
  puts cache.keys # [1,3,4]
20
20
 
21
21
 
@@ -37,4 +37,29 @@ TTL (time-to-live)
37
37
 
38
38
  # Three days later ...
39
39
  cache.fetch("banana") # nil
40
- cache.fetch("monkey") # nil
40
+ cache.fetch("monkey") # nil
41
+
42
+ SOFT_TTL
43
+ ========
44
+ Allows you to have two TTLs when calling fetch with a block.
45
+ After the soft-ttl expires, the block is called to refresh the value.
46
+ If the block completes normally, the value is replaced and expirations reset.
47
+ If the block raises a fatal (non-RuntimeError) exception, it bubbles up. But,
48
+ if the block raises a RuntimeError, the cached value is kept and used a little
49
+ longer, and the soft-ttl is postponed retry_delay into the future. If the block
50
+ is not called successfully before the normal TTL expires, then the cached value
51
+ expires and the block is called for a new value, but exceptions are not handled.
52
+
53
+ cache = LRUCache.new(:ttl => 1.hour,
54
+ :soft_ttl => 30.minutes,
55
+ :retry_delay => 1.minute)
56
+ cache.fetch("banana") { "yellow" } # "yellow"
57
+ cache.fetch("banana") { "george" } # "yellow"
58
+
59
+ # 30 minutes later ...
60
+ cache.fetch("banana") { raise "ruckus" } # "yellow"
61
+ cache.fetch("banana") { "george" } # "yellow"
62
+
63
+ # 1 more minute later ...
64
+ cache.fetch("banana") { "george" } # "george"
65
+ cache.fetch("banana") { "barney" } # "george"
data/lib/lrucache.rb CHANGED
@@ -4,14 +4,19 @@ require "priority_queue"
4
4
  # Not thread-safe!
5
5
  class LRUCache
6
6
 
7
- attr_reader :default, :max_size, :ttl
7
+ attr_reader :default, :max_size, :ttl, :soft_ttl, :retry_delay
8
8
 
9
9
  def initialize(opts={})
10
10
  @max_size = Integer(opts[:max_size] || 100)
11
11
  @default = opts[:default]
12
12
  @ttl = Float(opts[:ttl] || 0)
13
+ @soft_ttl = Float(opts[:soft_ttl] || 0)
14
+ @retry_delay = Float(opts[:retry_delay] || 0)
13
15
  raise "max_size must not be negative" if @max_size < 0
14
- raise "ttl must be positive or zero" unless @ttl >= 0
16
+ raise "ttl must not be negative" if @ttl < 0
17
+ raise "soft_ttl must not be negative" if @soft_ttl < 0
18
+ raise "retry_delay must not be negative" if @retry_delay < 0
19
+
15
20
  @pqueue = PriorityQueue.new
16
21
  @data = {}
17
22
  @counter = 0
@@ -26,49 +31,59 @@ class LRUCache
26
31
  def include?(key)
27
32
  datum = @data[key]
28
33
  return false if datum.nil?
29
- value, expires = datum
30
- if expires.nil? || expires > Time.now # no expiration, or not expired
31
- access(key)
32
- true
33
- else # expired
34
+ if datum.expired?
34
35
  delete(key)
35
36
  false
37
+ else
38
+ access(key)
39
+ true
36
40
  end
37
41
  end
38
42
 
39
- def store(key, value, ttl=nil)
43
+ def store(key, value, args={})
40
44
  evict_lru! unless @data.include?(key) || @data.size < max_size
41
- ttl ||= @ttl
42
- expires =
43
- if ttl.is_a?(Time)
44
- ttl
45
- else
46
- ttl = Float(ttl)
47
- (ttl > 0) ? (Time.now + ttl) : nil
48
- end
49
- @data[key] = [value, expires]
45
+ ttl, soft_ttl, retry_delay = extract_arguments(args)
46
+ expiration = expiration_date(ttl)
47
+ soft_expiration = expiration_date(soft_ttl)
48
+ @data[key] = Datum.new(value, expiration, soft_expiration)
50
49
  access(key)
50
+ value
51
51
  end
52
52
 
53
53
  alias :[]= :store
54
54
 
55
- def fetch(key, ttl=nil)
55
+ def fetch(key, args={})
56
56
  datum = @data[key]
57
- unless datum.nil?
58
- value, expires = datum
59
- if expires.nil? || expires > Time.now # no expiration, or not expired
57
+ if datum.nil?
58
+ if block_given?
59
+ store(key, value = yield, args)
60
+ else
61
+ @default
62
+ end
63
+ elsif datum.expired?
64
+ delete(key)
65
+ if block_given?
66
+ store(key, value = yield, args)
67
+ else
68
+ @default
69
+ end
70
+ elsif datum.soft_expired?
71
+ if block_given?
72
+ begin
73
+ store(key, value = yield, args)
74
+ rescue RuntimeError => e
75
+ access(key)
76
+ ttl, soft_ttl, retry_delay = extract_arguments(args)
77
+ datum.soft_expiration = (Time.now + retry_delay) if retry_delay > 0
78
+ datum.value
79
+ end
80
+ else
60
81
  access(key)
61
- return value
62
- else # expired
63
- delete(key)
82
+ datum.value
64
83
  end
65
- end
66
- if block_given?
67
- value = yield
68
- store(key, value, ttl)
69
- value
70
84
  else
71
- @default
85
+ access(key)
86
+ datum.value
72
87
  end
73
88
  end
74
89
 
@@ -93,6 +108,46 @@ class LRUCache
93
108
 
94
109
  private
95
110
 
111
+ class Datum
112
+ attr_reader :value, :expiration, :soft_expiration
113
+ attr_writer :soft_expiration
114
+ def initialize(value, expiration, soft_expiration)
115
+ @value = value
116
+ @expiration = expiration
117
+ @soft_expiration = soft_expiration
118
+ end
119
+
120
+ def expired?
121
+ !@expiration.nil? && @expiration <= Time.now
122
+ end
123
+
124
+ def soft_expired?
125
+ !@soft_expiration.nil? && @soft_expiration <= Time.now
126
+ end
127
+ end
128
+
129
+ def expiration_date(ttl)
130
+ if ttl.is_a?(Time)
131
+ ttl
132
+ else
133
+ ttl = Float(ttl)
134
+ (ttl > 0) ? (Time.now + ttl) : nil
135
+ end
136
+ end
137
+
138
+ def extract_arguments(args)
139
+ if args.is_a?(Hash)
140
+ ttl = args[:ttl] || @ttl
141
+ soft_ttl = args[:soft_ttl] || @soft_ttl
142
+ retry_delay = args[:retry_delay] || @retry_delay
143
+ [ttl, soft_ttl, retry_delay]
144
+ else
145
+ # legacy arg
146
+ ttl = args || @ttl
147
+ [ttl, @soft_ttl, @retry_delay]
148
+ end
149
+ end
150
+
96
151
  def evict_lru!
97
152
  key, priority = @pqueue.delete_min
98
153
  @data.delete(key) unless priority.nil?
@@ -1,3 +1,3 @@
1
1
  class LRUCache
2
- VERSION = "0.1.1"
2
+ VERSION = "0.1.2"
3
3
  end
@@ -197,7 +197,8 @@ describe LRUCache do
197
197
  c = LRUCache.new(:ttl => 0)
198
198
  c.store(:a,'a')
199
199
  stored = c.instance_variable_get(:@data)[:a]
200
- stored.should == ['a', nil]
200
+ stored.value.should == 'a'
201
+ stored.expiration.should == nil
201
202
  end
202
203
  end
203
204
  context "when ttl is not given and the cache's default ttl is greater than zero" do
@@ -206,7 +207,8 @@ describe LRUCache do
206
207
  now = Time.now
207
208
  Timecop.freeze(now) { c.store(:a,'a') }
208
209
  stored = c.instance_variable_get(:@data)[:a]
209
- stored.last.should == now + 1
210
+ stored.value.should == 'a'
211
+ stored.expiration.should == now + 1
210
212
  end
211
213
  end
212
214
  context "when ttl is a Time" do
@@ -215,7 +217,8 @@ describe LRUCache do
215
217
  ttl = Time.now + 246
216
218
  c.store(:a, 'a', ttl)
217
219
  stored = c.instance_variable_get(:@data)[:a]
218
- stored.last.should == ttl
220
+ stored.value.should == 'a'
221
+ stored.expiration.should == ttl
219
222
  end
220
223
  end
221
224
  context "when ttl can be parsed as a float" do
@@ -224,7 +227,8 @@ describe LRUCache do
224
227
  now = Time.now
225
228
  Timecop.freeze(now) { c.store(:a, 'a', "98.6") }
226
229
  stored = c.instance_variable_get(:@data)[:a]
227
- stored.last.should == now + 98.6
230
+ stored.value.should == 'a'
231
+ stored.expiration.should == now + 98.6
228
232
  end
229
233
  end
230
234
  context "when ttl cannot be parsed as a float" do
@@ -302,27 +306,27 @@ describe LRUCache do
302
306
  end
303
307
  end
304
308
  context "when a block is given" do
305
- context "when the key does not exist" do
309
+ context "and the key does not exist" do
306
310
  it "should call the block and store and return the result" do
307
311
  c = LRUCache.new
308
312
  ttl = double(:ttl)
309
313
  result = double(:result)
310
- c.should_receive(:store).with(:a, result, ttl)
314
+ c.should_receive(:store).with(:a, result, ttl).and_return(result)
311
315
  c.fetch(:a, ttl){ result }.should == result
312
316
  end
313
317
  end
314
- context "when the key has been evicted" do
318
+ context "and the key has been evicted" do
315
319
  it "should call the block and store and return the result" do
316
320
  c = LRUCache.new
317
321
  c[:a] = 'a'
318
322
  c.send(:evict_lru!)
319
323
  ttl = double(:ttl)
320
324
  result = double(:result)
321
- c.should_receive(:store).with(:a, result, ttl)
325
+ c.should_receive(:store).with(:a, result, ttl).and_return(result)
322
326
  c.fetch(:a, ttl){ result }.should == result
323
327
  end
324
328
  end
325
- context "when the key has expired" do
329
+ context "and the key has expired" do
326
330
  it "should call the block and store and return the result" do
327
331
  c = LRUCache.new
328
332
  now = Time.now
@@ -330,12 +334,49 @@ describe LRUCache do
330
334
  Timecop.freeze(now + 20) do
331
335
  ttl = double(:ttl)
332
336
  result = double(:result)
333
- c.should_receive(:store).with(:a, result, ttl)
337
+ c.should_receive(:store).with(:a, result, ttl).and_return(result)
334
338
  c.fetch(:a, ttl){ result }.should == result
335
339
  end
336
340
  end
337
341
  end
338
- context "when the key is present and un-expired" do
342
+ context 'and the key has "soft"-expired' do
343
+ before(:each) do
344
+ @c = LRUCache.new
345
+ @c.store(:a, 'a', :ttl => 10_000, :soft_ttl => Time.now - 60, :retry_delay => 10)
346
+ @args = {:ttl => 10_000, :soft_ttl => 60, :retry_delay => 10}
347
+ end
348
+ context "and the block raises a runtime exception" do
349
+ it "should continue to return the old value" do
350
+ @c.should_not_receive(:store)
351
+ @c.fetch(:a, @args) { raise "no!" }.should == 'a'
352
+ end
353
+ it "should extend the soft-expiration by retry_delay" do
354
+ Timecop.freeze(Time.now) do
355
+ data = @c.instance_variable_get(:@data)
356
+ original_soft_expiration = data[:a].soft_expiration
357
+ @c.should_not_receive(:store)
358
+ @c.fetch(:a, @args) { raise "no!" }
359
+ data = @c.instance_variable_get(:@data)
360
+ data[:a].soft_expiration.should == Time.now + @args[:retry_delay]
361
+ end
362
+ end
363
+ end
364
+ context "and the block raises a fatal exception" do
365
+ it "should allow the exception through" do
366
+ expect {
367
+ @c.fetch(:a, @args) { raise(NoMemoryError,"panic!") }
368
+ }.to raise_exception(NoMemoryError)
369
+ end
370
+ end
371
+ context "and the block does not raise an exception" do
372
+ it "should call the block and store and return the result" do
373
+ result = double(:result)
374
+ @c.should_receive(:store).with(:a, result, @args).and_return(result)
375
+ @c.fetch(:a, @args) { result }.should == result
376
+ end
377
+ end
378
+ end
379
+ context "and the key is present and un-expired" do
339
380
  it "should return the cached value without calling the block" do
340
381
  c = LRUCache.new(:ttl => nil)
341
382
  c[:a] = 'a'
metadata CHANGED
@@ -1,160 +1,111 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: lrucache
3
- version: !ruby/object:Gem::Version
4
- hash: 25
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
5
  prerelease:
6
- segments:
7
- - 0
8
- - 1
9
- - 1
10
- version: 0.1.1
11
6
  platform: ruby
12
- authors:
7
+ authors:
13
8
  - Chris Johnson
14
9
  autorequire:
15
10
  bindir: bin
16
11
  cert_chain: []
17
-
18
- date: 2012-01-03 00:00:00 Z
19
- dependencies:
20
- - !ruby/object:Gem::Dependency
21
- version_requirements: &id001 !ruby/object:Gem::Requirement
12
+ date: 2012-03-31 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: PriorityQueue
16
+ requirement: &70184418403860 !ruby/object:Gem::Requirement
22
17
  none: false
23
- requirements:
18
+ requirements:
24
19
  - - ~>
25
- - !ruby/object:Gem::Version
26
- hash: 31
27
- segments:
28
- - 0
29
- - 1
30
- - 2
20
+ - !ruby/object:Gem::Version
31
21
  version: 0.1.2
32
- requirement: *id001
33
22
  type: :runtime
34
23
  prerelease: false
35
- name: PriorityQueue
36
- - !ruby/object:Gem::Dependency
37
- version_requirements: &id002 !ruby/object:Gem::Requirement
24
+ version_requirements: *70184418403860
25
+ - !ruby/object:Gem::Dependency
26
+ name: rspec
27
+ requirement: &70184418403360 !ruby/object:Gem::Requirement
38
28
  none: false
39
- requirements:
29
+ requirements:
40
30
  - - ~>
41
- - !ruby/object:Gem::Version
42
- hash: 23
43
- segments:
44
- - 2
45
- - 6
46
- - 0
31
+ - !ruby/object:Gem::Version
47
32
  version: 2.6.0
48
- requirement: *id002
49
33
  type: :development
50
34
  prerelease: false
51
- name: rspec
52
- - !ruby/object:Gem::Dependency
53
- version_requirements: &id003 !ruby/object:Gem::Requirement
35
+ version_requirements: *70184418403360
36
+ - !ruby/object:Gem::Dependency
37
+ name: simplecov
38
+ requirement: &70184418402900 !ruby/object:Gem::Requirement
54
39
  none: false
55
- requirements:
40
+ requirements:
56
41
  - - ~>
57
- - !ruby/object:Gem::Version
58
- hash: 11
59
- segments:
60
- - 0
61
- - 4
62
- - 2
42
+ - !ruby/object:Gem::Version
63
43
  version: 0.4.2
64
- requirement: *id003
65
44
  type: :development
66
45
  prerelease: false
67
- name: simplecov
68
- - !ruby/object:Gem::Dependency
69
- version_requirements: &id004 !ruby/object:Gem::Requirement
46
+ version_requirements: *70184418402900
47
+ - !ruby/object:Gem::Dependency
48
+ name: rb-fsevent
49
+ requirement: &70184418402400 !ruby/object:Gem::Requirement
70
50
  none: false
71
- requirements:
51
+ requirements:
72
52
  - - ~>
73
- - !ruby/object:Gem::Version
74
- hash: 9
75
- segments:
76
- - 0
77
- - 4
78
- - 3
53
+ - !ruby/object:Gem::Version
79
54
  version: 0.4.3
80
- requirement: *id004
81
55
  type: :development
82
56
  prerelease: false
83
- name: rb-fsevent
84
- - !ruby/object:Gem::Dependency
85
- version_requirements: &id005 !ruby/object:Gem::Requirement
57
+ version_requirements: *70184418402400
58
+ - !ruby/object:Gem::Dependency
59
+ name: guard
60
+ requirement: &70184418401940 !ruby/object:Gem::Requirement
86
61
  none: false
87
- requirements:
62
+ requirements:
88
63
  - - ~>
89
- - !ruby/object:Gem::Version
90
- hash: 3
91
- segments:
92
- - 0
93
- - 6
94
- - 2
64
+ - !ruby/object:Gem::Version
95
65
  version: 0.6.2
96
- requirement: *id005
97
66
  type: :development
98
67
  prerelease: false
99
- name: guard
100
- - !ruby/object:Gem::Dependency
101
- version_requirements: &id006 !ruby/object:Gem::Requirement
68
+ version_requirements: *70184418401940
69
+ - !ruby/object:Gem::Dependency
70
+ name: guard-bundler
71
+ requirement: &70184418401480 !ruby/object:Gem::Requirement
102
72
  none: false
103
- requirements:
73
+ requirements:
104
74
  - - ~>
105
- - !ruby/object:Gem::Version
106
- hash: 29
107
- segments:
108
- - 0
109
- - 1
110
- - 3
75
+ - !ruby/object:Gem::Version
111
76
  version: 0.1.3
112
- requirement: *id006
113
77
  type: :development
114
78
  prerelease: false
115
- name: guard-bundler
116
- - !ruby/object:Gem::Dependency
117
- version_requirements: &id007 !ruby/object:Gem::Requirement
79
+ version_requirements: *70184418401480
80
+ - !ruby/object:Gem::Dependency
81
+ name: guard-rspec
82
+ requirement: &70184418401020 !ruby/object:Gem::Requirement
118
83
  none: false
119
- requirements:
84
+ requirements:
120
85
  - - ~>
121
- - !ruby/object:Gem::Version
122
- hash: 11
123
- segments:
124
- - 0
125
- - 4
126
- - 2
86
+ - !ruby/object:Gem::Version
127
87
  version: 0.4.2
128
- requirement: *id007
129
88
  type: :development
130
89
  prerelease: false
131
- name: guard-rspec
132
- - !ruby/object:Gem::Dependency
133
- version_requirements: &id008 !ruby/object:Gem::Requirement
90
+ version_requirements: *70184418401020
91
+ - !ruby/object:Gem::Dependency
92
+ name: timecop
93
+ requirement: &70184418400560 !ruby/object:Gem::Requirement
134
94
  none: false
135
- requirements:
95
+ requirements:
136
96
  - - ~>
137
- - !ruby/object:Gem::Version
138
- hash: 25
139
- segments:
140
- - 0
141
- - 3
142
- - 5
97
+ - !ruby/object:Gem::Version
143
98
  version: 0.3.5
144
- requirement: *id008
145
99
  type: :development
146
100
  prerelease: false
147
- name: timecop
101
+ version_requirements: *70184418400560
148
102
  description: A simple LRU-cache based on a hash and priority queue
149
- email:
103
+ email:
150
104
  - chris@kindkid.com
151
105
  executables: []
152
-
153
106
  extensions: []
154
-
155
107
  extra_rdoc_files: []
156
-
157
- files:
108
+ files:
158
109
  - .gitignore
159
110
  - .rvmrc
160
111
  - Gemfile
@@ -166,39 +117,30 @@ files:
166
117
  - lrucache.gemspec
167
118
  - spec/lrucache_spec.rb
168
119
  - spec/spec_helper.rb
169
- homepage: ""
120
+ homepage: ''
170
121
  licenses: []
171
-
172
122
  post_install_message:
173
123
  rdoc_options: []
174
-
175
- require_paths:
124
+ require_paths:
176
125
  - lib
177
- required_ruby_version: !ruby/object:Gem::Requirement
126
+ required_ruby_version: !ruby/object:Gem::Requirement
178
127
  none: false
179
- requirements:
180
- - - ">="
181
- - !ruby/object:Gem::Version
182
- hash: 3
183
- segments:
184
- - 0
185
- version: "0"
186
- required_rubygems_version: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ! '>='
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ required_rubygems_version: !ruby/object:Gem::Requirement
187
133
  none: false
188
- requirements:
189
- - - ">="
190
- - !ruby/object:Gem::Version
191
- hash: 3
192
- segments:
193
- - 0
194
- version: "0"
134
+ requirements:
135
+ - - ! '>='
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
195
138
  requirements: []
196
-
197
139
  rubyforge_project: lrucache
198
140
  rubygems_version: 1.8.10
199
141
  signing_key:
200
142
  specification_version: 3
201
143
  summary: A simple LRU-cache based on a hash and priority queue
202
- test_files:
144
+ test_files:
203
145
  - spec/lrucache_spec.rb
204
146
  - spec/spec_helper.rb