frivol 0.2.0 → 0.3.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.
- checksums.yaml +7 -0
- data/.travis.yml +15 -0
- data/Gemfile +13 -0
- data/LICENSE +1 -1
- data/README.rdoc +58 -38
- data/Rakefile +16 -26
- data/VERSION +1 -1
- data/frivol.gemspec +74 -62
- data/lib/frivol.rb +14 -363
- data/lib/frivol/class_methods.rb +195 -0
- data/lib/frivol/config.rb +39 -0
- data/lib/frivol/functor.rb +37 -0
- data/lib/frivol/helpers.rb +133 -0
- data/lib/frivol/time_extensions.rb +45 -0
- data/test/fake_redis.rb +36 -11
- data/test/helper.rb +16 -11
- data/test/test_buckets.rb +53 -0
- data/test/test_condition.rb +43 -0
- data/test/test_condition_with_counters.rb +90 -0
- data/test/test_counters.rb +72 -0
- data/test/test_else_with_counters.rb +39 -0
- data/test/test_extensions.rb +15 -0
- data/test/test_frivol.rb +96 -388
- data/test/test_frivolize.rb +81 -0
- data/test/test_seeds.rb +53 -0
- data/test/test_threads.rb +15 -0
- metadata +88 -88
- data/.gitignore +0 -21
@@ -0,0 +1,195 @@
|
|
1
|
+
module Frivol
|
2
|
+
# == Frivol::ClassMethods
|
3
|
+
# These methods are available on the class level when Frivol is included in the class.
|
4
|
+
module ClassMethods
|
5
|
+
# Set the storage expiry time in seconds for the default bucket or the bucket passed.
|
6
|
+
def storage_expires_in(time, bucket = nil)
|
7
|
+
@frivol_storage_expiry ||= {}
|
8
|
+
@frivol_storage_expiry[bucket.to_s] = time
|
9
|
+
end
|
10
|
+
|
11
|
+
# Get the storage expiry time in seconds for the default bucket or the bucket passed.
|
12
|
+
def storage_expiry(bucket = nil)
|
13
|
+
@frivol_storage_expiry ||= {}
|
14
|
+
@frivol_storage_expiry.key?(bucket.to_s) ? @frivol_storage_expiry[bucket.to_s] : NEVER_EXPIRE
|
15
|
+
end
|
16
|
+
|
17
|
+
# Create a storage bucket.
|
18
|
+
# Frivol creates store_#{bucket} and retrieve_#{bucket} methods automatically.
|
19
|
+
# These methods work exactly like the default store and retrieve methods except that the bucket is
|
20
|
+
# stored in it's own key in Redis and can have it's own expiry time.
|
21
|
+
#
|
22
|
+
# Counters are special in that they do not store a hash but only a single integer value and also
|
23
|
+
# that the data in a counter is not cached for the lifespan of the object, but rather each call
|
24
|
+
# hits Redis. This is intended to make counters thread safe (for example you may have multiple
|
25
|
+
# workers working on a job and they can each increment a progress counter which would not work
|
26
|
+
# with the default retrieve/store method that normal buckets use). For this to actually be thread safe
|
27
|
+
# you need to pass the thread safe option to the config when you make the connection.
|
28
|
+
#
|
29
|
+
# In the case of a counter, the methods work slightly differently:
|
30
|
+
# - store_#{bucket} only takes an integer value to store (no key)
|
31
|
+
# - retrieve_#{bucket} only takes an integer default, and returns only the integer value
|
32
|
+
# - there is an added increment_#{bucket} method which increments the counter by 1
|
33
|
+
# - as well as increment_#{bucket}_by(value) method which increments the counter by the value
|
34
|
+
# - and similar decrement_#{bucket} and decrement_#{bucket}_by(value) methods
|
35
|
+
#
|
36
|
+
# Options are
|
37
|
+
# - <tt>:expires_in</tt> which sets the expiry time for a bucket;
|
38
|
+
# - <tt>:counter</tt> to create a special counter storage bucket;
|
39
|
+
# - <tt>:condition</tt> that must be satisfied before an action is taken on a bucket;
|
40
|
+
# - <tt>:else</tt>, which is an action that is performed if <tt>:condition</tt> is not satisfied
|
41
|
+
def storage_bucket(bucket, options = {})
|
42
|
+
time = options[:expires_in]
|
43
|
+
storage_expires_in(time, bucket) if !time.nil?
|
44
|
+
|
45
|
+
is_counter = options[:counter]
|
46
|
+
seed_callback = options[:seed]
|
47
|
+
|
48
|
+
|
49
|
+
condition_block = Functor.new(options[:condition], true).compile
|
50
|
+
else_block = Functor.new(options[:else]).compile
|
51
|
+
|
52
|
+
define_method :condition_evaluation do |*args, &block|
|
53
|
+
if instance_exec(*args, &condition_block)
|
54
|
+
block.call
|
55
|
+
else
|
56
|
+
instance_exec(*args, &else_block)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
self.class_eval do
|
61
|
+
if is_counter
|
62
|
+
define_method "store_#{bucket}" do |value|
|
63
|
+
condition_evaluation("store_#{bucket}", value) do
|
64
|
+
Frivol::Helpers.store_counter(self, bucket, value)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
define_method "retrieve_#{bucket}" do |default|
|
69
|
+
return_value = default
|
70
|
+
condition_evaluation("store_#{bucket}", default) do
|
71
|
+
return_value = Frivol::Helpers.retrieve_counter(self, bucket, default)
|
72
|
+
end
|
73
|
+
return_value
|
74
|
+
end
|
75
|
+
|
76
|
+
define_method "increment_#{bucket}" do
|
77
|
+
condition_evaluation("increment_#{bucket}") do
|
78
|
+
Frivol::Helpers.increment_counter(self, bucket, seed_callback)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
define_method "increment_#{bucket}_by" do |amount|
|
83
|
+
condition_evaluation("increment_#{bucket}_by", amount) do
|
84
|
+
Frivol::Helpers.increment_counter_by(self, bucket, amount, seed_callback)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
define_method "decrement_#{bucket}" do
|
89
|
+
Frivol::Helpers.decrement_counter(self, bucket, seed_callback)
|
90
|
+
end
|
91
|
+
|
92
|
+
define_method "decrement_#{bucket}_by" do |amount|
|
93
|
+
Frivol::Helpers.decrement_counter_by(self, bucket, amount, seed_callback)
|
94
|
+
end
|
95
|
+
else
|
96
|
+
define_method "store_#{bucket}" do |keys_and_values|
|
97
|
+
condition_evaluation("store_#{bucket}", keys_and_values) do
|
98
|
+
hash = Frivol::Helpers.retrieve_hash(self, bucket)
|
99
|
+
keys_and_values.each do |key, value|
|
100
|
+
hash[key.to_s] = value
|
101
|
+
end
|
102
|
+
Frivol::Helpers.store_hash(self, hash, bucket)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
define_method "retrieve_#{bucket}" do |keys_and_defaults|
|
107
|
+
hash = {}
|
108
|
+
condition_evaluation("store_#{bucket}", keys_and_defaults) do
|
109
|
+
hash = Frivol::Helpers.retrieve_hash(self, bucket)
|
110
|
+
end
|
111
|
+
|
112
|
+
result = keys_and_defaults.map do |key, default|
|
113
|
+
hash[key.to_s] || (default.is_a?(Symbol) && respond_to?(default) && send(default)) || default
|
114
|
+
end
|
115
|
+
return result.first if result.size == 1
|
116
|
+
result
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
define_method "delete_#{bucket}" do
|
121
|
+
condition_evaluation("delete_#{bucket}") do
|
122
|
+
Frivol::Helpers.delete_hash(self, bucket)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
define_method "clear_#{bucket}" do
|
127
|
+
condition_evaluation("clear_#{bucket}") do
|
128
|
+
Frivol::Helpers.clear_hash(self, bucket)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# Use Frivol to cache results for a method (similar to memoize).
|
134
|
+
# Options are :bucket which sets the bucket name for the storage,
|
135
|
+
# :expires_in which sets the expiry time for a bucket,
|
136
|
+
# and :counter to create a special counter storage bucket.
|
137
|
+
#
|
138
|
+
# If not :counter the key is the method_name.
|
139
|
+
#
|
140
|
+
# If you supply :expires_in you must also supply a :bucket otherwise
|
141
|
+
# it is ignored (and the default class expires_in is used if supplied).
|
142
|
+
#
|
143
|
+
# If :counter and no :bucket is provided the :bucket is set to the
|
144
|
+
# :bucket is set to the method_name (and so the :expires_in will be used).
|
145
|
+
def frivolize(method_name, options = {})
|
146
|
+
bucket = options[:bucket]
|
147
|
+
time = options[:expires_in]
|
148
|
+
is_counter = options[:counter]
|
149
|
+
seed_callback = options[:seed]
|
150
|
+
|
151
|
+
bucket = method_name if bucket.nil? && is_counter
|
152
|
+
frivolized_method_name = "frivolized_#{method_name}"
|
153
|
+
|
154
|
+
self.class_eval do
|
155
|
+
alias_method frivolized_method_name, method_name
|
156
|
+
unless bucket.nil?
|
157
|
+
storage_bucket(bucket, {
|
158
|
+
:expires_in => time,
|
159
|
+
:counter => is_counter,
|
160
|
+
:seed => seed_callback })
|
161
|
+
end
|
162
|
+
|
163
|
+
if is_counter
|
164
|
+
define_method method_name do
|
165
|
+
value = send "retrieve_#{bucket}", -2147483647 # A rediculously small number that is unlikely to be used: -2**31 + 1
|
166
|
+
if value == -2147483647
|
167
|
+
value = send frivolized_method_name
|
168
|
+
send "store_#{bucket}", value
|
169
|
+
end
|
170
|
+
value
|
171
|
+
end
|
172
|
+
elsif !bucket.nil?
|
173
|
+
define_method method_name do
|
174
|
+
value = send "retrieve_#{bucket}", { method_name => false }
|
175
|
+
if !value
|
176
|
+
value = send frivolized_method_name
|
177
|
+
send "store_#{bucket}", { method_name => value }
|
178
|
+
end
|
179
|
+
value
|
180
|
+
end
|
181
|
+
else
|
182
|
+
define_method method_name do
|
183
|
+
value = retrieve method_name => false
|
184
|
+
if !value
|
185
|
+
value = send frivolized_method_name
|
186
|
+
store method_name.to_sym => value
|
187
|
+
end
|
188
|
+
value
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Frivol
|
2
|
+
# == Frivol::Config
|
3
|
+
# Sets the Frivol configuration (currently only the Redis config), allows access to the configured Redis instance,
|
4
|
+
# and has a helper method to include Frivol in a class with an optional storage expiry parameter
|
5
|
+
module Config
|
6
|
+
# Set the Redis configuration.
|
7
|
+
#
|
8
|
+
# Expects a hash such as
|
9
|
+
# REDIS_CONFIG = {
|
10
|
+
# :host => "localhost",
|
11
|
+
# :port => 6379
|
12
|
+
# }
|
13
|
+
# Frivol::Config.redis_config = REDIS_CONFIG
|
14
|
+
def self.redis_config=(config)
|
15
|
+
@@redis_config = config
|
16
|
+
Thread.current[:frivol_redis] = nil
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns the configured Redis instance
|
20
|
+
def self.redis
|
21
|
+
Thread.current[:frivol_redis] ||= Redis.new(@@redis_config)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.allow_json_create
|
25
|
+
@@allow_json_create ||= []
|
26
|
+
end
|
27
|
+
|
28
|
+
# A convenience method to include Frivol in a class, with an optional storage expiry parameter.
|
29
|
+
#
|
30
|
+
# For example, you might have the following in environment.rb:
|
31
|
+
# Frivol::Config.redis_config = REDIS_CONFIG
|
32
|
+
# Frivol::Config.include_in ActiveRecord::Base, 600
|
33
|
+
# Which would include Frivol in ActiveRecord::Base and set the default storage expiry to 10 minutes
|
34
|
+
def self.include_in(host_class, storage_expires_in = nil)
|
35
|
+
host_class.send(:include, Frivol)
|
36
|
+
host_class.storage_expires_in storage_expires_in if storage_expires_in
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Frivol
|
2
|
+
# == Frivol::Functor
|
3
|
+
# Compiles proc, symbols, false, true into a proc that executes within a scope,
|
4
|
+
# or on an object.
|
5
|
+
class Functor
|
6
|
+
# Create a new functor which takes:
|
7
|
+
# method: can be a proc, symbol, false or true
|
8
|
+
# default: the value which is returned from the compiled proc if method is
|
9
|
+
# not a proc, symbol, false or true. Defaults to nil
|
10
|
+
def initialize(method, default=nil)
|
11
|
+
@method = method
|
12
|
+
@default = default
|
13
|
+
end
|
14
|
+
|
15
|
+
# returns a compiled proc based on the initialization arguments
|
16
|
+
def compile
|
17
|
+
case @method
|
18
|
+
when Proc
|
19
|
+
method = @method
|
20
|
+
proc do |*args|
|
21
|
+
args.unshift(self)
|
22
|
+
method.call(*args)
|
23
|
+
end
|
24
|
+
when Symbol
|
25
|
+
method = @method
|
26
|
+
proc do |*args|
|
27
|
+
self.send(method, *args)
|
28
|
+
end
|
29
|
+
when FalseClass, TrueClass
|
30
|
+
proc{ @method }
|
31
|
+
else
|
32
|
+
default_return = @default
|
33
|
+
proc{ default_return }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
module Frivol
|
2
|
+
module Helpers #:nodoc:
|
3
|
+
require 'multi_json'
|
4
|
+
|
5
|
+
def self.dump_json(hash)
|
6
|
+
MultiJson.dump(hash)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.load_json(json)
|
10
|
+
hash = MultiJson.load(json)
|
11
|
+
return hash if Frivol::Config.allow_json_create.empty?
|
12
|
+
hash.each do |k,v|
|
13
|
+
if v.is_a?(Hash) && v['json_class']
|
14
|
+
klass = constantize(v['json_class'])
|
15
|
+
hash[k] = klass.send(:json_create, v) if Frivol::Config.allow_json_create.include?(klass)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.constantize(const)
|
21
|
+
unless /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/ =~ const
|
22
|
+
raise NameError, "#{const.inspect} is not a valid constant name!"
|
23
|
+
end
|
24
|
+
Object.module_eval("::#{$1}", __FILE__, __LINE__)
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
def self.store_hash(instance, hash, bucket = nil)
|
29
|
+
data, is_new = get_data_and_is_new instance
|
30
|
+
data[bucket.to_s] = hash
|
31
|
+
|
32
|
+
store_value instance, is_new[bucket.to_s], dump_json(hash), bucket
|
33
|
+
|
34
|
+
self.set_data_and_is_new instance, data, is_new
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.store_value(instance, is_new, value, bucket = nil)
|
38
|
+
key = instance.send(:storage_key, bucket)
|
39
|
+
time = instance.class.storage_expiry(bucket)
|
40
|
+
if time == Frivol::NEVER_EXPIRE
|
41
|
+
Frivol::Config.redis[key] = value
|
42
|
+
else
|
43
|
+
Frivol::Config.redis.multi do |redis|
|
44
|
+
# TODO: write test for the to_i bug fix
|
45
|
+
time = redis.ttl(key).to_i unless is_new
|
46
|
+
redis[key] = value
|
47
|
+
redis.expire(key, time)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.retrieve_hash(instance, bucket = nil)
|
53
|
+
data, is_new = get_data_and_is_new instance
|
54
|
+
return data[bucket.to_s] if data.key?(bucket.to_s)
|
55
|
+
key = instance.send(:storage_key, bucket)
|
56
|
+
json = Frivol::Config.redis[key]
|
57
|
+
|
58
|
+
is_new[bucket.to_s] = json.nil?
|
59
|
+
|
60
|
+
hash = json.nil? ? {} : load_json(json)
|
61
|
+
data[bucket.to_s] = hash
|
62
|
+
|
63
|
+
self.set_data_and_is_new instance, data, is_new
|
64
|
+
hash
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.delete_hash(instance, bucket = nil)
|
68
|
+
key = instance.send(:storage_key, bucket)
|
69
|
+
Frivol::Config.redis.del key
|
70
|
+
clear_hash(instance, bucket)
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.clear_hash(instance, bucket = nil)
|
74
|
+
key = instance.send(:storage_key, bucket)
|
75
|
+
data = instance.instance_variable_defined?(:@frivol_data) ? instance.instance_variable_get(:@frivol_data) : {}
|
76
|
+
data.delete(bucket.to_s)
|
77
|
+
instance.instance_variable_set :@frivol_data, data
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.get_data_and_is_new(instance)
|
81
|
+
data = instance.instance_variable_defined?(:@frivol_data) ? instance.instance_variable_get(:@frivol_data) : {}
|
82
|
+
is_new = instance.instance_variable_defined?(:@frivol_is_new) ? instance.instance_variable_get(:@frivol_is_new) : {}
|
83
|
+
[data, is_new]
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.set_data_and_is_new(instance, data, is_new)
|
87
|
+
instance.instance_variable_set :@frivol_data, data
|
88
|
+
instance.instance_variable_set :@frivol_is_new, is_new
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.store_counter(instance, counter, value)
|
92
|
+
key = instance.send(:storage_key, counter)
|
93
|
+
is_new = !Frivol::Config.redis.exists(key)
|
94
|
+
store_value instance, is_new, value, counter
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.retrieve_counter(instance, counter, default)
|
98
|
+
key = instance.send(:storage_key, counter)
|
99
|
+
(Frivol::Config.redis[key] || default).to_i
|
100
|
+
end
|
101
|
+
|
102
|
+
def self.increment_counter(instance, counter, seed_callback=nil)
|
103
|
+
key = instance.send(:storage_key, counter)
|
104
|
+
store_counter_seed_value(key, instance, counter, seed_callback)
|
105
|
+
Frivol::Config.redis.incr(key)
|
106
|
+
end
|
107
|
+
|
108
|
+
def self.increment_counter_by(instance, counter, amount, seed_callback=nil)
|
109
|
+
key = instance.send(:storage_key, counter)
|
110
|
+
store_counter_seed_value(key, instance, counter, seed_callback)
|
111
|
+
Frivol::Config.redis.incrby(key, amount)
|
112
|
+
end
|
113
|
+
|
114
|
+
def self.decrement_counter(instance, counter, seed_callback=nil)
|
115
|
+
key = instance.send(:storage_key, counter)
|
116
|
+
store_counter_seed_value(key, instance, counter, seed_callback)
|
117
|
+
Frivol::Config.redis.decr(key)
|
118
|
+
end
|
119
|
+
|
120
|
+
def self.decrement_counter_by(instance, counter, amount, seed_callback=nil)
|
121
|
+
key = instance.send(:storage_key, counter)
|
122
|
+
store_counter_seed_value(key, instance, counter, seed_callback)
|
123
|
+
Frivol::Config.redis.decrby(key, amount)
|
124
|
+
end
|
125
|
+
|
126
|
+
def self.store_counter_seed_value(key, instance, counter, seed_callback)
|
127
|
+
unless Frivol::Config.redis.exists(key) || seed_callback.nil?
|
128
|
+
store_counter( instance, counter, seed_callback.call(instance))
|
129
|
+
end
|
130
|
+
end
|
131
|
+
private_class_method :store_counter_seed_value
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
# == Time
|
4
|
+
# An extension to the <tt>Time</tt> class which allows instances to be
|
5
|
+
# serialized by <tt>MultiJson#dump</tt> and deserialized by
|
6
|
+
# <tt>MultiJson#load</tt>.
|
7
|
+
class Time
|
8
|
+
# Serialize to JSON
|
9
|
+
def to_json(*a)
|
10
|
+
MultiJson.dump(
|
11
|
+
'json_class' => self.class.name,
|
12
|
+
'data' => self.to_s
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Deserialize from JSON
|
17
|
+
def self.json_create(o)
|
18
|
+
Time.parse(*o['data'])
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
Frivol::Config.allow_json_create << Time
|
23
|
+
|
24
|
+
begin
|
25
|
+
# == ActiveSupport::TimeWithZone
|
26
|
+
# An extension to the <tt>ActiveSupport::TimeWithZone</tt> class which allows
|
27
|
+
# instances to be serialized by <tt>MultiJson#dump</tt> and deserialized by
|
28
|
+
# <tt>MultiJson#load</tt>.
|
29
|
+
class ActiveSupport::TimeWithZone
|
30
|
+
# Serialize to JSON
|
31
|
+
def to_json(*a)
|
32
|
+
MultiJson.dump(
|
33
|
+
'json_class' => self.class.name,
|
34
|
+
'data' => self.to_s
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Deserialize from JSON
|
39
|
+
def self.json_create(o)
|
40
|
+
Time.zone.parse(*o['data'])
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
Frivol::Config.allow_json_create << ActiveSupport::TimeWithZone
|
45
|
+
rescue; end
|