timeout_cache 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  A `TimeoutCache` is a simple cache where objects are stored with a fixed expiration time. It is useful when you want to keep an object around for a sufficiently long period of time, but want them to disappear after, say, 5 seconds, or on the first day of next month.
4
4
 
5
+ ```
6
+ gem install timeout_cache
7
+ ```
8
+
5
9
  # How do I use this thing?
6
10
 
7
11
  A `TimeoutCache` is easy to use.
@@ -31,6 +35,8 @@ You can also use an instance of `Time`:
31
35
  cache.set(:x, :y, :time => Time.now + 50)
32
36
  ```
33
37
 
38
+ You can use any object as the value for `:time` provided it is an `Integer`, a `Time`, has a `to_int` method, or has a `to_time` method.
39
+
34
40
  If you want to change the default expiration time, do so when you make a new instance of `TimeoutCache` by passing the length of time in seconds.
35
41
 
36
42
  ```ruby
@@ -41,7 +47,11 @@ Unless an object is added to the cache using the `:time` option, the default is
41
47
 
42
48
  # Entry deletion and pruning
43
49
 
44
- Expired entries are deleted lazily. If an entry is added with an expire time of 15 seconds and nothing touches the cache, the entry will still be in the cache after 20 seconds. Expired entries are deleted only when a call to `#[]` (or, equivalently, `#get`) finds an expired entry. At that point, `#prune` is called.
50
+ Expired entries are deleted lazily to try and avoid to cost of excessively pruning. If an entry is added with an expire time of 15 seconds and nothing touches the cache, the entry will still be in the cache after 20 seconds. Expired entries are deleted when certain methods are called:
51
+
52
+ * `#[]` (or, equivalently, `#get`) provided that the entry being retrieved has expired
53
+ * `#size`
54
+ * `#empty?`
45
55
 
46
56
  If you want to manually prune the cache, you may do so by calling `#prune`.
47
57
 
@@ -13,7 +13,7 @@
13
13
  # cache[:foo] #=> nil
14
14
  #
15
15
  class TimeoutCache
16
- VERSION = "0.0.1"
16
+ VERSION = "0.0.2"
17
17
 
18
18
  # Wraps an object by attaching a time to it.
19
19
  class TimedObject # :nodoc:
@@ -38,12 +38,15 @@ class TimeoutCache
38
38
 
39
39
  # Creates a new TimeoutCache.
40
40
  #
41
- # If <tt>timeout</tt> is specified, the default survival time for
42
- # a cache entry is <tt>timeout</tt> in seconds.
41
+ # If <tt>timeout</tt> is specified, #to_int is called on the argument,
42
+ # and the return value is used as the default survival time for
43
+ # a cache entry in seconds.
43
44
  #
44
45
  # If no default value is used, then DEFAULT_TIMEOUT is the default
45
46
  # time-to-expire in seconds.
46
47
  def initialize(timeout = DEFAULT_TIMEOUT)
48
+ timeout = timeout.to_int if timeout.respond_to?(:to_int)
49
+
47
50
  raise ArgumentError.new("Timeout must be > 0") unless timeout > 0
48
51
 
49
52
  @timeout = timeout
@@ -52,8 +55,9 @@ class TimeoutCache
52
55
  @store = {}
53
56
  end
54
57
 
55
- # Returns the number of items in the cache.
58
+ # Calls #prune and then returns the number of items in the cache.
56
59
  def size
60
+ prune
57
61
  @store.size
58
62
  end
59
63
  alias_method :length, :size
@@ -62,8 +66,7 @@ class TimeoutCache
62
66
  # then this returns nil. If the value has an expire time earlier than or equal
63
67
  # to the current time, this returns nil.
64
68
  #
65
- # As an implementation detail, this method calls #prune whenever it finds
66
- # an element that has expired.
69
+ # This method calls #prune whenever it finds an element that has expired.
67
70
  def [](key)
68
71
  val = @store[key]
69
72
 
@@ -103,13 +106,27 @@ class TimeoutCache
103
106
  def set(key, value, options = {})
104
107
  time = options[:time] || timeout
105
108
 
