frivol 0.1.4 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -28,6 +28,21 @@
28
28
  # The <tt>expire_storage(time)</tt> method can be used to set the expiry time in seconds of the temporary storage.
29
29
  # The default is not to expire the storage, in which case it will live for as long as Redis keeps it.
30
30
  # <tt>delete_storage</tt>, as the name suggests will immediately delete the storage.
31
+ #
32
+ # Since version 0.1.5 Frivol can create different storage buckets. Note that this introduces a breaking change
33
+ # to the <tt>storage_key</tt> method if you have overriden it. It now takes a +bucket+ parameter.
34
+ #
35
+ # Buckets can have their own expiry time and there are special counter buckets which simply keep an integer count.
36
+ #
37
+ # storage_bucket :my_bucket, :expires_in => 5.minutes
38
+ # storage_bucket :my_counter, :counter => true
39
+ #
40
+ # Given the above, Frivol will create <tt>store_my_bucket</tt> and <tt>retrieve_my_bucket</tt> methods which work
41
+ # exactly like the standard +store+ and +retrieve+ methods. There will also be <tt>store_my_counter</tt>,
42
+ # <tt>retrieve_my_counter</tt> and <tt>increment_my_counter</tt> methods. The counter store and retrieve only
43
+ # take a integer (value and default, respectively) and the increment does not take a parameter.
44
+ #
45
+ # These methods are thread safe if you pass <tt>:thread_safe => true</tt> to the Redis configuration.
31
46
  #
32
47
  # Frivol uses the +storage_key+ method to create a base key for storage in Redis. The current implementation uses
33
48
  # <tt>"#{self.class.name}-#{id}"</tt> so you'll want to override that method if you have classes that don't
@@ -44,8 +59,8 @@
44
59
  # @key = key
45
60
  # end
46
61
  #
47
- # def storage_key
48
- # "frivol-test-#{key}" # override the storage key because we don't respond_to? id
62
+ # def storage_key(bucket = nil)
63
+ # "frivol-test-#{key}" # override the storage key because we don't respond_to? :id, and don't care about buckets
49
64
  # end
50
65
  #
51
66
  # def big_complex_calc
@@ -69,6 +84,9 @@ require "redis"
69
84
 
70
85
  # == Frivol
71
86
  module Frivol
87
+ # Defines a constant to indicate that storage should never expire
88
+ NEVER_EXPIRE = nil
89
+
72
90
  # Store a hash of keys and values.
73
91
  #
74
92
  # The hash need not be the complete hash of all things stored, just those you want to change.
@@ -77,11 +95,11 @@ module Frivol
77
95
  # is intended to be hidden and while it is true that it currently uses a <tt>Hash#to_json</tt> you should not
78
96
  # rely on this.
79
97
  def store(keys_and_values)
80
- Frivol::Helpers.retrieve_hash self
98
+ hash = Frivol::Helpers.retrieve_hash(self)
81
99
  keys_and_values.each do |key, value|
82
- @frivol_hash[key.to_s] = value
100
+ hash[key.to_s] = value
83
101
  end
84
- Frivol::Helpers.store_hash self
102
+ Frivol::Helpers.store_hash(self, hash)
85
103
  end
86
104
 
87
105
  # Retrieve stored values, or defaults.
@@ -94,9 +112,9 @@ module Frivol
94
112
  # If the default is a symbol, Frivol will attempt to get the default from a method named after that symbol.
95
113
  # If the class does not <tt>respond_to?</tt> a method by that name, the symbol will assumed to be the default.
96
114
  def retrieve(keys_and_defaults)
97
- Frivol::Helpers.retrieve_hash self
115
+ hash = Frivol::Helpers.retrieve_hash(self)
98
116
  result = keys_and_defaults.map do |key, default|
99
- @frivol_hash[key.to_s] || (default.is_a?(Symbol) && respond_to?(default) && send(default)) || default
117
+ hash[key.to_s] || (default.is_a?(Symbol) && respond_to?(default) && send(default)) || default
100
118
  end
101
119
  return result.first if result.size == 1
102
120
  result
@@ -108,18 +126,22 @@ module Frivol
108
126
  end
109
127
 
110
128
  # Expire the stored data in +time+ seconds.
111
- def expire_storage(time)
129
+ def expire_storage(time, bucket = nil)
112
130
  return if time.nil?
113
- Frivol::Config.redis.expire storage_key, time
131
+ Frivol::Config.redis.expire storage_key(bucket), time
114
132
  end
