frivol 0.1.4 → 0.1.5

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.
@@ -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