109
+ # we can technically do away with checking against Integer explicitly since
110
+ # the to_int call will take care of it for us through Integer#to_int returning
111
+ # self, but it's here for the sake of clarity, mostly.
112
+ #
113
+ # on the other hand, the #to_time call is necessary, since, for some reason,
114
+ # Time#to_time only exists if you require "time".
106
115
  time = case time
107
116
  when Integer
108
117
  Time.now + time
109
118
  when Time
110
119
  time
120
+ else
121
+ if time.respond_to?(:to_int)
122
+ Time.now + time.to_int
123
+ elsif time.respond_to?(:to_time)
124
+ time.to_time
125
+ end
111
126
  end
112
127
 
128
+ raise(ArgumentError, "time argument #{time.inspect} could not be converted to a time") if time.nil?
129
+
113
130
  return nil if time <= Time.now
114
131
 
115
132
  v = TimedObject.new(value, time)
@@ -118,8 +135,9 @@ class TimeoutCache
118
135
  value
119
136
  end
120
137
 
121
- # Returns true if the cache is empty, otherwise false.
138
+ # Calls #prune and then returns true if the cache is empty, otherwise false.
122
139
  def empty?
140
+ prune
123
141
  @store.empty?
124
142
  end
125
143
 
@@ -136,7 +154,7 @@ class TimeoutCache
136
154
  # If nothing was removed, returns nil, otherwise returns
137
155
  # the number of elements removed.
138
156
  def prune
139
- return nil if empty?
157
+ return nil if @store.empty?
140
158
 
141
159
  count = 0
142
160
 
@@ -16,7 +16,7 @@ class TimeoutCache
16
16
  VERSION = "0.0.1"
17
17
 
18
18
  # Wraps an object by attaching a time to it.
19
- class TimedObject #:nodoc:
19
+ class TimedObject # :nodoc:
20
20
  attr_reader :value, :expires_at
21
21
 
22
22
  def initialize(value, time)
@@ -38,12 +38,15 @@ class TimeoutCache
38
38
 
39
39
  # Creates a new TimeoutCache.
40
40
  #
41
- # If <tt>timeout</tt> is specified, the default survival time for
42
- # a cache entry is <tt>timeout</tt> in seconds.
41
+ # If <tt>timeout</tt> is specified, #to_int is called on the argument,
42
+ # and the return value is used as the default survival time for
43
+ # a cache entry in seconds.
43
44
  #
44
45
  # If no default value is used, then DEFAULT_TIMEOUT is the default
45
46
  # time-to-expire in seconds.
46
47
  def initialize(timeout = DEFAULT_TIMEOUT)
48
+ timeout = timeout.to_int if timeout.respond_to?(:to_int)
49
+
47
50
  raise ArgumentError.new("Timeout must be > 0") unless timeout > 0
48
51
 
49
52
  @timeout = timeout
@@ -52,8 +55,9 @@ class TimeoutCache
52
55
  @store = {}
53
56
  end
54
57
 
55
- # Returns the number of items in the cache.
58
+ # Calls #prune and then returns the number of items in the cache.
56
59
  def size
60
+ prune
57
61
  @store.size
58
62
  end
59
63
  alias_method :length, :size
@@ -62,8 +66,7 @@ class TimeoutCache
62
66
  # then this returns nil. If the value has an expire time earlier than or equal
63
67
  # to the current time, this returns nil.
64
68
  #
65
- # As an implementation detail, this method calls #prune whenever it finds
66
- # an element that has expired.
69
+ # This method calls #prune whenever it finds an element that has expired.
67
70
  def [](key)
68
71
  val = @store[key]
69
72
 
@@ -103,13 +106,27 @@ class TimeoutCache
103
106
  def set(key, value, options = {})
104
107
  time = options[:time] || timeout
105
108
 
109
+ # we can technically do away with checking against Integer explicitly since
110
+ # the to_int call will take care of it for us through Integer#to_int returning
111
+ # self, but it's here for the sake of clarity, mostly.
112
+ #
113
+ # on the other hand, the #to_time call is necessary, since, for some reason,
114
+ # Time#to_time only exists if you require "time".
106
115
  time = case time