115
133
 
116
134
  # The base key used for storage in Redis.
117
135
  #
118
- # This method has been implemented for use with ActiveRecord and uses <tt>"#{self.class.name}-#{id}"</tt>
136
+ # This method has been implemented for use with ActiveRecord and uses <tt>"#{self.class.name}-#{id}"</tt>
137
+ # for the default bucket and <tt>"#{self.class.name}-#{id}-#{bucket}"</tt> for a named bucket.
119
138
  # If you are not using ActiveRecord, or using classes that don't respond to id, you should override
120
139
  # this method in your class.
121
- def storage_key
140
+ #
141
+ # NOTE: This method has changed since version 0.1.4, and now has the bucket parameter (default: nil)
142
+ def storage_key(bucket = nil)
122
143
  @frivol_key ||= "#{self.class.name}-#{id}"
144
+ bucket.nil? ? @frivol_key : "#{@frivol_key}-#{bucket}"
123
145
  end
124
146
 
125
147
  # == Frivol::Config
@@ -156,46 +178,144 @@ module Frivol
156
178
  end
157
179
 
158
180
  module Helpers #:nodoc:
159
- def self.store_hash(instance)
160
- hash = instance.instance_variable_get(:@frivol_hash)
161
- is_new = instance.instance_variable_get(:@frivol_is_new)
162
- key = instance.send(:storage_key)
181
+ def self.store_hash(instance, hash, bucket = nil)
182
+ data, is_new = get_hash_and_is_new(instance, bucket)
183
+ data[bucket.to_s] = hash
184
+
185
+ key = instance.send(:storage_key, bucket)
163
186
  Frivol::Config.redis[key] = hash.to_json
164
- if is_new
165
- instance.expire_storage instance.class.storage_expiry
166
- instance.instance_variable_set :@frivol_is_new, false
187
+
188
+ if is_new[bucket.to_s]
189
+ time = instance.class.storage_expiry(bucket)
190
+ Frivol::Config.redis.expire(key, time) if time != Frivol::NEVER_EXPIRE
191
+ is_new[bucket.to_s] = false
167
192
  end
168
193
  end
169
194
 
170
- def self.retrieve_hash(instance)
171
- return instance.instance_variable_get(:@frivol_hash) if instance.instance_variable_defined? :@frivol_hash
172
- key = instance.send(:storage_key)
195
+ def self.retrieve_hash(instance, bucket = nil)
196
+ data, is_new = get_hash_and_is_new(instance, bucket)
197
+ return data[bucket.to_s] if data.key?(bucket.to_s)
198
+ key = instance.send(:storage_key, bucket)
173
199
  json = Frivol::Config.redis[key]
174
- instance.instance_variable_set :@frivol_is_new, json.nil?
200
+
201
+ is_new[bucket.to_s] = json.nil?
202
+ instance.instance_variable_set :@frivol_is_new, is_new
203
+
175
204
  hash = json.nil? ? {} : JSON.parse(json)
176
- instance.instance_variable_set :@frivol_hash, hash
205
+ data[bucket.to_s] = hash
206
+ instance.instance_variable_set :@frivol_data, data
207
+
177
208
  hash
178
209
  end
179
210
 
180
- def self.delete_hash(instance)
181
- key = instance.send(:storage_key)
211
+ def self.delete_hash(instance, bucket = nil)
212
+ key = instance.send(:storage_key, bucket)
182
213
  Frivol::Config.redis.del key
183
- instance.instance_variable_set :@frivol_hash, {}
214
+
215
+ data = instance.instance_variable_defined?(:@frivol_data) ? instance.instance_variable_get(:@frivol_data) : {}
216
+ data.delete(bucket.to_s)
217
+ instance.instance_variable_set :@frivol_data, data
218
+ end
219
+
220
+ def self.get_hash_and_is_new(instance, bucket)
221
+ data = instance.instance_variable_defined?(:@frivol_data) ? instance.instance_variable_get(:@frivol_data) : {}
222
+ is_new = instance.instance_variable_defined?(:@frivol_is_new) ? instance.instance_variable_get(:@frivol_is_new) : {}
223
+ [data, is_new]
224
+ end
225
+
226
+ def self.store_counter(instance, counter, value)
227
+ key = instance.send(:storage_key, counter)
228
+ Frivol::Config.redis[key] = value
229
+ end
230
+
231
+ def self.retrieve_counter(instance, counter, default)
232
+ key = instance.send(:storage_key, counter)
233
+ (Frivol::Config.redis[key] || default).to_i
234
+ end
235
+
236
+ def self.increment_counter(instance, counter)
237
+ key = instance.send(:storage_key, counter)
238
+ Frivol::Config.redis.incr(key)
184
239
  end
