frivol 0.1.7 → 0.2.0

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