107
116
  when Integer
108
117
  Time.now + time
109
118
  when Time
110
119
  time
120
+ else
121
+ if time.respond_to?(:to_int)
122
+ Time.now + time.to_int
123
+ elsif time.respond_to?(:to_time)
124
+ time.to_time
125
+ end
111
126
  end
112
127
 
128
+ raise(ArgumentError, "time argument #{time.inspect} could not be converted to a time") if time.nil?
129
+
113
130
  return nil if time <= Time.now
114
131
 
115
132
  v = TimedObject.new(value, time)
@@ -118,8 +135,9 @@ class TimeoutCache
118
135
  value
119
136
  end
120
137
 
121
- # Returns true if the cache is empty, otherwise false.
138
+ # Calls #prune and then returns true if the cache is empty, otherwise false.
122
139
  def empty?
140
+ prune
123
141
  @store.empty?
124
142
  end
125
143
 
@@ -136,7 +154,7 @@ class TimeoutCache
136
154
  # If nothing was removed, returns nil, otherwise returns
137
155
  # the number of elements removed.
138
156
  def prune
139
- return nil if empty?
157
+ return nil if @store.empty?
140
158
 
141
159
  count = 0
142
160
 
data/rakefile CHANGED
@@ -6,7 +6,7 @@ $:.unshift(File.expand_path("lib", __FILE__))
6
6
  require "timeout_cache"
7
7
 
8
8
  RSpec::Core::RakeTask.new(:test) do |t|
9
- t.rspec_opts = "-I test --color --format nested"
9
+ t.rspec_opts = "-I test --color"
10
10
  t.pattern = "test/**/*_spec.rb"
11
11
  t.verbose = false
12
12
  t.fail_on_error = true
@@ -23,4 +23,13 @@ Rake::RDocTask.new(:docs) do |rd|
23
23
  rd.rdoc_files.include("README.md", "lib")
24
24
  end
25
25
 
26
+ namespace :gem do
27
+ desc "Build the gem from the .gemspec"
28
+ task :build do
29
+ sh("gem build timeout_cache.gemspec")
30
+ puts
31
+ puts "You can push with gem push <built-gem>.gem"
32
+ end
33
+ end
34
+
26
35
  task :default => :test
@@ -1,7 +1,18 @@
1
1
  require "timeout_cache"
2
2
  require "rspec"
3
+ require "timecop"
3
4
 
4
5
  describe TimeoutCache do
6
+ def sleep(n)
7
+ Timecop.travel(Time.now + n)
8
+ end
9
+
10
+ after :all do
11
+ # If we don't return, the runtime for the tests makes it seem like
12
+ # we had actually slept!
13
+ Timecop.return
14
+ end
15
+
5
16
  it "uses the global default timeout with no time specified" do
6
17
  subject.timeout.should == TimeoutCache::DEFAULT_TIMEOUT
7
18
  end
@@ -10,6 +21,17 @@ describe TimeoutCache do
10
21
  TimeoutCache.new(50).timeout.should == 50
11
22
  end
12
23
 
24
+ it "can be instantiated with anything that responds to to_int" do
25
+ x = double("")
26
+ x.should_receive(:to_int).and_return(2)
27
+
28
+ c = TimeoutCache.new(x)
29
+ c[:a] = :b
30
+ c[:a].should == :b
31
+ sleep 3
32
+ c[:a].should be_nil
33
+ end
34
+
13
35
  it "cannot be instantiated with a global timeout <= 0" do
14
36
  [0, -1, -5].each do |i|
15
37
  expect { TimeoutCache.new(i) }.to raise_error(ArgumentError)
@@ -30,6 +52,32 @@ describe TimeoutCache do
30
52
  subject.expire_time(:a).should == t
31
53
  end
32
54
 