185
240
  end
186
241
 
187
242
  # == Frivol::ClassMethods
188
243
  # These methods are available on the class level when Frivol is included in the class.
189
244
  module ClassMethods
190
- # Set the storage expiry time in seconds.
191
- def storage_expires_in(time)
192
- @frivol_storage_expiry = time
245
+ # Set the storage expiry time in seconds for the default bucket or the bucket passed.
246
+ def storage_expires_in(time, bucket = nil)
247
+ @frivol_storage_expiry ||= {}
248
+ @frivol_storage_expiry[bucket.to_s] = time
193
249
  end
194
250
 
195
- # Get the storage expiry time in seconds.
196
- def storage_expiry
197
- @frivol_storage_expiry
251
+ # Get the storage expiry time in seconds for the default bucket or the bucket passed.
252
+ def storage_expiry(bucket = nil)
253
+ @frivol_storage_expiry ||= {}
254
+ @frivol_storage_expiry.key?(bucket.to_s) ? @frivol_storage_expiry[bucket.to_s] : NEVER_EXPIRE
198
255
  end
256
+
257
+ # Create a storage bucket.
258
+ # Frivol creates store_#{bucket} and retrieve_#{bucket} methods automatically.
259
+ # These methods work exactly like the default store and retrieve methods except that the bucket is
260
+ # stored in it's own key in Redis and can have it's own expiry time.
261
+ #
262
+ # Counters are special in that they do not store a hash but only a single integer value and also
263
+ # that the data in a counter is not cached for the lifespan of the object, but rather each call
264
+ # hits Redis. This is intended to make counters thread safe (for example you may have multiple
265
+ # workers working on a job and they can each increment a progress counter which would not work
266
+ # with the default retrieve/store method that normal buckets use). For this to actually be thread safe
267
+ # you need to pass the thread safe option to the config when you make the connection.
268
+ #
269
+ # In the case of a counter, the methods work slightly differently:
270
+ # - store_#{bucket} only takes an integer value to store (no key)
271
+ # - retrieve_#{bucket} only takes an integer default, and returns only the integer value
272
+ # - there is an added increment_#{bucket} method which increments the counter by 1
273
+ #
274
+ # Options are :expires_in which sets the expiry time for a bucket,
275
+ # and :counter to create a special counter storage bucket.
276
+ def storage_bucket(bucket, options = {})
277
+ time = options[:expires_in]
278
+ storage_expires_in(time, bucket) if !time.nil?
279
+ is_counter = options[:counter]
280
+
281
+ self.class_eval do
282
+ if is_counter
283
+ define_method "store_#{bucket}" do |value|
284
+ Frivol::Helpers.store_counter(self, bucket, value)
285
+ end
286
+
287
+ define_method "retrieve_#{bucket}" do |default|
288
+ Frivol::Helpers.retrieve_counter(self, bucket, default)
289
+ end
290
+
291
+ define_method "increment_#{bucket}" do
292
+ Frivol::Helpers.increment_counter(self, bucket)
293
+ end
294
+ else
295
+ define_method "store_#{bucket}" do |keys_and_values|
296
+ hash = Frivol::Helpers.retrieve_hash(self, bucket)
297
+ keys_and_values.each do |key, value|
298
+ hash[key.to_s] = value
299
+ end
300
+ Frivol::Helpers.store_hash(self, hash, bucket)
301
+ end
302
+
303
+ define_method "retrieve_#{bucket}" do |keys_and_defaults|
304
+ hash = Frivol::Helpers.retrieve_hash(self, bucket)
305
+ result = keys_and_defaults.map do |key, default|
306
+ hash[key.to_s] || (default.is_a?(Symbol) && respond_to?(default) && send(default)) || default
307
+ end
308
+ return result.first if result.size == 1
309
+ result
310
+ end
311
+ end
312
+ end
313
+ end
314
+
315
+ # def storage_default(keys_and_defaults)
316
+ # @frivol_defaults ||= {}
317
+ # @frivol_defaults.merge keys_and_defaults
318
+ # end
199
319
  end
200
320
 
201
321
  def self.included(host) #:nodoc:
@@ -21,11 +21,15 @@ class Redis
21
21
  @expires[key] = nil
22
22
  end
23
23
 
24
+ def incr(key)
25
+ @storage[key] += 1
26
+ end
27
+
24
28
  def expire(key, time)
25
29
  @expires[key] = Time.now + time
26
30
  end
27
31
 
28
- def flush_db
32
+ def flushdb
29
33
  @storage = {}
30
34
  end
31
35
  end
@@ -2,12 +2,12 @@ require 'helper'
2
2
 
3
3
  class TestFrivol < Test::Unit::TestCase
4
4
  def setup
5
- #fake_redis # Comment out this line to test against a real live Redis
6
- Frivol::Config.redis_config = {} # This will connect to a default Redis setup, otherwise set to { :host => "localhost", :port => 6379 }, for example
5
+ # fake_redis # Comment out this line to test against a real live Redis
6
+ Frivol::Config.redis_config = { :thread_safe => true } # This will connect to a default Redis setup, otherwise set to { :host => "localhost", :port => 6379 }, for example
7
7
  end
8
8
 
9
9
  def teardown
10
- Frivol::Config.redis.flush_db
10
+ Frivol::Config.redis.flushdb
11
11
  end
12
12
 
13
13
  should "have a default storage key made up of the class name and id" do
@@ -86,7 +86,7 @@ class TestFrivol < Test::Unit::TestCase
86
86
 
87
87
  should "be able to override the key method" do
88
88
  class OverrideKeyTestClass < TestClass
89
- def storage_key
89
+ def storage_key(bucket = nil)
90
90
  "my_storage"
91
91
  end
92
92
  end
@@ -130,10 +130,21 @@ class TestFrivol < Test::Unit::TestCase
130
130
  t.save
131
131
  t.expire_storage 0.5
132
132
  sleep 1
133
- t = TestClass.new # Get a fresh instance so that the @frivol_hash is empty
133
+ t = TestClass.new # Get a fresh instance so that the @frivol_data is empty
134
134
  assert_equal "default", t.load
135
135
  end
136
136
 
137
+ should "use default expiry set on the class" do
138
+ class ExpiryTestClass < TestClass
139
+ storage_expires_in 0.5
140
+ end
141
+ t = ExpiryTestClass.new
142
+ t.save
143
+ sleep 1
144
+ t = TestClass.new # Get a fresh instance so that the @frivol_data is empty
145
+ assert_equal "default", t.load
146
+ end
147
+
137
148
  should "be able to include in other classes with storage expiry" do
138
149
  class BlankTestClass
139
150
  end
@@ -156,4 +167,125 @@ class TestFrivol < Test::Unit::TestCase
156
167
  t.delete_storage
157
168
  assert_equal "default", t.load
158
169
  end
