lrucache 0.1.1 → 0.1.2

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/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