55
+ context "if the :time value is neither a Time nor an Integer" do
56
+ it "accepts any object which responds to #to_time" do
57
+ s = double("")
58
+ s.should_receive(:to_time).and_return(Time.now + 2)
59
+
60
+ subject.set(:a, 5, :time => s)
61
+ subject[:a].should_not be_nil
62
+ sleep 3
63
+ subject[:a].should be_nil
64
+ end
65
+
66
+ it "accepts any object which responds to #to_int" do
67
+ s = double("")
68
+ s.should_receive(:to_int).any_number_of_times.and_return(2)
69
+
70
+ subject.set(:a, 5, :time => s)
71
+ subject[:a].should_not be_nil
72
+ sleep(s.to_int + 1)
73
+ subject[:a].should be_nil
74
+ end
75
+
76
+ it "throws ArgumentError if it can't get a time out of it" do
77
+ expect { subject.set(:a, :b, :time => "not a time") }.to raise_error(ArgumentError)
78
+ end
79
+ end
80
+
33
81
  # here be dragons. if it takes too long to execute these
34
82
  # commands, the times won't match, so this test isn't deterministic
35
83
  it "sets a default timeout time with no time value specified" do
@@ -116,6 +164,12 @@ describe TimeoutCache do
116
164
  subject[:a] = 1
117
165
  subject.size.should_not == 0
118
166
  end
167
+
168
+ it "ignores expired entries" do
169
+ subject.set(:a, :b, :time => 1)
170
+ sleep 2
171
+ subject.size.should == 0
172
+ end
119
173
  end
120
174
 
121
175
  describe "pruning" do
@@ -123,18 +177,26 @@ describe TimeoutCache do
123
177
  subject.set(:a, 1, :time => 2)
124
178
  subject.set(:b, 1, :time => 2)
125
179
  sleep 3
126
- subject.size.should == 2
180
+ subject.should_receive(:prune)
127
181
  subject.get(:a)
128
- subject.size.should == 0
129
182
  end
130
183
 
131
184
  it "does not happen when calling #get(key) when get(key) is not expired" do
132
185
  subject.set(:a, 1, :time => 200)
133
186
  subject.set(:b, 1, :time => 2)
134
187
  sleep 3
135
- subject.size.should == 2
188
+ subject.should_not_receive(:prune)
136
189
  subject.get(:a)
137
- subject.size.should == 2
190
+ end
191
+
192
+ it "happens on #size call" do
193
+ subject.should_receive(:prune)
194
+ subject.size
195
+ end
196
+
197
+ it "happens on an #empty? call" do
198
+ subject.should_receive(:prune)
199
+ subject.empty?
138
200
  end
139
201
  end
140
202
 
@@ -1,7 +1,16 @@
1
1
  require "timeout_cache"
2
2
  require "rspec"
3
+ require "timecop"
3
4
 
4
5
  describe TimeoutCache do
6
+ def sleep(n)
7
+ Timecop.travel(Time.now + n)
8
+ end
9
+
10
+ after :all do
11
+ Timecop.return
12
+ end
13
+
5
14
  it "uses the global default timeout with no time specified" do
6
15
  subject.timeout.should == TimeoutCache::DEFAULT_TIMEOUT
7
16
  end
@@ -10,6 +19,17 @@ describe TimeoutCache do
10
19
  TimeoutCache.new(50).timeout.should == 50
11
20
  end
12
21
 
22
+ it "can be instantiated with anything that responds to to_int" do
23
+ x = double("")
24
+ x.should_receive(:to_int).and_return(2)
25
+
26
+ c = TimeoutCache.new(x)
27
+ c[:a] = :b
28
+ c[:a].should == :b
29
+ sleep 3
30
+ c[:a].should be_nil
31
+ end
32
+
13
33
  it "cannot be instantiated with a global timeout <= 0" do
14
34
  [0, -1, -5].each do |i|
15
35
  expect { TimeoutCache.new(i) }.to raise_error(ArgumentError)
@@ -30,6 +50,32 @@ describe TimeoutCache do
30
50
  subject.expire_time(:a).should == t
31
51
  end
32
52
 
