frivol 0.1.7 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.7
1
+ 0.2.0
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{frivol}
8
- s.version = "0.1.7"
8
+ s.version = "0.2.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Marc Heiligers"]
12
- s.date = %q{2010-11-01}
12
+ s.date = %q{2011-03-17}
13
13
  s.description = %q{Simple Redis backed temporary storage intended primarily for use with ActiveRecord models to provide caching}
14
14
  s.email = %q{marc@eternal.co.za}
15
15
  s.extra_rdoc_files = [
@@ -188,17 +188,24 @@ module Frivol
188
188
  data, is_new = get_data_and_is_new instance
189
189
  data[bucket.to_s] = hash
190
190
 
191
- key = instance.send(:storage_key, bucket)
192
- Frivol::Config.redis[key] = hash.to_json
193
-
194
- if is_new[bucket.to_s]
195
- time = instance.class.storage_expiry(bucket)
196
- Frivol::Config.redis.expire(key, time) if time != Frivol::NEVER_EXPIRE
197
- is_new[bucket.to_s] = false
198
- end
191
+ store_value instance, is_new[bucket.to_s], hash.to_json, bucket
199
192
 
200
193
  self.set_data_and_is_new instance, data, is_new
201
194
  end
195
+
196
+ def self.store_value(instance, is_new, value, bucket = nil)
197
+ key = instance.send(:storage_key, bucket)
198
+ time = instance.class.storage_expiry(bucket)
199
+ if time == Frivol::NEVER_EXPIRE
200
+ Frivol::Config.redis[key] = value
201
+ else
202
+ Frivol::Config.redis.multi do |redis|
203
+ time = redis.ttl(key) unless is_new
204
+ redis[key] = value
205
+ redis.expire(key, time)
206
+ end
207
+ end
208
+ end
202
209
 
203
210
  def self.retrieve_hash(instance, bucket = nil)
204
211
  data, is_new = get_data_and_is_new instance
@@ -242,11 +249,7 @@ module Frivol
242
249
  def self.store_counter(instance, counter, value)
243
250
  key = instance.send(:storage_key, counter)
244
251
  is_new = !Frivol::Config.redis.exists(key)
245
- Frivol::Config.redis[key] = value
246
- if is_new
247
- time = instance.class.storage_expiry(counter)
248
- Frivol::Config.redis.expire(key, time) if time != Frivol::NEVER_EXPIRE
249
- end
252
+ store_value instance, is_new, value, counter
250
253
  end
251
254
 
252
255
  def self.retrieve_counter(instance, counter, default)
@@ -339,6 +342,60 @@ module Frivol
339
342
  Frivol::Helpers.clear_hash(self, bucket)
340
343
  end
341
344
  end
345
+
346
+ # Use Frivol to cache results for a method (similar to memoize).
347
+ # Options are :bucket which sets the bucket name for the storage,
348
+ # :expires_in which sets the expiry time for a bucket,
349
+ # and :counter to create a special counter storage bucket.
350
+ #
351
+ # If not :counter the key is the method_name.
352
+ #
353
+ # If you supply :expires_in you must also supply a :bucket otherwise
354
+ # it is ignored (and the default class expires_in is used if supplied).
355
+ #
356
+ # If :counter and no :bucket is provided the :bucket is set to the
357
+ # :bucket is set to the method_name (and so the :expires_in will be used).
358
+ def frivolize(method_name, options = {})
359
+ bucket = options[:bucket]
360
+ time = options[:expires_in]
361
+ is_counter = options[:counter]
362
+ bucket = method_name if bucket.nil? && is_counter
363
+ frivolized_method_name = "frivolized_#{method_name}"
364
+
365
+ self.class_eval do
366
+ alias_method frivolized_method_name, method_name
367
+ storage_bucket(bucket, { :expires_in => time, :counter => is_counter }) unless bucket.nil?
368
+
369
+ if is_counter
370
+ define_method method_name do
371
+ value = send "retrieve_#{bucket}", -2147483647 # A rediculously small number that is unlikely to be used: -2**31 + 1
372
+ if value == -2147483647
373
+ value = send frivolized_method_name
374
+ send "store_#{bucket}", value
375
+ end
376
+ value
377
+ end
378
+ elsif !bucket.nil?
379
+ define_method method_name do
380
+ value = send "retrieve_#{bucket}", { method_name => false }
381
+ if !value
382
+ value = send frivolized_method_name
383
+ send "store_#{bucket}", { method_name => value }
384
+ end
385
+ value
386
+ end
387
+ else
388
+ define_method method_name do
389
+ value = retrieve method_name => false
390
+ if !value
391
+ value = send frivolized_method_name
392
+ store method_name.to_sym => value
393
+ end
394
+ value
395
+ end
396
+ end
397
+ end
398
+ end
342
399
  end
343
400
 
344
401
  # def storage_default(keys_and_defaults)
@@ -5,35 +5,61 @@ class Redis
5
5
  end
6
6
 
7
7
  def [](key)
8
- # puts "retrieve #{key}"
8
+ # puts "retrieve #{key} => #{@storage[key]} (#{(!@expires[key].nil? && @expires[key] < Time.now) ? 'expired' : 'alive'})"
9
9
  return nil if !@expires[key].nil? && @expires[key] < Time.now
10
10
  @storage[key]
11
11
  end
12
12
 
13
13
  def []=(key, value)
14
- # puts "store #{key}"
14
+ # puts "store #{key} => #{value}"
15
15
  @storage[key] = value
16
+ @expires.delete key
16
17
  end
17
18
 
18
19
  def del(key)
19
20
  # puts "del #{key}"
20
- @storage[key] = nil
21
- @expires[key] = nil
21
+ @storage.delete key
22
+ @expires.delete key
22
23
  end
23
24
 
24
25
  def incr(key)
26
+ # puts "incr #{key}"
25
27
  @storage[key] += 1
26
28
  end
27
29
 
28
30
  def expire(key, time)
29
- @expires[key] = Time.now + time
31
+ begin
32
+ t = Integer(time)
33
+ rescue
34
+ raise RuntimeError.new("-ERR value is not an integer")
35
+ end
36
+ @expires[key] = Time.now + t
30
37
  end
31
38
 
32
39
  def exists(key)
33
40
  @storage.key? key
34
41
  end
35
42
 
43
+ def ttl(key)
44
+ # puts "ttl #{key}"
45
+ return -1 if @expires[key].nil? || @expires[key] < Time.now
46
+ (@expires[key] - Time.now).to_i
47
+ end
48
+
49
+ def multi(&block)
50
+ yield(self)
51
+ end
52
+
36
53
  def flushdb
37
54
  @storage = {}
38
55
  end
56
+
57
+ # Help with debugging
58
+ def inspect
59
+ @storage.keys.sort.map do |key|
60
+ result = "\n#{key} => #{@storage[key].inspect}"
61
+ result << "\n\texpires at #{@expires[key]}" if @expires.key?(key)
62
+ result
63
+ end
64
+ end
39
65
  end
@@ -1,4 +1,4 @@
1
- require 'helper'
1
+ require "#{File.expand_path(File.dirname(__FILE__))}/helper.rb"
2
2
 
3
3
  class TestFrivol < Test::Unit::TestCase
4
4
  def setup
@@ -8,6 +8,7 @@ class TestFrivol < Test::Unit::TestCase
8
8
  end
9
9
 
10
10
  def teardown
11
+ # puts Frivol::Config.redis.inspect
11
12
  Frivol::Config.redis.flushdb
12
13
  end
13
14
 
@@ -114,38 +115,52 @@ class TestFrivol < Test::Unit::TestCase
114
115
  assert_equal Time.local(2010, 8, 20), t.load
115
116
  end
116
117
 
117
- should "expire storage the first time it's stored" do
118
+ should "expire storage the everytime time it's stored" do
118
119
  class ExpiryTestClass < TestClass
119
120
  storage_expires_in 60
120
121
  end
121
122
 
122
- Frivol::Config.redis.expects(:expire).once
123
+ Frivol::Config.redis.expects(:expire).twice
124
+ Frivol::Config.redis.expects(:ttl).once
123
125
  t = ExpiryTestClass.new
124
126
  t.load
125
127
  t.save
128
+ t = ExpiryTestClass.new # get a new one
126
129
  t.save
127
130
  end
128
131
 
129
132
  should "expires should not throw nasty errors" do
130
133
  t = TestClass.new
131
134
  t.save
132
- t.expire_storage 0.5
133
- sleep 1
135
+ t.expire_storage 1
136
+ sleep 2
134
137
  t = TestClass.new # Get a fresh instance so that the @frivol_data is empty
135
138
  assert_equal "default", t.load
136
139
  end
137
140
 
138
141
  should "use default expiry set on the class" do
139
142
  class ExpiryTestClass < TestClass
140
- storage_expires_in 0.5
143
+ storage_expires_in 1
141
144
  end
142
145
  t = ExpiryTestClass.new
143
146
  t.save
144
- sleep 1
145
- t = TestClass.new # Get a fresh instance so that the @frivol_data is empty
147
+ sleep 2
148
+ t = ExpiryTestClass.new # Get a fresh instance so that the @frivol_data is empty
146
149
  assert_equal "default", t.load
147
150
  end
148
151
 
152
+ # If you save a value to a volatile key, the expiry is cleared
153
+ should "resaving a value should not clear the expiry" do
154
+ class ResavingExpiryTestClass < TestClass
155
+ storage_expires_in 2
156
+ end
157
+ t = ResavingExpiryTestClass.new
158
+ t.save
159
+ assert Frivol::Config.redis.ttl(t.storage_key) > 0
160
+ t.save # a second time
161
+ assert Frivol::Config.redis.ttl(t.storage_key) > 0
162
+ end
163
+
149
164
  should "be able to include in other classes with storage expiry" do
150
165
  class BlankTestClass
151
166
  end
@@ -199,18 +214,18 @@ class TestFrivol < Test::Unit::TestCase
199
214
 
200
215
  should "have different expiry times for different buckets" do
201
216
  class ExpireBucketsTestClass < TestClass
202
- storage_bucket :blue, :expires_in => 0.5
217
+ storage_bucket :blue, :expires_in => 1
203
218
  storage_expires_in 2
204
219
  end
205
220
  t = ExpireBucketsTestClass.new
206
- assert_equal 0.5, ExpireBucketsTestClass.storage_expiry(:blue)
221
+ assert_equal 1, ExpireBucketsTestClass.storage_expiry(:blue)
207
222
  assert_equal 2, ExpireBucketsTestClass.storage_expiry
208
223
  end
209
224
 
210
225
  should "expire data in buckets" do
211
226
  class ExpireBucketsTestClass < TestClass
212
- storage_bucket :blue, :expires_in => 0.5
213
- storage_expires_in 2
227
+ storage_bucket :blue, :expires_in => 1
228
+ storage_expires_in 3
214
229
 
215
230
  def save_blue
216
231
  store_blue :value => "blue value"
@@ -223,7 +238,7 @@ class TestFrivol < Test::Unit::TestCase
223
238
  t = ExpireBucketsTestClass.new
224
239
  t.save
225
240
  t.save_blue
226
- sleep 1
241
+ sleep 2
227
242
  t = ExpireBucketsTestClass.new # get a new instance so @frivol_data is empty
228
243
  assert_equal "value", t.load
229
244
  assert_equal "blue default", t.load_blue
@@ -261,15 +276,15 @@ class TestFrivol < Test::Unit::TestCase
261
276
 
262
277
  should "set expiry on counters" do
263
278
  class SetExpireCounterTestClass < TestClass
264
- storage_bucket :sheep_counter, :counter => true, :expires_in => 0.5
279
+ storage_bucket :sheep_counter, :counter => true, :expires_in => 1
265
280
  end
266
281
  t = SetExpireCounterTestClass.new
267
- assert_equal 0.5, SetExpireCounterTestClass.storage_expiry(:sheep_counter)
282
+ assert_equal 1, SetExpireCounterTestClass.storage_expiry(:sheep_counter)
268
283
  end
269
284
 
270
285
  should "expire a counter bucket" do
271
286
  class ExpireCounterTestClass < TestClass
272
- storage_bucket :kitten_grave, :counter => true, :expires_in => 0.5
287
+ storage_bucket :kitten_grave, :counter => true, :expires_in => 1
273
288
 
274
289
  def bury_kittens
275
290
  store_kitten_grave 10
@@ -279,12 +294,13 @@ class TestFrivol < Test::Unit::TestCase
279
294
  retrieve_kitten_grave 0
280
295
  end
281
296
  end
297
+
282
298
  k = ExpireCounterTestClass.new
283
299
  k.bury_kittens
284
300
  assert_equal 10, k.peek_in_grave
285
- sleep(0.6)
286
- assert_equal 0, k.peek_in_grave
287
301
 
302
+ sleep 2
303
+ assert_equal 0, k.peek_in_grave
288
304
  end
289
305
 
290
306
  # Note: this test will fail from time to time using fake_redis because fake_redis is not thread safe
@@ -359,5 +375,71 @@ class TestFrivol < Test::Unit::TestCase
359
375
  assert_equal "value", t.load_gold
360
376
  end
361
377
 
378
+ should "frivolize methods" do
379
+ class FrivolizeTestClass < TestClass
380
+ def dinosaur_count
381
+ 10
382
+ end
383
+ frivolize :dinosaur_count
384
+ end
385
+
386
+ Frivol::Config.redis.expects(:[]=).once
387
+ Frivol::Config.redis.expects(:[]).twice.returns(nil, { :dinosaur_count => 10 }.to_json)
388
+
389
+ t = FrivolizeTestClass.new
390
+ assert t.methods.include? "dinosaur_count"
391
+ assert t.methods.include? "frivolized_dinosaur_count"
392
+
393
+ assert_equal 10, t.dinosaur_count
394
+
395
+ t = FrivolizeTestClass.new # a fresh instance
396
+ assert_equal 10, t.dinosaur_count
397
+ end
398
+
399
+ should "frivolize methods with expiry in a bucket" do
400
+ class FrivolizeExpiringBucketTestClass < TestClass
401
+ def dinosaur_count
402
+ 10
403
+ end
404
+ frivolize :dinosaur_count, { :bucket => :dinosaurs, :expires_in => 1 }
405
+ end
406
+ Frivol::Config.redis.expects(:[]=).twice
407
+ Frivol::Config.redis.expects(:[]).times(3).returns(nil, { :dinosaur_count => 10 }.to_json, nil)
408
+
409
+ t = FrivolizeExpiringBucketTestClass.new
410
+ assert_equal 10, t.dinosaur_count
411
+
412
+ t = FrivolizeExpiringBucketTestClass.new # a fresh instance
413
+ assert_equal 10, t.dinosaur_count
414
+
415
+ sleep 2
416
+
417
+ t = FrivolizeExpiringBucketTestClass.new # another fresh instance after value expired
418
+ assert_equal 10, t.dinosaur_count
419
+ end
420
+
421
+ should "frivolize methods with expiry as a counter" do
422
+ class FrivolizeExpiringBucketTestClass < TestClass
423
+ def dinosaur_count
424
+ 10
425
+ end
426
+ frivolize :dinosaur_count, { :expires_in => 1, :counter => true }
427
+ end
428
+ Frivol::Config.redis.expects(:[]=).twice
429
+ Frivol::Config.redis.expects(:[]).times(3).returns(nil, 10, nil)
430
+
431
+ t = FrivolizeExpiringBucketTestClass.new
432
+ assert t.methods.include? "store_dinosaur_count" # check that the bucket name is the method name
433
+
434
+ assert_equal 10, t.dinosaur_count
435
+
436
+ t = FrivolizeExpiringBucketTestClass.new # a fresh instance
437
+ assert_equal 10, t.dinosaur_count
438
+
439
+ sleep 2
440
+
441
+ t = FrivolizeExpiringBucketTestClass.new # another fresh instance after value expired
442
+ assert_equal 10, t.dinosaur_count
443
+ end
362
444
 
363
445
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: frivol
3
3
  version: !ruby/object:Gem::Version
4
- hash: 21
4
+ hash: 23
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
- - 1
9
- - 7
10
- version: 0.1.7
8
+ - 2
9
+ - 0
10
+ version: 0.2.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Marc Heiligers
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2010-11-01 00:00:00 +02:00
18
+ date: 2011-03-17 00:00:00 +02:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency