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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 2a24e083670901a1e0114ec42a4da90f666ef23f
|
4
|
+
data.tar.gz: ce84cd346fde8137c233c18feb0e13858c1c5f41
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d5bfd4d1406e56a839615cca1577f31fc900d4a8f23e2ca788bbcf0d2b5ea6507fc63f635a8db9de734a8a81083973ec0d8c1670338e0b66fd6c62805765b27e
|
7
|
+
data.tar.gz: f641d253e43ae8fe48ddb2bde13940289423bf877302c61d72b6d8b490feedbb1d84b2e3dc794dd03bcc0f85046979c3ee54e362cbb92390cb6c457bad5244fd
|
data/.travis.yml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
language: ruby
|
2
|
+
bundler_args: --without development
|
3
|
+
before_install:
|
4
|
+
- 'echo ''gem: --no-ri --no-rdoc'' > ~/.gemrc'
|
5
|
+
rvm:
|
6
|
+
- 1.8.7
|
7
|
+
- 1.9.2
|
8
|
+
- 1.9.3
|
9
|
+
- 2.0.0
|
10
|
+
# - 2.1.0
|
11
|
+
- jruby-18mode
|
12
|
+
- jruby-19mode
|
13
|
+
- ruby-head
|
14
|
+
- jruby-head
|
15
|
+
- ree
|
data/Gemfile
ADDED
data/LICENSE
CHANGED
data/README.rdoc
CHANGED
@@ -1,80 +1,100 @@
|
|
1
1
|
= Frivol - Frivolously simple temporary storage backed by Redis
|
2
2
|
A really simple Redis-backed temporary storage mechanism intended to be used with ActiveRecord,
|
3
|
-
but will work with other ORM's or any classes really.
|
4
|
-
|
5
|
-
|
6
|
-
of
|
7
|
-
with communication of status from background tasks running in Resque on workers to the front
|
3
|
+
but will work with other ORM's or any classes really.
|
4
|
+
I developed Frivol secifically for use in Mad Mimi (http://madmimi.com) to help with caching
|
5
|
+
of data which requires fairly long running (multi-second) database queries, and also to aid
|
6
|
+
with communication of status from background Resque jobs running on the workers to the front
|
8
7
|
end web servers. Redis was chosen because we already had Resque, which is Redis-backed. Also,
|
9
8
|
unlike memcached, Redis persists it's data to disk, meaning there is far less warmup required
|
10
|
-
when a hot system is restarted. Frivol's design is such that it solves our problem, but I
|
11
|
-
believe it is generic enough to be used in many Rails web projects and even in other types of
|
12
|
-
projects altogether.
|
9
|
+
when a hot system is restarted. Frivol's design is such that it solves our problem, but I
|
10
|
+
believe it is generic enough to be used in many Rails web projects and even in other types of
|
11
|
+
projects altogether.
|
13
12
|
|
14
13
|
== Usage
|
15
14
|
Configure Frivol in your configuration, for example in an initializer or in environment.rb
|
16
15
|
REDIS_CONFIG = {
|
17
|
-
:host => "localhost",
|
16
|
+
:host => "localhost",
|
18
17
|
:port => 6379
|
19
18
|
}
|
20
19
|
Frivol::Config.redis_config = REDIS_CONFIG
|
20
|
+
|
21
21
|
Now include Frivol in whichever classes you'd like to make use of temporary storage. You can optionally
|
22
|
-
call the <tt>storage_expires_in(time)</tt> class method to set a default expiry. In your methods you can
|
22
|
+
call the <tt>storage_expires_in(time)</tt> class method to set a default expiry. In your methods you can
|
23
23
|
now call the <tt>store(keys_and_values)</tt> and <tt>retrieve(keys_and_defaults)</tt> methods.
|
24
|
-
|
25
24
|
Defaults in the +retrieve+ method can be symbols, in which case Frivol will check if the class <tt>respond_to?</tt>
|
26
25
|
a method by that name to get the default.
|
27
26
|
|
28
|
-
The <tt>expire_storage(time)</tt> method can be used to set the expiry time in seconds of the temporary storage.
|
27
|
+
The <tt>expire_storage(time)</tt> method can be used to set the expiry time in seconds of the temporary storage.
|
29
28
|
The default is not to expire the storage, in which case it will live for as long as Redis keeps it.
|
29
|
+
<tt>delete_storage</tt>, as the name suggests will immediately delete the storage, while <tt>clear_storage</tt>
|
30
|
+
will clear the cache that Frivol keeps and force the next <tt>retrieve</tt> to return to Redis for the data.
|
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.
|
30
34
|
|
35
|
+
Buckets can have their own expiry time and there are special counter buckets which simply keep an integer count.
|
36
|
+
storage_bucket :my_bucket, :expires_in => 5.minutes
|
37
|
+
storage_bucket :my_counter, :counter => true
|
38
|
+
|
39
|
+
Given the above, Frivol will create <tt>store_my_bucket</tt> and <tt>retrieve_my_bucket</tt> methods which work
|
40
|
+
exactly like the standard +store+ and +retrieve+ methods. There will also be <tt>store_my_counter</tt>,
|
41
|
+
<tt>retrieve_my_counter</tt> and <tt>increment_my_counter</tt> methods. The counter store and retrieve only
|
42
|
+
take a integer (value and default, respectively) and the increment does not take a parameter. Since version 0.2.1
|
43
|
+
there is also <tt>increment_my_counter_by</tt>, <tt>decrement_my_counter</tt> and <tt>decrement_my_counter_by<tt>.
|
44
|
+
|
45
|
+
Fine grained control of storing and retrieving values from buckets can be controlled using the :condition and
|
46
|
+
:else options.
|
47
|
+
storage_bucket :my_bucket,
|
48
|
+
:condition => Proc.new{ |object, frivol_method, *args| ... },
|
49
|
+
:else => :your_method
|
50
|
+
For the above example, frivol execute the :condition proc and passes the instance of the current class, which
|
51
|
+
method is being attempted (increment, increment_by, store, retrieve, etc.) and any arguments that may have been
|
52
|
+
passed on to frivol.
|
53
|
+
If the condition returns a truthy result, the frivol method is executed unimpeded, otherwise frivol moves on to
|
54
|
+
:else. :else for the above example is a method on the instance, and that method must be able to receive the frivol
|
55
|
+
method used (as a string) and any arguments passed to that method:
|
56
|
+
def your_method(frivol_method, *args)
|
57
|
+
...
|
58
|
+
end
|
59
|
+
The :condition and :else options can be specified as a proc, symbol, true or false.
|
31
60
|
Frivol uses the +storage_key+ method to create a base key for storage in Redis. The current implementation uses
|
32
61
|
<tt>"#{self.class.name}-#{id}"</tt> so you'll want to override that method if you have classes that don't
|
33
|
-
respond to id.
|
62
|
+
respond to id.
|
34
63
|
|
35
|
-
Frivol also extends Time to allow it to be (de)serialized to JSON, which currently used to store
|
36
|
-
data in Redis.
|
37
64
|
== Example
|
38
65
|
class BigComplexCalcer
|
39
66
|
include Frivol
|
40
67
|
storage_expires_in 600 # temporary storage expires in 10 minutes.
|
41
|
-
|
42
68
|
def initialize(key)
|
43
69
|
@key = key
|
44
70
|
end
|
45
|
-
|
46
|
-
|
47
|
-
"frivol-test-#{key}" # override the storage key because we don't respond_to? id
|
71
|
+
def storage_key(bucket = nil)
|
72
|
+
"frivol-test-#{key}" # override the storage key because we don't respond_to? :id, and don't care about buckets
|
48
73
|
end
|
49
|
-
|
50
74
|
def big_complex_calc
|
51
75
|
retrieve :complex => :do_big_complex_calc # do_big_complex_calc is the method to get the default from
|
52
76
|
end
|
53
|
-
|
54
77
|
def last_calc_done
|
55
|
-
last = retrieve
|
78
|
+
last = retrieve(:last => nil) # default is nil
|
56
79
|
return "never" if last.nil?
|
57
|
-
return "#{Time.now - last} seconds ago"
|
80
|
+
return "#{Time.now - Time.at(last)} seconds ago"
|
58
81
|
end
|
59
|
-
|
60
82
|
def do_big_complex_calc
|
61
83
|
# Wee! Do some really hard work here...
|
62
84
|
# ...still working...
|
63
|
-
store :complex => result, :last => Time.now # ...and let's keep the result for at least 10 minutes, as well as the last
|
85
|
+
store :complex => result, :last => Time.now.to_i # ...and let's keep the result for at least 10 minutes, as well as the last me we did it
|
64
86
|
end
|
65
87
|
end
|
66
88
|
|
89
|
+
== Time Extensions
|
90
|
+
These extensions allow the storing and retrieving of <tt>Time</tt> and
|
91
|
+
<tt>ActiveSupport::TimeWithZone</tt> objects in Frivol. We now recommend that
|
92
|
+
times are stored using <tt>#to_i</tt>, but the extensions are provided for
|
93
|
+
legacy Frivol users. In order to use them you will need to have:
|
94
|
+
require 'frivol/extensions'
|
95
|
+
You can also create your own extensions to save other complex objects in
|
96
|
+
Frivol. Your classes will need a <tt>#to_json(*a)</tt> to dump the object
|
97
|
+
and a <tt>.json_create(o)</tt> class method to load the object. You will need to
|
98
|
+
tell <tt>Frivol</tt> to allow the use of create extensions for your class with:
|
99
|
+
Frivol::Config.allow_json_create << Klass
|
67
100
|
|
68
|
-
== Note on Patches/Pull Requests
|
69
|
-
|
70
|
-
* Fork the project.
|
71
|
-
* Make your feature addition or bug fix.
|
72
|
-
* Add tests for it. This is important so I don't break it in a
|
73
|
-
future version unintentionally.
|
74
|
-
* Commit, do not mess with rakefile, version, or history.
|
75
|
-
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
76
|
-
* Send me a pull request. Bonus points for topic branches.
|
77
|
-
|
78
|
-
== Copyright
|
79
|
-
|
80
|
-
Copyright (c) 2010 Marc Heiligers. See LICENSE for details.
|
data/Rakefile
CHANGED
@@ -10,9 +10,6 @@ begin
|
|
10
10
|
gem.email = "marc@eternal.co.za"
|
11
11
|
gem.homepage = "http://github.com/marcheiligers/frivol"
|
12
12
|
gem.authors = ["Marc Heiligers"]
|
13
|
-
gem.add_dependency "json", ">= 1.2.0"
|
14
|
-
gem.add_dependency "redis", ">= 2.0.10"
|
15
|
-
gem.add_development_dependency "shoulda", ">= 2.11.1"
|
16
13
|
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
17
14
|
end
|
18
15
|
Jeweler::GemcutterTasks.new
|
@@ -22,34 +19,27 @@ end
|
|
22
19
|
|
23
20
|
require 'rake/testtask'
|
24
21
|
Rake::TestTask.new(:test) do |test|
|
25
|
-
test.libs << '
|
22
|
+
test.libs << 'test'
|
26
23
|
test.pattern = 'test/**/test_*.rb'
|
27
24
|
test.verbose = true
|
28
25
|
end
|
29
26
|
|
30
|
-
begin
|
31
|
-
require 'rcov/rcovtask'
|
32
|
-
Rcov::RcovTask.new do |test|
|
33
|
-
test.libs << 'test'
|
34
|
-
test.pattern = 'test/**/test_*.rb'
|
35
|
-
test.verbose = true
|
36
|
-
end
|
37
|
-
rescue LoadError
|
38
|
-
task :rcov do
|
39
|
-
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
task :test => :check_dependencies
|
44
|
-
|
45
27
|
task :default => :test
|
46
28
|
|
47
|
-
|
48
|
-
|
49
|
-
|
29
|
+
begin
|
30
|
+
require 'rdoc/task'
|
31
|
+
Rake::RDocTask.new do |rdoc|
|
32
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
50
33
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
34
|
+
rdoc.rdoc_dir = 'rdoc'
|
35
|
+
rdoc.title = "frivol #{version}"
|
36
|
+
rdoc.rdoc_files.include('README*')
|
37
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
38
|
+
end
|
39
|
+
rescue LoadError, RuntimeError => e
|
40
|
+
if e.is_a?(LoadError) || (e.is_a?(RuntimeError) && e.message.start_with?("ERROR: 'rake/rdoctask'"))
|
41
|
+
puts "RDocTask (or a dependency) not available. Maybe older Ruby?"
|
42
|
+
else
|
43
|
+
raise e
|
44
|
+
end
|
55
45
|
end
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.3.0
|
data/frivol.gemspec
CHANGED
@@ -1,87 +1,99 @@
|
|
1
1
|
# Generated by jeweler
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
-
# Instead, edit Jeweler::Tasks in
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
4
|
# -*- encoding: utf-8 -*-
|
5
|
+
# stub: frivol 0.3.0 ruby lib
|
5
6
|
|
6
7
|
Gem::Specification.new do |s|
|
7
|
-
s.name =
|
8
|
-
s.version = "0.
|
8
|
+
s.name = "frivol"
|
9
|
+
s.version = "0.3.0"
|
9
10
|
|
10
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
12
|
+
s.require_paths = ["lib"]
|
11
13
|
s.authors = ["Marc Heiligers"]
|
12
|
-
s.date =
|
13
|
-
s.description =
|
14
|
-
s.email =
|
14
|
+
s.date = "2014-03-06"
|
15
|
+
s.description = "Simple Redis backed temporary storage intended primarily for use with ActiveRecord models to provide caching"
|
16
|
+
s.email = "marc@eternal.co.za"
|
15
17
|
s.extra_rdoc_files = [
|
16
18
|
"LICENSE",
|
17
|
-
|
19
|
+
"README.rdoc"
|
18
20
|
]
|
19
21
|
s.files = [
|
20
22
|
".document",
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
s.require_paths = ["lib"]
|
60
|
-
s.rubygems_version = %q{1.3.7}
|
61
|
-
s.summary = %q{Simple Redis backed temporary storage}
|
62
|
-
s.test_files = [
|
23
|
+
".travis.yml",
|
24
|
+
"Gemfile",
|
25
|
+
"LICENSE",
|
26
|
+
"README.rdoc",
|
27
|
+
"Rakefile",
|
28
|
+
"VERSION",
|
29
|
+
"doc/classes/Frivol.html",
|
30
|
+
"doc/classes/Frivol.src/M000003.html",
|
31
|
+
"doc/classes/Frivol.src/M000004.html",
|
32
|
+
"doc/classes/Frivol.src/M000005.html",
|
33
|
+
"doc/classes/Frivol.src/M000006.html",
|
34
|
+
"doc/classes/Frivol.src/M000007.html",
|
35
|
+
"doc/classes/Frivol.src/M000008.html",
|
36
|
+
"doc/classes/Frivol/ClassMethods.html",
|
37
|
+
"doc/classes/Frivol/ClassMethods.src/M000009.html",
|
38
|
+
"doc/classes/Frivol/ClassMethods.src/M000010.html",
|
39
|
+
"doc/classes/Frivol/ClassMethods.src/M000011.html",
|
40
|
+
"doc/classes/Frivol/Config.html",
|
41
|
+
"doc/classes/Frivol/Config.src/M000012.html",
|
42
|
+
"doc/classes/Frivol/Config.src/M000013.html",
|
43
|
+
"doc/classes/Frivol/Config.src/M000014.html",
|
44
|
+
"doc/classes/Time.html",
|
45
|
+
"doc/classes/Time.src/M000001.html",
|
46
|
+
"doc/classes/Time.src/M000002.html",
|
47
|
+
"doc/created.rid",
|
48
|
+
"doc/files/lib/frivol_rb.html",
|
49
|
+
"doc/fr_class_index.html",
|
50
|
+
"doc/fr_file_index.html",
|
51
|
+
"doc/fr_method_index.html",
|
52
|
+
"doc/index.html",
|
53
|
+
"doc/rdoc-style.css",
|
54
|
+
"frivol.gemspec",
|
55
|
+
"lib/frivol.rb",
|
56
|
+
"lib/frivol/class_methods.rb",
|
57
|
+
"lib/frivol/config.rb",
|
58
|
+
"lib/frivol/functor.rb",
|
59
|
+
"lib/frivol/helpers.rb",
|
60
|
+
"lib/frivol/time_extensions.rb",
|
63
61
|
"test/fake_redis.rb",
|
64
|
-
|
65
|
-
|
62
|
+
"test/helper.rb",
|
63
|
+
"test/test_buckets.rb",
|
64
|
+
"test/test_condition.rb",
|
65
|
+
"test/test_condition_with_counters.rb",
|
66
|
+
"test/test_counters.rb",
|
67
|
+
"test/test_else_with_counters.rb",
|
68
|
+
"test/test_extensions.rb",
|
69
|
+
"test/test_frivol.rb",
|
70
|
+
"test/test_frivolize.rb",
|
71
|
+
"test/test_seeds.rb",
|
72
|
+
"test/test_threads.rb"
|
66
73
|
]
|
74
|
+
s.homepage = "http://github.com/marcheiligers/frivol"
|
75
|
+
s.rubygems_version = "2.2.1"
|
76
|
+
s.summary = "Simple Redis backed temporary storage"
|
67
77
|
|
68
78
|
if s.respond_to? :specification_version then
|
69
|
-
|
70
|
-
s.specification_version = 3
|
79
|
+
s.specification_version = 4
|
71
80
|
|
72
81
|
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
73
|
-
s.add_runtime_dependency(%q<
|
74
|
-
s.add_runtime_dependency(%q<redis>, [">=
|
75
|
-
s.
|
82
|
+
s.add_runtime_dependency(%q<multi_json>, [">= 0"])
|
83
|
+
s.add_runtime_dependency(%q<redis>, [">= 0"])
|
84
|
+
s.add_runtime_dependency(%q<rake>, [">= 0"])
|
85
|
+
s.add_development_dependency(%q<jeweler>, [">= 0"])
|
76
86
|
else
|
77
|
-
s.add_dependency(%q<
|
78
|
-
s.add_dependency(%q<redis>, [">=
|
79
|
-
s.add_dependency(%q<
|
87
|
+
s.add_dependency(%q<multi_json>, [">= 0"])
|
88
|
+
s.add_dependency(%q<redis>, [">= 0"])
|
89
|
+
s.add_dependency(%q<rake>, [">= 0"])
|
90
|
+
s.add_dependency(%q<jeweler>, [">= 0"])
|
80
91
|
end
|
81
92
|
else
|
82
|
-
s.add_dependency(%q<
|
83
|
-
s.add_dependency(%q<redis>, [">=
|
84
|
-
s.add_dependency(%q<
|
93
|
+
s.add_dependency(%q<multi_json>, [">= 0"])
|
94
|
+
s.add_dependency(%q<redis>, [">= 0"])
|
95
|
+
s.add_dependency(%q<rake>, [">= 0"])
|
96
|
+
s.add_dependency(%q<jeweler>, [">= 0"])
|
85
97
|
end
|
86
98
|
end
|
87
99
|
|
data/lib/frivol.rb
CHANGED
@@ -1,93 +1,15 @@
|
|
1
|
-
# = Frivol - Frivolously simple temporary storage backed by Redis
|
2
|
-
# A really simple Redis-backed temporary storage mechanism intended to be used with ActiveRecord,
|
3
|
-
# but will work with other ORM's or any classes really.
|
4
|
-
#
|
5
|
-
# I developed Frivol secifically for use in Mad Mimi (http://madmimi.com) to help with caching
|
6
|
-
# of data which requires fairly long running (multi-second) database queries, and also to aid
|
7
|
-
# with communication of status from background Resque jobs running on the workers to the front
|
8
|
-
# end web servers. Redis was chosen because we already had Resque, which is Redis-backed. Also,
|
9
|
-
# unlike memcached, Redis persists it's data to disk, meaning there is far less warmup required
|
10
|
-
# when a hot system is restarted. Frivol's design is such that it solves our problem, but I
|
11
|
-
# believe it is generic enough to be used in many Rails web projects and even in other types of
|
12
|
-
# projects altogether.
|
13
|
-
#
|
14
|
-
# == Usage
|
15
|
-
# Configure Frivol in your configuration, for example in an initializer or in environment.rb
|
16
|
-
# REDIS_CONFIG = {
|
17
|
-
# :host => "localhost",
|
18
|
-
# :port => 6379
|
19
|
-
# }
|
20
|
-
# Frivol::Config.redis_config = REDIS_CONFIG
|
21
|
-
# Now include Frivol in whichever classes you'd like to make use of temporary storage. You can optionally
|
22
|
-
# call the <tt>storage_expires_in(time)</tt> class method to set a default expiry. In your methods you can
|
23
|
-
# now call the <tt>store(keys_and_values)</tt> and <tt>retrieve(keys_and_defaults)</tt> methods.
|
24
|
-
#
|
25
|
-
# Defaults in the +retrieve+ method can be symbols, in which case Frivol will check if the class <tt>respond_to?</tt>
|
26
|
-
# a method by that name to get the default.
|
27
|
-
#
|
28
|
-
# The <tt>expire_storage(time)</tt> method can be used to set the expiry time in seconds of the temporary storage.
|
29
|
-
# The default is not to expire the storage, in which case it will live for as long as Redis keeps it.
|
30
|
-
# <tt>delete_storage</tt>, as the name suggests will immediately delete the storage, while <tt>clear_storage</tt>
|
31
|
-
# will clear the cache that Frivol keeps and force the next <tt>retrieve</tt> to return to Redis for the data.
|
32
|
-
#
|
33
|
-
# Since version 0.1.5 Frivol can create different storage buckets. Note that this introduces a breaking change
|
34
|
-
# to the <tt>storage_key</tt> method if you have overriden it. It now takes a +bucket+ parameter.
|
35
|
-
#
|
36
|
-
# Buckets can have their own expiry time and there are special counter buckets which simply keep an integer count.
|
37
|
-
#
|
38
|
-
# storage_bucket :my_bucket, :expires_in => 5.minutes
|
39
|
-
# storage_bucket :my_counter, :counter => true
|
40
|
-
#
|
41
|
-
# Given the above, Frivol will create <tt>store_my_bucket</tt> and <tt>retrieve_my_bucket</tt> methods which work
|
42
|
-
# exactly like the standard +store+ and +retrieve+ methods. There will also be <tt>store_my_counter</tt>,
|
43
|
-
# <tt>retrieve_my_counter</tt> and <tt>increment_my_counter</tt> methods. The counter store and retrieve only
|
44
|
-
# take a integer (value and default, respectively) and the increment does not take a parameter.
|
45
|
-
#
|
46
|
-
# These methods are thread safe if you pass <tt>:thread_safe => true</tt> to the Redis configuration.
|
47
|
-
#
|
48
|
-
# Frivol uses the +storage_key+ method to create a base key for storage in Redis. The current implementation uses
|
49
|
-
# <tt>"#{self.class.name}-#{id}"</tt> so you'll want to override that method if you have classes that don't
|
50
|
-
# respond to id.
|
51
|
-
#
|
52
|
-
# Frivol also extends Time to allow it to be (de)serialized to JSON, which currently used to store
|
53
|
-
# data in Redis.
|
54
|
-
# == Example
|
55
|
-
# class BigComplexCalcer
|
56
|
-
# include Frivol
|
57
|
-
# storage_expires_in 600 # temporary storage expires in 10 minutes.
|
58
|
-
#
|
59
|
-
# def initialize(key)
|
60
|
-
# @key = key
|
61
|
-
# end
|
62
|
-
#
|
63
|
-
# def storage_key(bucket = nil)
|
64
|
-
# "frivol-test-#{key}" # override the storage key because we don't respond_to? :id, and don't care about buckets
|
65
|
-
# end
|
66
|
-
#
|
67
|
-
# def big_complex_calc
|
68
|
-
# retrieve :complex => :do_big_complex_calc # do_big_complex_calc is the method to get the default from
|
69
|
-
# end
|
70
|
-
#
|
71
|
-
# def last_calc_done
|
72
|
-
# last = retrieve :last => nil # default is nil
|
73
|
-
# return "never" if last.nil?
|
74
|
-
# return "#{Time.now - last} seconds ago"
|
75
|
-
# end
|
76
|
-
#
|
77
|
-
# def do_big_complex_calc
|
78
|
-
# # Wee! Do some really hard work here...
|
79
|
-
# # ...still working...
|
80
|
-
# store :complex => result, :last => Time.now # ...and let's keep the result for at least 10 minutes, as well as the last time we did it
|
81
|
-
# end
|
82
|
-
# end
|
83
|
-
require "json"
|
84
1
|
require "redis"
|
85
2
|
|
86
3
|
# == Frivol
|
87
4
|
module Frivol
|
5
|
+
require "frivol/config"
|
6
|
+
require "frivol/functor"
|
7
|
+
require "frivol/helpers"
|
8
|
+
require "frivol/class_methods"
|
9
|
+
|
88
10
|
# Defines a constant to indicate that storage should never expire
|
89
11
|
NEVER_EXPIRE = nil
|
90
|
-
|
12
|
+
|
91
13
|
# Store a hash of keys and values.
|
92
14
|
#
|
93
15
|
# The hash need not be the complete hash of all things stored, just those you want to change.
|
@@ -102,10 +24,10 @@ module Frivol
|
|
102
24
|
end
|
103
25
|
Frivol::Helpers.store_hash(self, hash)
|
104
26
|
end
|
105
|
-
|
27
|
+
|
106
28
|
# Retrieve stored values, or defaults.
|
107
29
|
#
|
108
|
-
# If you retrieve a single key just that value is returned. If you retrieve multiple keys an array of
|
30
|
+
# If you retrieve a single key just that value is returned. If you retrieve multiple keys an array of
|
109
31
|
# values is returned. You might do:
|
110
32
|
# name = retrieve :name => "Marc Heiligers"
|
111
33
|
# first_name, last_name = retrieve :first_name => "Marc", :last_name => "Heiligers"
|
@@ -120,26 +42,26 @@ module Frivol
|
|
120
42
|
return result.first if result.size == 1
|
121
43
|
result
|
122
44
|
end
|
123
|
-
|
45
|
+
|
124
46
|
# Deletes the stored values (and clears the cache).
|
125
47
|
def delete_storage
|
126
48
|
Frivol::Helpers.delete_hash self
|
127
49
|
end
|
128
|
-
|
50
|
+
|
129
51
|
# Clears the cached values and forces the next retrieve to fetch from Redis.
|
130
52
|
def clear_storage
|
131
53
|
Frivol::Helpers.clear_hash self
|
132
54
|
end
|
133
|
-
|
55
|
+
|
134
56
|
# Expire the stored data in +time+ seconds.
|
135
57
|
def expire_storage(time, bucket = nil)
|
136
58
|
return if time.nil?
|
137
59
|
Frivol::Config.redis.expire storage_key(bucket), time
|
138
60
|
end
|
139
|
-
|
140
|
-
# The base key used for storage in Redis.
|
61
|
+
|
62
|
+
# The base key used for storage in Redis.
|
141
63
|
#
|
142
|
-
# This method has been implemented for use with ActiveRecord and uses <tt>"#{self.class.name}-#{id}"</tt>
|
64
|
+
# This method has been implemented for use with ActiveRecord and uses <tt>"#{self.class.name}-#{id}"</tt>
|
143
65
|
# for the default bucket and <tt>"#{self.class.name}-#{id}-#{bucket}"</tt> for a named bucket.
|
144
66
|
# If you are not using ActiveRecord, or using classes that don't respond to id, you should override
|
145
67
|
# this method in your class.
|
@@ -149,279 +71,8 @@ module Frivol
|
|
149
71
|
@frivol_key ||= "#{self.class.name}-#{id}"
|
150
72
|
bucket.nil? ? @frivol_key : "#{@frivol_key}-#{bucket}"
|
151
73
|
end
|
152
|
-
|
153
|
-
# == Frivol::Config
|
154
|
-
# Sets the Frivol configuration (currently only the Redis config), allows access to the configured Redis instance,
|
155
|
-
# and has a helper method to include Frivol in a class with an optional storage expiry parameter
|
156
|
-
module Config
|
157
|
-
# Set the Redis configuration.
|
158
|
-
#
|
159
|
-
# Expects a hash such as
|
160
|
-
# REDIS_CONFIG = {
|
161
|
-
# :host => "localhost",
|
162
|
-
# :port => 6379
|
163
|
-
# }
|
164
|
-
# Frivol::Config.redis_config = REDIS_CONFIG
|
165
|
-
def self.redis_config=(config)
|
166
|
-
@@redis = Redis.new(config)
|
167
|
-
end
|
168
|
-
|
169
|
-
# Returns the configured Redis instance
|
170
|
-
def self.redis
|
171
|
-
@@redis
|
172
|
-
end
|
173
|
-
|
174
|
-
# A convenience method to include Frivol in a class, with an optional storage expiry parameter.
|
175
|
-
#
|
176
|
-
# For example, you might have the following in environment.rb:
|
177
|
-
# Frivol::Config.redis_config = REDIS_CONFIG
|
178
|
-
# Frivol::Config.include_in ActiveRecord::Base, 600
|
179
|
-
# Which would include Frivol in ActiveRecord::Base and set the default storage expiry to 10 minutes
|
180
|
-
def self.include_in(host_class, storage_expires_in = nil)
|
181
|
-
host_class.send(:include, Frivol)
|
182
|
-
host_class.storage_expires_in storage_expires_in if storage_expires_in
|
183
|
-
end
|
184
|
-
end
|
185
|
-
|
186
|
-
module Helpers #:nodoc:
|
187
|
-
def self.store_hash(instance, hash, bucket = nil)
|
188
|
-
data, is_new = get_data_and_is_new instance
|
189
|
-
data[bucket.to_s] = hash
|
190
|
-
|
191
|
-
store_value instance, is_new[bucket.to_s], hash.to_json, bucket
|
192
|
-
|
193
|
-
self.set_data_and_is_new instance, data, is_new
|
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
|
209
|
-
|
210
|
-
def self.retrieve_hash(instance, bucket = nil)
|
211
|
-
data, is_new = get_data_and_is_new instance
|
212
|
-
return data[bucket.to_s] if data.key?(bucket.to_s)
|
213
|
-
key = instance.send(:storage_key, bucket)
|
214
|
-
json = Frivol::Config.redis[key]
|
215
|
-
|
216
|
-
is_new[bucket.to_s] = json.nil?
|
217
|
-
|
218
|
-
hash = json.nil? ? {} : JSON.parse(json)
|
219
|
-
data[bucket.to_s] = hash
|
220
|
-
|
221
|
-
self.set_data_and_is_new instance, data, is_new
|
222
|
-
hash
|
223
|
-
end
|
224
|
-
|
225
|
-
def self.delete_hash(instance, bucket = nil)
|
226
|
-
key = instance.send(:storage_key, bucket)
|
227
|
-
Frivol::Config.redis.del key
|
228
|
-
clear_hash(instance, bucket)
|
229
|
-
end
|
230
|
-
|
231
|
-
def self.clear_hash(instance, bucket = nil)
|
232
|
-
key = instance.send(:storage_key, bucket)
|
233
|
-
data = instance.instance_variable_defined?(:@frivol_data) ? instance.instance_variable_get(:@frivol_data) : {}
|
234
|
-
data.delete(bucket.to_s)
|
235
|
-
instance.instance_variable_set :@frivol_data, data
|
236
|
-
end
|
237
|
-
|
238
|
-
def self.get_data_and_is_new(instance)
|
239
|
-
data = instance.instance_variable_defined?(:@frivol_data) ? instance.instance_variable_get(:@frivol_data) : {}
|
240
|
-
is_new = instance.instance_variable_defined?(:@frivol_is_new) ? instance.instance_variable_get(:@frivol_is_new) : {}
|
241
|
-
[data, is_new]
|
242
|
-
end
|
243
|
-
|
244
|
-
def self.set_data_and_is_new(instance, data, is_new)
|
245
|
-
instance.instance_variable_set :@frivol_data, data
|
246
|
-
instance.instance_variable_set :@frivol_is_new, is_new
|
247
|
-
end
|
248
|
-
|
249
|
-
def self.store_counter(instance, counter, value)
|
250
|
-
key = instance.send(:storage_key, counter)
|
251
|
-
is_new = !Frivol::Config.redis.exists(key)
|
252
|
-
store_value instance, is_new, value, counter
|
253
|
-
end
|
254
|
-
|
255
|
-
def self.retrieve_counter(instance, counter, default)
|
256
|
-
key = instance.send(:storage_key, counter)
|
257
|
-
(Frivol::Config.redis[key] || default).to_i
|
258
|
-
end
|
259
|
-
|
260
|
-
def self.increment_counter(instance, counter)
|
261
|
-
key = instance.send(:storage_key, counter)
|
262
|
-
Frivol::Config.redis.incr(key)
|
263
|
-
end
|
264
|
-
end
|
265
|
-
|
266
|
-
# == Frivol::ClassMethods
|
267
|
-
# These methods are available on the class level when Frivol is included in the class.
|
268
|
-
module ClassMethods
|
269
|
-
# Set the storage expiry time in seconds for the default bucket or the bucket passed.
|
270
|
-
def storage_expires_in(time, bucket = nil)
|
271
|
-
@frivol_storage_expiry ||= {}
|
272
|
-
@frivol_storage_expiry[bucket.to_s] = time
|
273
|
-
end
|
274
|
-
|
275
|
-
# Get the storage expiry time in seconds for the default bucket or the bucket passed.
|
276
|
-
def storage_expiry(bucket = nil)
|
277
|
-
@frivol_storage_expiry ||= {}
|
278
|
-
@frivol_storage_expiry.key?(bucket.to_s) ? @frivol_storage_expiry[bucket.to_s] : NEVER_EXPIRE
|
279
|
-
end
|
280
|
-
|
281
|
-
# Create a storage bucket.
|
282
|
-
# Frivol creates store_#{bucket} and retrieve_#{bucket} methods automatically.
|
283
|
-
# These methods work exactly like the default store and retrieve methods except that the bucket is
|
284
|
-
# stored in it's own key in Redis and can have it's own expiry time.
|
285
|
-
#
|
286
|
-
# Counters are special in that they do not store a hash but only a single integer value and also
|
287
|
-
# that the data in a counter is not cached for the lifespan of the object, but rather each call
|
288
|
-
# hits Redis. This is intended to make counters thread safe (for example you may have multiple
|
289
|
-
# workers working on a job and they can each increment a progress counter which would not work
|
290
|
-
# with the default retrieve/store method that normal buckets use). For this to actually be thread safe
|
291
|
-
# you need to pass the thread safe option to the config when you make the connection.
|
292
|
-
#
|
293
|
-
# In the case of a counter, the methods work slightly differently:
|
294
|
-
# - store_#{bucket} only takes an integer value to store (no key)
|
295
|
-
# - retrieve_#{bucket} only takes an integer default, and returns only the integer value
|
296
|
-
# - there is an added increment_#{bucket} method which increments the counter by 1
|
297
|
-
#
|
298
|
-
# Options are :expires_in which sets the expiry time for a bucket,
|
299
|
-
# and :counter to create a special counter storage bucket.
|
300
|
-
def storage_bucket(bucket, options = {})
|
301
|
-
time = options[:expires_in]
|
302
|
-
storage_expires_in(time, bucket) if !time.nil?
|
303
|
-
is_counter = options[:counter]
|
304
|
-
|
305
|
-
self.class_eval do
|
306
|
-
if is_counter
|
307
|
-
define_method "store_#{bucket}" do |value|
|
308
|
-
Frivol::Helpers.store_counter(self, bucket, value)
|
309
|
-
end
|
310
|
-
|
311
|
-
define_method "retrieve_#{bucket}" do |default|
|
312
|
-
Frivol::Helpers.retrieve_counter(self, bucket, default)
|
313
|
-
end
|
314
|
-
|
315
|
-
define_method "increment_#{bucket}" do
|
316
|
-
Frivol::Helpers.increment_counter(self, bucket)
|
317
|
-
end
|
318
|
-
else
|
319
|
-
define_method "store_#{bucket}" do |keys_and_values|
|
320
|
-
hash = Frivol::Helpers.retrieve_hash(self, bucket)
|
321
|
-
keys_and_values.each do |key, value|
|
322
|
-
hash[key.to_s] = value
|
323
|
-
end
|
324
|
-
Frivol::Helpers.store_hash(self, hash, bucket)
|
325
|
-
end
|
326
|
-
|
327
|
-
define_method "retrieve_#{bucket}" do |keys_and_defaults|
|
328
|
-
hash = Frivol::Helpers.retrieve_hash(self, bucket)
|
329
|
-
result = keys_and_defaults.map do |key, default|
|
330
|
-
hash[key.to_s] || (default.is_a?(Symbol) && respond_to?(default) && send(default)) || default
|
331
|
-
end
|
332
|
-
return result.first if result.size == 1
|
333
|
-
result
|
334
|
-
end
|
335
|
-
end
|
336
|
-
|
337
|
-
define_method "delete_#{bucket}" do
|
338
|
-
Frivol::Helpers.delete_hash(self, bucket)
|
339
|
-
end
|
340
|
-
|
341
|
-
define_method "clear_#{bucket}" do
|
342
|
-
Frivol::Helpers.clear_hash(self, bucket)
|
343
|
-
end
|
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
74
|
|
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
|
399
|
-
end
|
400
|
-
|
401
|
-
# def storage_default(keys_and_defaults)
|
402
|
-
# @frivol_defaults ||= {}
|
403
|
-
# @frivol_defaults.merge keys_and_defaults
|
404
|
-
# end
|
405
|
-
end
|
406
|
-
|
407
75
|
def self.included(host) #:nodoc:
|
408
76
|
host.extend(ClassMethods)
|
409
77
|
end
|
410
78
|
end
|
411
|
-
|
412
|
-
# == Time
|
413
|
-
# An extension to the Time class which allows Time instances to be serialized by <tt>#to_json</tt> and deserialized by <tt>JSON#parse</tt>.
|
414
|
-
class Time
|
415
|
-
# Serialize to JSON
|
416
|
-
def to_json(*a)
|
417
|
-
{
|
418
|
-
'json_class' => self.class.name,
|
419
|
-
'data' => self.to_s
|
420
|
-
}.to_json(*a)
|
421
|
-
end
|
422
|
-
|
423
|
-
# Deserialize from JSON
|
424
|
-
def self.json_create(o)
|
425
|
-
Time.parse(*o['data'])
|
426
|
-
end
|
427
|
-
end
|