grosser-cachy 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.markdown +108 -0
- data/Rakefile +21 -0
- data/VERSION +1 -0
- data/cachy.gemspec +51 -0
- data/lib/cachy/memcached_wrapper.rb +8 -0
- data/lib/cachy/moneta_wrapper.rb +8 -0
- data/lib/cachy/wrapper.rb +17 -0
- data/lib/cachy.rb +193 -0
- data/spec/cachy/memcached_wrapper_spec.rb +63 -0
- data/spec/cachy/moneta_wrapper_spec.rb +59 -0
- data/spec/cachy_spec.rb +231 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/test_cache.rb +25 -0
- metadata +69 -0
data/README.markdown
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
Caching library to simplify and organize caching.
|
2
|
+
|
3
|
+
- works out of the box with Rails
|
4
|
+
- works with pure Memcache and [Moneta](http://github.com/wycats/moneta/tree/master)(-> Tokyo Cabinet / CouchDB / S3 / Berkeley DB / DataMapper / Memory store)
|
5
|
+
|
6
|
+
Install
|
7
|
+
=======
|
8
|
+
As Gem: ` sudo gem install grosser-cachy -s http://gems.github.com `
|
9
|
+
Or as Rails plugin: ` script/plugins install git://github.com/grosser/cachy.git `
|
10
|
+
|
11
|
+
|
12
|
+
Usage
|
13
|
+
=====
|
14
|
+
###Cachy.cache
|
15
|
+
result = Cachy.cache(:a_key){ expensive() }
|
16
|
+
result = Cachy.cache(:a_key, :expires_in => 1.minute){ expensive() }
|
17
|
+
result = Cachy.cache(:a_key, 'something else', Date.today.day){ expensive() }
|
18
|
+
|
19
|
+
####Cache expensive operation that is run many times by many processes
|
20
|
+
Example scenario: at application Startup 20 processes try to set the same cache -> 20 heavy database requests -> database timeout -> cache still empty -> ... -> death
|
21
|
+
|
22
|
+
# 19 Processes get [], 1 makes the request -- when cached all get the same result
|
23
|
+
result = Cachy.cache(:a_key, :while_running=>[]){ block_db_for_5_seconds }
|
24
|
+
|
25
|
+
|
26
|
+
####Seperate version for each key
|
27
|
+
Expire all all caches of one kind when e.g. codebase has been updated
|
28
|
+
|
29
|
+
100.times{ Cachy.cache(:a_key, rand(100000) ){ expensive() } }
|
30
|
+
Cachy.increment_key(:a_key) --> everything expired
|
31
|
+
|
32
|
+
|
33
|
+
####Uses I18n.locale if available
|
34
|
+
Cachy.cache(:a_key){ 'English' }
|
35
|
+
I18n.locale = :de
|
36
|
+
Cachy.cache(:a_key){ 'German' } != 'English'
|
37
|
+
|
38
|
+
####Explicitly not use I18n.locale
|
39
|
+
Cachy.cache(:a_key, :witout_locale=>true){ 'English' }
|
40
|
+
I18n.locale = :de
|
41
|
+
Cachy.cache(:a_key, :witout_locale=>true){ 'German' } == 'English'
|
42
|
+
|
43
|
+
####Caching results of other caches
|
44
|
+
When inner cache is expired outer cache would normally still show old results.
|
45
|
+
Exipre outer cache when inner cache is expired.
|
46
|
+
|
47
|
+
a = Cachy.cache(:a, :expires_in=>1.day){ expensive() }
|
48
|
+
b = Cachy.cache(:b, :expires_in=>1.week){ expensive_2() }
|
49
|
+
Cachy.cache(:surrounding, :expires_in=>5.hours, :keys=>[:a, :b]){ a + b * c }
|
50
|
+
Cachy.increment_key(:b) --> expires :b and :surrounding
|
51
|
+
|
52
|
+
####Hashing keys
|
53
|
+
In case they get to long for your caching backend, makes them short but unreadable.
|
54
|
+
|
55
|
+
Cachy.hash_keys = true # global
|
56
|
+
Cachy.cache(:a_key, :hash_key=>true){ expensive } # per call
|
57
|
+
|
58
|
+
#### Uses .cache_key when available
|
59
|
+
E.g. ActiveRecord objects are stored in the key with their updated_at timestamp.
|
60
|
+
When they are updated the cache is automatically expired.
|
61
|
+
|
62
|
+
Cachy.cache(:my_key, User.first){ expensive }
|
63
|
+
|
64
|
+
#### Uses CACHE_VERSION if defined
|
65
|
+
Use a global `CACHE_VERSION=1` so that all caches can be expired when something big changes.
|
66
|
+
The cache server does not need to be restarted and session data(Rails) is saved.
|
67
|
+
|
68
|
+
|
69
|
+
###Cachy.expire / .expire_view
|
70
|
+
Expires all locales of a key
|
71
|
+
Cachy.locales = [:de, :en] # by default filled with I18n.available_locales
|
72
|
+
Cachy.expire(:my_key) -> expires for :de, :en and no-locale
|
73
|
+
|
74
|
+
#expire "views/#{key}" (counterpart for Rails-view-caching)
|
75
|
+
Cachy.expire_view(:my_key)
|
76
|
+
Cachy.expire(:my_key, :prefix=>'views/')
|
77
|
+
|
78
|
+
|
79
|
+
###Cachy.key
|
80
|
+
Use to cache e.g. Erb output
|
81
|
+
<% cache Cachy.key(:a_key), :expires_in=>1.hour do %>
|
82
|
+
More html ...
|
83
|
+
<% end %>
|
84
|
+
|
85
|
+
|
86
|
+
###Cachy.cache_store
|
87
|
+
No ActionController::Base.cache_store ?
|
88
|
+
Give me something that responds to read/write(Rails style) or []/store([Moneta](http://github.com/wycats/moneta/tree/master)) or get/set(Memcached)
|
89
|
+
Cachy.cache_store = some_cache
|
90
|
+
|
91
|
+
|
92
|
+
###Cachy.locales
|
93
|
+
No I18n.available_locales ?
|
94
|
+
Cachy.locales = [:de, :en, :fr]
|
95
|
+
|
96
|
+
TODO
|
97
|
+
====
|
98
|
+
- optionally store dependent keys (:keys=>xxx), so that they can be setup up once and dont need to be remembered
|
99
|
+
|
100
|
+
Authors
|
101
|
+
=======
|
102
|
+
|
103
|
+
###Contributors
|
104
|
+
- [mindreframer](http://www.simplewebapp.de/roman)
|
105
|
+
|
106
|
+
[Michael Grosser](http://pragmatig.wordpress.com)
|
107
|
+
grosser.michael@gmail.com
|
108
|
+
Hereby placed under public domain, do what you want, just do not hold me accountable...
|
data/Rakefile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
desc "Run all specs in spec directory"
|
2
|
+
task :default do
|
3
|
+
options = "--colour --format progress --loadby --reverse"
|
4
|
+
files = FileList['spec/**/*_spec.rb']
|
5
|
+
system("spec #{options} #{files}")
|
6
|
+
end
|
7
|
+
|
8
|
+
|
9
|
+
begin
|
10
|
+
require 'jeweler'
|
11
|
+
project_name = 'cachy'
|
12
|
+
Jeweler::Tasks.new do |gem|
|
13
|
+
gem.name = project_name
|
14
|
+
gem.summary = "Caching library for projects that have many processes or many caches"
|
15
|
+
gem.email = "grosser.michael@gmail.com"
|
16
|
+
gem.homepage = "http://github.com/grosser/#{project_name}"
|
17
|
+
gem.authors = ["Michael Grosser"]
|
18
|
+
end
|
19
|
+
rescue LoadError
|
20
|
+
puts "Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
21
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/cachy.gemspec
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{cachy}
|
5
|
+
s.version = "0.1.0"
|
6
|
+
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.authors = ["Michael Grosser"]
|
9
|
+
s.date = %q{2009-09-06}
|
10
|
+
s.email = %q{grosser.michael@gmail.com}
|
11
|
+
s.extra_rdoc_files = [
|
12
|
+
"README.markdown"
|
13
|
+
]
|
14
|
+
s.files = [
|
15
|
+
"README.markdown",
|
16
|
+
"Rakefile",
|
17
|
+
"VERSION",
|
18
|
+
"cachy.gemspec",
|
19
|
+
"lib/cachy.rb",
|
20
|
+
"lib/cachy/memcached_wrapper.rb",
|
21
|
+
"lib/cachy/moneta_wrapper.rb",
|
22
|
+
"lib/cachy/wrapper.rb",
|
23
|
+
"spec/cachy/memcached_wrapper_spec.rb",
|
24
|
+
"spec/cachy/moneta_wrapper_spec.rb",
|
25
|
+
"spec/cachy_spec.rb",
|
26
|
+
"spec/spec_helper.rb",
|
27
|
+
"spec/test_cache.rb"
|
28
|
+
]
|
29
|
+
s.homepage = %q{http://github.com/grosser/cachy}
|
30
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
31
|
+
s.require_paths = ["lib"]
|
32
|
+
s.rubygems_version = %q{1.3.4}
|
33
|
+
s.summary = %q{Caching library for projects that have many processes or many caches}
|
34
|
+
s.test_files = [
|
35
|
+
"spec/test_cache.rb",
|
36
|
+
"spec/cachy/memcached_wrapper_spec.rb",
|
37
|
+
"spec/cachy/moneta_wrapper_spec.rb",
|
38
|
+
"spec/spec_helper.rb",
|
39
|
+
"spec/cachy_spec.rb"
|
40
|
+
]
|
41
|
+
|
42
|
+
if s.respond_to? :specification_version then
|
43
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
44
|
+
s.specification_version = 3
|
45
|
+
|
46
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
47
|
+
else
|
48
|
+
end
|
49
|
+
else
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class Cachy::Wrapper
|
2
|
+
def initialize(wrapped)
|
3
|
+
@wrapped = wrapped
|
4
|
+
end
|
5
|
+
|
6
|
+
def read(key)
|
7
|
+
@wrapped[key]
|
8
|
+
end
|
9
|
+
|
10
|
+
def method_missing(name, args)
|
11
|
+
@wrapped.send(name, *args)
|
12
|
+
end
|
13
|
+
|
14
|
+
def respond_to?(x)
|
15
|
+
super(x) || @wrapped.respond_to?(x)
|
16
|
+
end
|
17
|
+
end
|
data/lib/cachy.rb
ADDED
@@ -0,0 +1,193 @@
|
|
1
|
+
class Cachy
|
2
|
+
WHILE_RUNNING_TMEOUT = 5*60 #seconds
|
3
|
+
KEY_VERSION_TIMEOUT = 30 #seconds
|
4
|
+
|
5
|
+
# Cache the result of a block
|
6
|
+
#
|
7
|
+
# Cachy.cache(:my_key){ expensive() }
|
8
|
+
# Cachy.cache(:my_key, :expires_in=>1.hour){ expensive() }
|
9
|
+
# Cachy.cache(:my_key, :keys=>[:dependent_key]){ expensive() }
|
10
|
+
# Cachy.cache(:my_key, :without_locale=>true){ expensive() }
|
11
|
+
# Cachy.cache(:my_key, :hash_key=>true){ expensive() }
|
12
|
+
def self.cache(*args)
|
13
|
+
key = key(*args)
|
14
|
+
options = extract_options!(args)
|
15
|
+
|
16
|
+
# Cached result?
|
17
|
+
result = cache_store.read(key) and return result
|
18
|
+
|
19
|
+
# Calculate result!
|
20
|
+
set_while_running(key, options)
|
21
|
+
|
22
|
+
result = yield
|
23
|
+
cache_store.write key, result, options
|
24
|
+
result
|
25
|
+
end
|
26
|
+
|
27
|
+
# Constructs a cache-key (first argument must be a String/Symbol)
|
28
|
+
#
|
29
|
+
# Cachy.key :my_key
|
30
|
+
# Cachy.key :my_key, User.first, :locale=>:de
|
31
|
+
# Cachy.key :my_key, User.first, :without_locale=>true, :hash_key=>true
|
32
|
+
def self.key(*args)
|
33
|
+
options = extract_options!(args)
|
34
|
+
ensure_valid_keys options
|
35
|
+
|
36
|
+
key = (args + meta_key_parts(args.first, options)).compact.map do |part|
|
37
|
+
if part.respond_to? :cache_key
|
38
|
+
part.cache_key
|
39
|
+
else
|
40
|
+
part
|
41
|
+
end
|
42
|
+
end * "_"
|
43
|
+
|
44
|
+
key = (options[:hash_key] || hash_keys) ? hash(key) : key
|
45
|
+
options[:prefix].to_s + key + options[:suffix].to_s
|
46
|
+
end
|
47
|
+
|
48
|
+
# Expire all possible locales of a cache, use the same arguments as with cache
|
49
|
+
#
|
50
|
+
# Cachy.expire(:my_key, User.first)
|
51
|
+
# Cachy.expire(:my_key, User.first, :keys=>[:dependent_keys])
|
52
|
+
# Cachy.expire(:my_key, :prefix=>'views/')
|
53
|
+
def self.expire(*args)
|
54
|
+
options = extract_options!(args)
|
55
|
+
|
56
|
+
(locales+[false]).each do |locale|
|
57
|
+
without_locale = (locale==false)
|
58
|
+
args_with_locale = args + [options.merge(:locale=>locale, :without_locale=>without_locale)]
|
59
|
+
cache_store.delete key(*args_with_locale)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.expire_view(*args)
|
64
|
+
options = extract_options!(args)
|
65
|
+
args = args + [options.merge(:prefix=>'views/')]
|
66
|
+
expire(*args)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Fetch key_versions from cache every KEY_VERSION_TIMEOUT seconds,
|
70
|
+
# otherwise every .key call would result in an cache request
|
71
|
+
@@key_versions = {:versions=>{}, :last_set=>0}
|
72
|
+
def self.key_versions
|
73
|
+
if key_versions_expired?
|
74
|
+
versions = cache_store.read("cachy_key_versions") || {}
|
75
|
+
@@key_versions = {:versions=>versions, :last_set=>Time.now.to_i}
|
76
|
+
end
|
77
|
+
@@key_versions[:versions]
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.key_versions=(data)
|
81
|
+
@@key_versions[:last_set] = 0 #expire current key
|
82
|
+
cache_store.write("cachy_key_versions", data)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Expires all caches that use this key
|
86
|
+
def self.increment_key(key)
|
87
|
+
key = key.to_sym
|
88
|
+
version = key_versions[key] || 0
|
89
|
+
version += 1
|
90
|
+
self.key_versions = key_versions.merge(key => version)
|
91
|
+
version
|
92
|
+
end
|
93
|
+
|
94
|
+
class << self
|
95
|
+
attr_accessor :hash_keys
|
96
|
+
end
|
97
|
+
|
98
|
+
# Wrap non ActiveSupport style cache stores,
|
99
|
+
# to get the same interface for all
|
100
|
+
def self.cache_store=(x)
|
101
|
+
if x.respond_to? :read and x.respond_to? :write
|
102
|
+
@cache_store=x
|
103
|
+
elsif x.respond_to? "[]" and x.respond_to? :set
|
104
|
+
require 'cachy/memcached_wrapper'
|
105
|
+
@cache_store = MemcachedWrapper.new(x)
|
106
|
+
elsif x.respond_to? "[]" and x.respond_to? :store
|
107
|
+
require 'cachy/moneta_wrapper'
|
108
|
+
@cache_store = MonetaWrapper.new(x)
|
109
|
+
else
|
110
|
+
raise "This cache_store type is not usable for Cachy!"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def self.cache_store
|
115
|
+
@cache_store || raise("Use: Cachy.cache_store = your_cache_store")
|
116
|
+
end
|
117
|
+
|
118
|
+
self.cache_store = ActionController::Base.cache_store if defined? ActionController::Base
|
119
|
+
|
120
|
+
# locales
|
121
|
+
class << self
|
122
|
+
attr_accessor :locales
|
123
|
+
end
|
124
|
+
|
125
|
+
self.locales = if defined?(I18n) and I18n.respond_to?(:available_locales)
|
126
|
+
I18n.available_locales
|
127
|
+
else
|
128
|
+
[]
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
# Do we need to fetch fresh key_versions from cache ?
|
134
|
+
def self.key_versions_expired?
|
135
|
+
key_versions_timeout = Time.now.to_i - KEY_VERSION_TIMEOUT
|
136
|
+
@@key_versions[:last_set] < key_versions_timeout
|
137
|
+
end
|
138
|
+
|
139
|
+
# Temorarily store something else in the cache,
|
140
|
+
# so that a often-called and slow cache-block is not run by
|
141
|
+
# multiple processes in parallel
|
142
|
+
def self.set_while_running(key, options)
|
143
|
+
return unless options.key? :while_running
|
144
|
+
warn "You cannot set while_running to nil or false" unless options[:while_running]
|
145
|
+
cache_store.write key, options[:while_running], :expires_in=>WHILE_RUNNING_TMEOUT
|
146
|
+
end
|
147
|
+
|
148
|
+
def self.meta_key_parts(key, options)
|
149
|
+
unless [String, Symbol].include?(key.class)
|
150
|
+
raise ":key must be first argument of Cachy call"
|
151
|
+
end
|
152
|
+
|
153
|
+
parts = []
|
154
|
+
parts << "v#{key_version_for(key)}"
|
155
|
+
parts << global_cache_version
|
156
|
+
parts << (options[:locale] || locale) unless options[:without_locale]
|
157
|
+
|
158
|
+
keys = [*options[:keys]].compact # [*x] == .to_a without warnings
|
159
|
+
parts += keys.map{|k| "#{k}v#{key_version_for(k)}" }
|
160
|
+
parts
|
161
|
+
end
|
162
|
+
|
163
|
+
def self.key_version_for(key)
|
164
|
+
key = key.to_sym
|
165
|
+
key_versions[key] || increment_key(key)
|
166
|
+
end
|
167
|
+
|
168
|
+
def self.ensure_valid_keys(options)
|
169
|
+
invalid = options.keys - [:keys, :expires_in, :without_locale, :locale, :while_running, :hash_key, :prefix, :suffix]
|
170
|
+
raise "unknown keys #{invalid.inspect}" unless invalid.empty?
|
171
|
+
end
|
172
|
+
|
173
|
+
def self.hash(string)
|
174
|
+
require "digest/md5"
|
175
|
+
Digest::MD5.hexdigest(string)
|
176
|
+
end
|
177
|
+
|
178
|
+
def self.global_cache_version
|
179
|
+
defined?(CACHE_VERSION) ? CACHE_VERSION : nil
|
180
|
+
end
|
181
|
+
|
182
|
+
def self.locale
|
183
|
+
(defined?(I18n) and I18n.respond_to?(:locale)) ? I18n.locale : nil
|
184
|
+
end
|
185
|
+
|
186
|
+
def self.extract_options!(args)
|
187
|
+
if args.last.is_a? Hash
|
188
|
+
args.pop
|
189
|
+
else
|
190
|
+
{}
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'spec/spec_helper'
|
2
|
+
|
3
|
+
class TestMemcached
|
4
|
+
def initialize
|
5
|
+
@wrapped = {}
|
6
|
+
end
|
7
|
+
|
8
|
+
def set(key, object, ttl = nil)
|
9
|
+
raise 'nope!' if ttl.is_a? Hash or (ttl and not ttl.is_a? Numeric)
|
10
|
+
@wrapped[key] = object
|
11
|
+
end
|
12
|
+
|
13
|
+
def get(key)
|
14
|
+
@wrapped[key]
|
15
|
+
end
|
16
|
+
|
17
|
+
def [](x)
|
18
|
+
@wrapped[x]
|
19
|
+
end
|
20
|
+
|
21
|
+
def clear
|
22
|
+
@wrapped.clear
|
23
|
+
end
|
24
|
+
|
25
|
+
def delete(key)
|
26
|
+
@wrapped.delete(key)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "Cachy::MemcachedWrapper" do
|
31
|
+
before :all do
|
32
|
+
@cache = TestMemcached.new
|
33
|
+
Cachy.cache_store = @cache
|
34
|
+
end
|
35
|
+
|
36
|
+
before do
|
37
|
+
@cache.clear
|
38
|
+
end
|
39
|
+
|
40
|
+
it "is wrapped" do
|
41
|
+
Cachy.cache_store.class.should == Cachy::MemcachedWrapper
|
42
|
+
end
|
43
|
+
|
44
|
+
it "can cache" do
|
45
|
+
Cachy.cache(:x){ 'SUCCESS' }
|
46
|
+
Cachy.cache(:x){ 'FAIL' }.should == 'SUCCESS'
|
47
|
+
end
|
48
|
+
|
49
|
+
it "can cache without expires" do
|
50
|
+
@cache.should_receive(:set).with('x_v1', 'SUCCESS', 0)
|
51
|
+
Cachy.cache(:x){ 'SUCCESS' }
|
52
|
+
end
|
53
|
+
|
54
|
+
it "can cache with expires" do
|
55
|
+
@cache.should_receive(:set).with('x_v1', 'SUCCESS', 1)
|
56
|
+
Cachy.cache(:x, :expires_in=>1){ 'SUCCESS' }
|
57
|
+
end
|
58
|
+
|
59
|
+
it "can expire" do
|
60
|
+
@cache.should_receive(:delete).with('x_v1')
|
61
|
+
Cachy.expire(:x)
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'spec/spec_helper'
|
2
|
+
|
3
|
+
class TestMoneta
|
4
|
+
def initialize
|
5
|
+
@wrapped = {}
|
6
|
+
end
|
7
|
+
|
8
|
+
def store(key, object, options={})
|
9
|
+
raise 'nope!' if options[:expires_in] and not options[:expires_in].is_a? Numeric
|
10
|
+
@wrapped[key] = object
|
11
|
+
end
|
12
|
+
|
13
|
+
def [](x)
|
14
|
+
@wrapped[x]
|
15
|
+
end
|
16
|
+
|
17
|
+
def clear
|
18
|
+
@wrapped.clear
|
19
|
+
end
|
20
|
+
|
21
|
+
def delete(key)
|
22
|
+
@wrapped.delete(key)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "Cachy::MonetaWrapper" do
|
27
|
+
before :all do
|
28
|
+
@cache = TestMoneta.new
|
29
|
+
Cachy.cache_store = @cache
|
30
|
+
end
|
31
|
+
|
32
|
+
before do
|
33
|
+
@cache.clear
|
34
|
+
end
|
35
|
+
|
36
|
+
it "is wrapped" do
|
37
|
+
Cachy.cache_store.class.should == Cachy::MonetaWrapper
|
38
|
+
end
|
39
|
+
|
40
|
+
it "can cache" do
|
41
|
+
Cachy.cache(:x){ 'SUCCESS' }
|
42
|
+
Cachy.cache(:x){ 'FAIL' }.should == 'SUCCESS'
|
43
|
+
end
|
44
|
+
|
45
|
+
it "can cache without expires" do
|
46
|
+
@cache.should_receive(:store).with('x_v1', 'SUCCESS', {})
|
47
|
+
Cachy.cache(:x){ 'SUCCESS' }
|
48
|
+
end
|
49
|
+
|
50
|
+
it "can cache with expires" do
|
51
|
+
@cache.should_receive(:store).with('x_v1', 'SUCCESS', :expires_in=>1)
|
52
|
+
Cachy.cache(:x, :expires_in=>1){ 'SUCCESS' }
|
53
|
+
end
|
54
|
+
|
55
|
+
it "can expire" do
|
56
|
+
@cache.should_receive(:delete).with('x_v1')
|
57
|
+
Cachy.expire(:x)
|
58
|
+
end
|
59
|
+
end
|
data/spec/cachy_spec.rb
ADDED
@@ -0,0 +1,231 @@
|
|
1
|
+
require 'spec/spec_helper'
|
2
|
+
|
3
|
+
TEST_CACHE = TestCache.new
|
4
|
+
|
5
|
+
describe Cachy do
|
6
|
+
before :all do
|
7
|
+
Cachy.cache_store = TEST_CACHE
|
8
|
+
end
|
9
|
+
|
10
|
+
before do
|
11
|
+
TEST_CACHE.clear
|
12
|
+
end
|
13
|
+
|
14
|
+
describe :cache do
|
15
|
+
it "caches" do
|
16
|
+
Cachy.cache(:my_key){ "X" }.should == "X"
|
17
|
+
Cachy.cache(:my_key){ "ABC" }.should == "X"
|
18
|
+
end
|
19
|
+
|
20
|
+
it "expires" do
|
21
|
+
Cachy.cache(:his_key, :expires_in=> -100){ 'X' }.should == 'X'
|
22
|
+
Cachy.cache(:his_key, :expires_in=> -100){ 'X' }.should == 'X'
|
23
|
+
end
|
24
|
+
|
25
|
+
it "sets cache to intermediate value while running expensive query" do
|
26
|
+
Cachy.cache(:my_key, :while_running=>'A') do
|
27
|
+
Cachy.cache(:my_key){ 'X' }.should == 'A'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
it "can not set while_running to nil" do
|
32
|
+
Cachy.should_receive(:warn)
|
33
|
+
Cachy.cache(:my_key, :while_running=>nil) do
|
34
|
+
Cachy.cache(:my_key){ 'X' }.should == "X"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe :expire do
|
40
|
+
it "expires the cache for all languages" do
|
41
|
+
Cachy.cache(:my_key){ "without_locale" }
|
42
|
+
|
43
|
+
locales = [:de, :en, :fr]
|
44
|
+
locales.each do |l|
|
45
|
+
Cachy.stub!(:locale).and_return l
|
46
|
+
Cachy.cache(:my_key){ l }
|
47
|
+
end
|
48
|
+
|
49
|
+
TEST_CACHE.keys.select{|k| k=~ /my_key/}.size.should == 4
|
50
|
+
Cachy.stub!(:locales).and_return locales
|
51
|
+
Cachy.expire(:my_key)
|
52
|
+
TEST_CACHE.keys.select{|k| k=~ /my_key/}.size.should == 0
|
53
|
+
end
|
54
|
+
|
55
|
+
it "does not expire other keys" do
|
56
|
+
Cachy.cache(:another_key){ 'X' }
|
57
|
+
Cachy.cache(:my_key){ 'X' }
|
58
|
+
Cachy.cache(:yet_another_key){ 'X' }
|
59
|
+
Cachy.expire :my_key
|
60
|
+
TEST_CACHE.keys.should include("another_key_v1")
|
61
|
+
TEST_CACHE.keys.should include("yet_another_key_v1")
|
62
|
+
end
|
63
|
+
|
64
|
+
it "expires the cache when no available_locales are set" do
|
65
|
+
Cachy.cache(:another_key){ "X" }
|
66
|
+
Cachy.cache(:my_key){ "X" }
|
67
|
+
|
68
|
+
TEST_CACHE.keys.select{|k| k=~ /my_key/}.size.should == 1
|
69
|
+
Cachy.expire(:my_key)
|
70
|
+
TEST_CACHE.keys.select{|k| k=~ /my_key/}.size.should == 0
|
71
|
+
end
|
72
|
+
|
73
|
+
it "expires the cache with prefix" do
|
74
|
+
key = 'views/my_key_v1'
|
75
|
+
TEST_CACHE.write(key, 'x')
|
76
|
+
TEST_CACHE.read(key).should_not == nil
|
77
|
+
Cachy.expire(:my_key, :prefix=>'views/')
|
78
|
+
TEST_CACHE.read(key).should == nil
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
describe :expire_view do
|
83
|
+
it "expires the cache with prefix" do
|
84
|
+
key = 'views/my_key_v1'
|
85
|
+
TEST_CACHE.write(key, 'x')
|
86
|
+
TEST_CACHE.read(key).should_not == nil
|
87
|
+
Cachy.expire_view(:my_key)
|
88
|
+
TEST_CACHE.read(key).should == nil
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
describe :key do
|
93
|
+
it "builds based on cache_key" do
|
94
|
+
user = mock(:cache_key=>'XXX',:something_else=>'YYY')
|
95
|
+
Cachy.key(:my_key, 1, user, "abc").should == 'my_key_1_XXX_abc_v1'
|
96
|
+
end
|
97
|
+
|
98
|
+
it "adds current_language" do
|
99
|
+
Cachy.stub!(:locale).and_return :de
|
100
|
+
Cachy.key(:x).should == "x_v1_de"
|
101
|
+
end
|
102
|
+
|
103
|
+
it "raises on unknown options" do
|
104
|
+
lambda{Cachy.key(:x, :asdasd=>'asd')}.should raise_error
|
105
|
+
end
|
106
|
+
|
107
|
+
it "gets the locale from I18n" do
|
108
|
+
module I18n
|
109
|
+
def self.locale
|
110
|
+
:de
|
111
|
+
end
|
112
|
+
end
|
113
|
+
key = Cachy.key(:x)
|
114
|
+
Object.send :remove_const, :I18n #cleanup
|
115
|
+
key.should == "x_v1_de"
|
116
|
+
end
|
117
|
+
|
118
|
+
describe "with :hash_key" do
|
119
|
+
before do
|
120
|
+
@hash = '3b2b8f418849bc73071375529f8515be'
|
121
|
+
end
|
122
|
+
|
123
|
+
after do
|
124
|
+
Cachy.hash_keys = false
|
125
|
+
end
|
126
|
+
|
127
|
+
it "hashed the key to a stable value" do
|
128
|
+
Cachy.key(:xxx, :hash_key=>true).should == @hash
|
129
|
+
end
|
130
|
+
|
131
|
+
it "changes when key changes" do
|
132
|
+
Cachy.key(:xxx, :hash_key=>true).should_not == Cachy.key(:yyy, :hash_key=>true)
|
133
|
+
end
|
134
|
+
|
135
|
+
it "changes when arguments change" do
|
136
|
+
Cachy.key(:xxx, :hash_key=>true).should_not == Cachy.key(:xxx, 1, :hash_key=>true)
|
137
|
+
end
|
138
|
+
|
139
|
+
it "can be triggered by Cachy.hash_keys" do
|
140
|
+
Cachy.hash_keys = true
|
141
|
+
Cachy.key(:xxx).should == @hash
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
describe "with :keys" do
|
146
|
+
it "is stable" do
|
147
|
+
Cachy.key(:x, :keys=>'asd').should == Cachy.key(:x, :keys=>'asd')
|
148
|
+
end
|
149
|
+
|
150
|
+
it "changes when dependent key changes" do
|
151
|
+
lambda{
|
152
|
+
Cachy.increment_key('asd')
|
153
|
+
}.should change{Cachy.key(:x, :keys=>'asd')}
|
154
|
+
end
|
155
|
+
|
156
|
+
it "is different for different keys" do
|
157
|
+
Cachy.key(:x, :keys=>'xxx').should_not == Cachy.key(:x, :keys=>'yyy')
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
describe 'with :without_locale' do
|
162
|
+
it "is stable with same locale" do
|
163
|
+
Cachy.stub!(:locale).and_return :de
|
164
|
+
Cachy.key(:x, :without_locale=>true).should == Cachy.key(:x, :without_locale=>true)
|
165
|
+
end
|
166
|
+
|
167
|
+
it "is stable with different locales" do
|
168
|
+
Cachy.stub!(:locale).and_return :de
|
169
|
+
de_key = Cachy.key(:x, :without_locale=>true)
|
170
|
+
Cachy.stub!(:locale).and_return :en
|
171
|
+
Cachy.key(:x, :without_locale=>true).should == de_key
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
describe 'with :locale' do
|
176
|
+
it "changes the default key" do
|
177
|
+
Cachy.key(:x, :locale=>:de).should_not == Cachy.key(:x)
|
178
|
+
end
|
179
|
+
|
180
|
+
it "is a different key for different locales" do
|
181
|
+
Cachy.key(:x, :locale=>:de).should_not == Cachy.key(:x, :locale=>:en)
|
182
|
+
end
|
183
|
+
|
184
|
+
it "is the same key for the same locales" do
|
185
|
+
Cachy.key(:x, :locale=>:de).should == Cachy.key(:x, :locale=>:de)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
describe :key_versions do
|
191
|
+
before do
|
192
|
+
Cachy.key_versions = {}
|
193
|
+
Cachy.key_versions.should == {}
|
194
|
+
end
|
195
|
+
|
196
|
+
it "adds a key when cache is called the first time" do
|
197
|
+
Cachy.cache(:xxx){1}
|
198
|
+
Cachy.key_versions[:xxx].should == 1
|
199
|
+
end
|
200
|
+
|
201
|
+
it "adds a string key as symbol" do
|
202
|
+
Cachy.cache('yyy'){1}
|
203
|
+
Cachy.key_versions[:yyy].should == 1
|
204
|
+
end
|
205
|
+
|
206
|
+
it "does not overwrite a key when it already exists" do
|
207
|
+
Cachy.key_versions = {:xxx => 3}
|
208
|
+
Cachy.cache(:xxx){1}
|
209
|
+
Cachy.cache(:xxx){1}
|
210
|
+
Cachy.key_versions[:xxx].should == 3
|
211
|
+
end
|
212
|
+
|
213
|
+
it "reloads when keys have expired" do
|
214
|
+
Cachy.send :class_variable_set, "@@key_versions", {:versions=>{:xx=>2}, :last_set=>(Time.now.to_i - 60)}
|
215
|
+
TEST_CACHE.write 'cachy_key_versions', {:xx=>1}
|
216
|
+
Cachy.key_versions.should == {:xx=>1}
|
217
|
+
end
|
218
|
+
|
219
|
+
it "does not reload when keys have not expired" do
|
220
|
+
Cachy.send :class_variable_set, "@@key_versions", {:versions=>{:xx=>2}, :last_set=>Time.now.to_i}
|
221
|
+
TEST_CACHE.write 'cachy_key_versions', {:xx=>1}
|
222
|
+
Cachy.key_versions.should == {:xx=>2}
|
223
|
+
end
|
224
|
+
|
225
|
+
it "expires when key_versions is set" do
|
226
|
+
Cachy.send :class_variable_set, "@@key_versions", {:versions=>{:xx=>2}, :last_set=>Time.now.to_i}
|
227
|
+
Cachy.key_versions = {:xx=>1}
|
228
|
+
Cachy.key_versions[:xx].should == 1
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
data/spec/spec_helper.rb
ADDED
data/spec/test_cache.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
class TestCache
|
2
|
+
def initialize
|
3
|
+
@data = {}
|
4
|
+
end
|
5
|
+
|
6
|
+
def read(name)
|
7
|
+
@data[name]
|
8
|
+
end
|
9
|
+
|
10
|
+
def write(name, value, options = nil)
|
11
|
+
@data[name] = value.freeze
|
12
|
+
end
|
13
|
+
|
14
|
+
def delete(name, options = nil)
|
15
|
+
@data.delete(name)
|
16
|
+
end
|
17
|
+
|
18
|
+
def clear
|
19
|
+
@data.clear
|
20
|
+
end
|
21
|
+
|
22
|
+
def keys
|
23
|
+
@data.keys
|
24
|
+
end
|
25
|
+
end
|
metadata
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: grosser-cachy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Michael Grosser
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-09-06 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description:
|
17
|
+
email: grosser.michael@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README.markdown
|
24
|
+
files:
|
25
|
+
- README.markdown
|
26
|
+
- Rakefile
|
27
|
+
- VERSION
|
28
|
+
- cachy.gemspec
|
29
|
+
- lib/cachy.rb
|
30
|
+
- lib/cachy/memcached_wrapper.rb
|
31
|
+
- lib/cachy/moneta_wrapper.rb
|
32
|
+
- lib/cachy/wrapper.rb
|
33
|
+
- spec/cachy/memcached_wrapper_spec.rb
|
34
|
+
- spec/cachy/moneta_wrapper_spec.rb
|
35
|
+
- spec/cachy_spec.rb
|
36
|
+
- spec/spec_helper.rb
|
37
|
+
- spec/test_cache.rb
|
38
|
+
has_rdoc: false
|
39
|
+
homepage: http://github.com/grosser/cachy
|
40
|
+
post_install_message:
|
41
|
+
rdoc_options:
|
42
|
+
- --charset=UTF-8
|
43
|
+
require_paths:
|
44
|
+
- lib
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: "0"
|
50
|
+
version:
|
51
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: "0"
|
56
|
+
version:
|
57
|
+
requirements: []
|
58
|
+
|
59
|
+
rubyforge_project:
|
60
|
+
rubygems_version: 1.2.0
|
61
|
+
signing_key:
|
62
|
+
specification_version: 3
|
63
|
+
summary: Caching library for projects that have many processes or many caches
|
64
|
+
test_files:
|
65
|
+
- spec/test_cache.rb
|
66
|
+
- spec/cachy/memcached_wrapper_spec.rb
|
67
|
+
- spec/cachy/moneta_wrapper_spec.rb
|
68
|
+
- spec/spec_helper.rb
|
69
|
+
- spec/cachy_spec.rb
|