53
+ context "if the :time value is neither a Time nor an Integer" do
54
+ it "accepts any object which responds to #to_time" do
55
+ s = double("")
56
+ s.should_receive(:to_time).and_return(Time.now + 2)
57
+
58
+ subject.set(:a, 5, :time => s)
59
+ subject[:a].should_not be_nil
60
+ sleep 3
61
+ subject[:a].should be_nil
62
+ end
63
+
64
+ it "accepts any object which responds to #to_int" do
65
+ s = double("")
66
+ s.should_receive(:to_int).any_number_of_times.and_return(2)
67
+
68
+ subject.set(:a, 5, :time => s)
69
+ subject[:a].should_not be_nil
70
+ sleep(s.to_int + 1)
71
+ subject[:a].should be_nil
72
+ end
73
+
74
+ it "throws ArgumentError if it can't get a time out of it" do
75
+ expect { subject.set(:a, :b, :time => "not a time") }.to raise_error(ArgumentError)
76
+ end
77
+ end
78
+
33
79
  # here be dragons. if it takes too long to execute these
34
80
  # commands, the times won't match, so this test isn't deterministic
35
81
  it "sets a default timeout time with no time value specified" do
@@ -86,6 +132,10 @@ describe TimeoutCache do
86
132
 
87
133
  it "returns nil if nothing was deleted" do
88
134
  subject[:no_such_key].should be_nil
135
+
136
+ subject[0] = 1
137
+ subject.delete(0)
138
+ subject.delete(0).should be_nil
89
139
  end
90
140
 
91
141
  it "returns the value deleted if the key is deleted" do
@@ -112,6 +162,12 @@ describe TimeoutCache do
112
162
  subject[:a] = 1
113
163
  subject.size.should_not == 0
114
164
  end
165
+
166
+ it "ignores expired entries" do
167
+ subject.set(:a, :b, :time => 1)
168
+ sleep 2
169
+ subject.size.should == 0
170
+ end
115
171
  end
116
172
 
117
173
  describe "pruning" do
@@ -119,18 +175,26 @@ describe TimeoutCache do
119
175
  subject.set(:a, 1, :time => 2)
120
176
  subject.set(:b, 1, :time => 2)
121
177
  sleep 3
122
- subject.size.should == 2
178
+ subject.should_receive(:prune)
123
179
  subject.get(:a)
124
- subject.size.should == 0
125
180
  end
126
181
 
127
182
  it "does not happen when calling #get(key) when get(key) is not expired" do
128
183
  subject.set(:a, 1, :time => 200)
129
184
  subject.set(:b, 1, :time => 2)
130
185
  sleep 3
131
- subject.size.should == 2
186
+ subject.should_not_receive(:prune)
132
187
  subject.get(:a)
133
- subject.size.should == 2
188
+ end
189
+
190
+ it "happens on #size call" do
191
+ subject.should_receive(:prune)
192
+ subject.size
193
+ end
194
+
195
+ it "happens on an #empty? call" do
196
+ subject.should_receive(:prune)
197
+ subject.empty?
134
198
  end
135
199
  end
136
200
 
@@ -16,4 +16,5 @@ Gem::Specification.new do |s|
16
16
  s.test_files = Dir["test/*"]
17
17
  s.add_development_dependency "rake"
18
18
  s.add_development_dependency "rspec"
19
+ s.add_development_dependency "timecop"
19
20
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: timeout_cache
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -43,6 +43,22 @@ dependencies:
43
43
  - - ! '>='
44
44
  - !ruby/object:Gem::Version
45
45
  version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: timecop
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'
46
62
  description: Simple time-based cache.
47
63
  email:
48
64
  - adam@aprescott.com
@@ -72,12 +88,18 @@ required_ruby_version: !ruby/object:Gem::Requirement
72
88
  - - ! '>='
73
89
  - !ruby/object:Gem::Version
74
90
  version: '0'
91
+ segments:
92
+ - 0
93
+ hash: -1494641401151669738
75
94
  required_rubygems_version: !ruby/object:Gem::Requirement
76
95
  none: false
77
96
  requirements:
78
97
  - - ! '>='
79
98
  - !ruby/object:Gem::Version
80
99
  version: '0'
100
+ segments:
101
+ - 0
102
+ hash: -1494641401151669738
81
103
  requirements: []
82
104
  rubyforge_project:
83
105
  rubygems_version: 1.8.24