170
+
171
+ should "be able to create and use buckets" do
172
+ class SimpleBucketTestClass < TestClass
173
+ storage_bucket :blue
174
+ end
175
+ t = SimpleBucketTestClass.new
176
+ assert t.respond_to?(:store_blue)
177
+ assert t.respond_to?(:retrieve_blue)
178
+ end
179
+
180
+ should "store different values in different buckets" do
181
+ class StorageBucketTestClass < TestClass
182
+ storage_bucket :blue
183
+
184
+ def save_blue
185
+ store_blue :value => "blue value"
186
+ end
187
+
188
+ def load_blue
189
+ retrieve_blue :value => "blue default"
190
+ end
191
+ end
192
+ t = StorageBucketTestClass.new
193
+ t.save
194
+ t.save_blue
195
+ assert_equal "value", t.load
196
+ assert_equal "blue value", t.load_blue
197
+ end
198
+
199
+ should "have different expiry times for different buckets" do
200
+ class ExpireBucketsTestClass < TestClass
201
+ storage_bucket :blue, :expires_in => 0.5
202
+ storage_expires_in 2
203
+ end
204
+ t = ExpireBucketsTestClass.new
205
+ assert_equal 0.5, ExpireBucketsTestClass.storage_expiry(:blue)
206
+ assert_equal 2, ExpireBucketsTestClass.storage_expiry
207
+ end
208
+
209
+ should "expire data in buckets" do
210
+ class ExpireBucketsTestClass < TestClass
211
+ storage_bucket :blue, :expires_in => 0.5
212
+ storage_expires_in 2
213
+
214
+ def save_blue
215
+ store_blue :value => "blue value"
216
+ end
217
+
218
+ def load_blue
219
+ retrieve_blue :value => "blue default"
220
+ end
221
+ end
222
+ t = ExpireBucketsTestClass.new
223
+ t.save
224
+ t.save_blue
225
+ sleep 1
226
+ t = ExpireBucketsTestClass.new # get a new instance so @frivol_data is empty
227
+ assert_equal "value", t.load
228
+ assert_equal "blue default", t.load_blue
229
+ end
230
+
231
+ should "be able to create counter buckets" do
232
+ class SimpleCounterTestClass < TestClass
233
+ storage_bucket :blue, :counter => true
234
+ end
235
+ t = SimpleCounterTestClass.new
236
+ assert t.respond_to?(:store_blue)
237
+ assert t.respond_to?(:retrieve_blue)
238
+ assert t.respond_to?(:increment_blue)
239
+ end
240
+
241
+ should "store, increment and retrieve integers in a counter" do
242
+ class IncrCounterTestClass < TestClass
243
+ storage_bucket :blue, :counter => true
244
+
245
+ def save_blue
246
+ store_blue 10
247
+ end
248
+
249
+ def load_blue
250
+ retrieve_blue 0
251
+ end
252
+ end
253
+ t = IncrCounterTestClass.new
254
+ assert_equal 0, t.load_blue
255
+ t.save_blue
256
+ assert_equal 10, t.load_blue
257
+ assert_equal 11, t.increment_blue
258
+ assert_equal 11, t.load_blue
259
+ end
260
+
261
+ should "have thread safe counters" do
262
+ class ThreadCounterTestClass < TestClass
263
+ storage_bucket :blue, :counter => true
264
+
265
+ def save_blue
266
+ store_blue 10
267
+ end
268
+
269
+ def load_blue
270
+ retrieve_blue 0
271
+ end
272
+ end
273
+ t = ThreadCounterTestClass.new
274
+ t.save_blue
275
+ assert_equal 10, t.load_blue
276
+
277
+ threads = []
278
+ 100.times do
279
+ threads << Thread.new do
280
+ 10.times do
281
+ temp = ThreadCounterTestClass.new
282
+ temp.increment_blue
283
+ sleep(rand(10) / 100.0)
284
+ end
285
+ end
286
+ end
287
+ threads.each { |a| a.join }
288
+
289
+ assert_equal 1010, t.load_blue
290
+ end
159
291
  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: 19
4
+ hash: 17
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
8
  - 1
9
- - 4
10
- version: 0.1.4
9
+ - 5
10
+ version: 0.1.5
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-08-22 00:00:00 +02:00
18
+ date: 2010-10-15 00:00:00 +02:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -42,12 +42,12 @@ dependencies:
42
42
  requirements:
43
43
  - - ">="
44
44
  - !ruby/object:Gem::Version
45
- hash: 31
45
+ hash: 27
46
46
  segments:
47
- - 0
48
- - 1
49
47
  - 2
50
- version: 0.1.2
48
+ - 0
49
+ - 10
50
+ version: 2.0.10
51
51
  type: :runtime
52
52
  version_requirements: *id002
53
53
  - !ruby/object:Gem::Dependency
@@ -87,13 +87,18 @@ files:
87
87
  - doc/classes/Frivol.src/M000004.html
88
88
  - doc/classes/Frivol.src/M000005.html
89
89
  - doc/classes/Frivol.src/M000006.html
90
+ - doc/classes/Frivol.src/M000007.html
90
91
  - doc/classes/Frivol/ClassMethods.html
91
92
  - doc/classes/Frivol/ClassMethods.src/M000007.html
92
93
  - doc/classes/Frivol/ClassMethods.src/M000008.html
94
+ - doc/classes/Frivol/ClassMethods.src/M000009.html
95
+ - doc/classes/Frivol/ClassMethods.src/M000010.html
93
96
  - doc/classes/Frivol/Config.html
94
97
  - doc/classes/Frivol/Config.src/M000009.html
95
98
  - doc/classes/Frivol/Config.src/M000010.html
96
99
  - doc/classes/Frivol/Config.src/M000011.html
100
+ - doc/classes/Frivol/Config.src/M000012.html
101
+ - doc/classes/Frivol/Config.src/M000013.html
97
102
  - doc/classes/Time.html
98
103
  - doc/classes/Time.src/M000001.html
99
104
  - doc/classes/Time.src/M000002.html