merb-cache 0.9.7 → 0.9.8
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +2 -2
- data/README +207 -143
- data/Rakefile +55 -10
- data/TODO +0 -2
- data/lib/merb-cache/cache.rb +84 -0
- data/lib/merb-cache/core_ext/enumerable.rb +9 -0
- data/lib/merb-cache/core_ext/hash.rb +20 -0
- data/lib/merb-cache/merb_ext/controller.rb +167 -0
- data/lib/merb-cache/stores/fundamental/abstract_store.rb +101 -0
- data/lib/merb-cache/stores/fundamental/file_store.rb +112 -0
- data/lib/merb-cache/stores/fundamental/memcached_store.rb +112 -0
- data/lib/merb-cache/stores/strategy/abstract_strategy_store.rb +123 -0
- data/lib/merb-cache/stores/strategy/action_store.rb +56 -0
- data/lib/merb-cache/stores/strategy/adhoc_store.rb +69 -0
- data/lib/merb-cache/stores/strategy/gzip_store.rb +63 -0
- data/lib/merb-cache/stores/strategy/page_store.rb +64 -0
- data/lib/merb-cache/stores/strategy/sha1_store.rb +62 -0
- data/lib/merb-cache.rb +8 -7
- data/spec/merb-cache/cache_spec.rb +88 -0
- data/spec/merb-cache/core_ext/enumerable_spec.rb +22 -0
- data/spec/merb-cache/core_ext/hash_spec.rb +20 -0
- data/spec/merb-cache/merb_ext/controller_spec.rb +284 -0
- data/spec/merb-cache/stores/fundamental/abstract_store_spec.rb +166 -0
- data/spec/merb-cache/stores/fundamental/file_store_spec.rb +186 -0
- data/spec/merb-cache/stores/fundamental/memcached_store_spec.rb +243 -0
- data/spec/merb-cache/stores/strategy/abstract_strategy_store_spec.rb +78 -0
- data/spec/merb-cache/stores/strategy/action_store_spec.rb +189 -0
- data/spec/merb-cache/stores/strategy/adhoc_store_spec.rb +225 -0
- data/spec/merb-cache/stores/strategy/gzip_store_spec.rb +51 -0
- data/spec/merb-cache/stores/strategy/page_store_spec.rb +111 -0
- data/spec/merb-cache/stores/strategy/sha1_store_spec.rb +75 -0
- data/spec/spec_helper.rb +69 -72
- metadata +42 -31
- data/lib/merb-cache/cache-action.rb +0 -144
- data/lib/merb-cache/cache-fragment.rb +0 -95
- data/lib/merb-cache/cache-page.rb +0 -203
- data/lib/merb-cache/cache-store/database-activerecord.rb +0 -88
- data/lib/merb-cache/cache-store/database-datamapper.rb +0 -79
- data/lib/merb-cache/cache-store/database-sequel.rb +0 -78
- data/lib/merb-cache/cache-store/database.rb +0 -144
- data/lib/merb-cache/cache-store/dummy.rb +0 -106
- data/lib/merb-cache/cache-store/file.rb +0 -194
- data/lib/merb-cache/cache-store/memcache.rb +0 -199
- data/lib/merb-cache/cache-store/memory.rb +0 -168
- data/lib/merb-cache/merb-cache.rb +0 -165
- data/lib/merb-cache/merbtasks.rb +0 -6
- data/spec/config/database.yml +0 -14
- data/spec/controller.rb +0 -101
- data/spec/log/merb_test.log +0 -433
- data/spec/merb-cache-action_spec.rb +0 -162
- data/spec/merb-cache-fragment_spec.rb +0 -100
- data/spec/merb-cache-page_spec.rb +0 -150
- data/spec/merb-cache_spec.rb +0 -15
- data/spec/views/cache_controller/action1.html.erb +0 -4
- data/spec/views/cache_controller/action2.html.haml +0 -4
@@ -0,0 +1,167 @@
|
|
1
|
+
module Merb::Cache::CacheMixin
|
2
|
+
def self.included(base)
|
3
|
+
base.extend(ClassMethods)
|
4
|
+
end
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
def cache!(conditions = {})
|
8
|
+
before(:_cache_before, conditions.only(:if, :unless).merge(:with => conditions))
|
9
|
+
after(:_cache_after, conditions.only(:if, :unless).merge(:with => conditions))
|
10
|
+
end
|
11
|
+
|
12
|
+
def cache(*actions)
|
13
|
+
if actions.last.is_a? Hash
|
14
|
+
cache_action(*actions)
|
15
|
+
else
|
16
|
+
actions.each {|a| cache_action(*a)}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def cache_action(action, conditions = {})
|
21
|
+
before("_cache_#{action}_before", conditions.only(:if, :unless).merge(:with => [conditions], :only => action))
|
22
|
+
after("_cache_#{action}_after", conditions.only(:if, :unless).merge(:with => [conditions], :only => action))
|
23
|
+
alias_method "_cache_#{action}_before", :_cache_before
|
24
|
+
alias_method "_cache_#{action}_after", :_cache_after
|
25
|
+
end
|
26
|
+
|
27
|
+
def eager_cache(trigger_action, target = trigger_action, conditions = {}, &blk)
|
28
|
+
target, conditions = trigger_action, target if target.is_a? Hash
|
29
|
+
|
30
|
+
if target.is_a? Array
|
31
|
+
target_controller, target_action = *target
|
32
|
+
else
|
33
|
+
target_controller, target_action = self, target
|
34
|
+
end
|
35
|
+
|
36
|
+
after("_eager_cache_#{trigger_action}_to_#{target_controller.name.snake_case}__#{target_action}_after", conditions.only(:if, :unless).merge(:with => [target_controller, target_action, conditions, blk], :only => trigger_action))
|
37
|
+
alias_method "_eager_cache_#{trigger_action}_to_#{target_controller.name.snake_case}__#{target_action}_after", :_eager_cache_after
|
38
|
+
end
|
39
|
+
|
40
|
+
def eager_dispatch(action, env = {}, blk = nil)
|
41
|
+
kontroller = new(Merb::Request.new(env))
|
42
|
+
kontroller.force_cache!
|
43
|
+
|
44
|
+
blk.call(kontroller) unless blk.nil?
|
45
|
+
|
46
|
+
kontroller._dispatch(action)
|
47
|
+
|
48
|
+
kontroller
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def fetch_partial(template, opts={}, conditions = {})
|
53
|
+
template_id = template.to_s
|
54
|
+
if template_id =~ %r{^/}
|
55
|
+
template_path = File.dirname(template_id) / "_#{File.basename(template_id)}"
|
56
|
+
else
|
57
|
+
kontroller = (m = template_id.match(/.*(?=\/)/)) ? m[0] : controller_name
|
58
|
+
template_id = "_#{File.basename(template_id)}"
|
59
|
+
end
|
60
|
+
|
61
|
+
unused, template_key = _template_for(template_id, opts.delete(:format) || content_type, kontroller, template_path)
|
62
|
+
|
63
|
+
fetch_proc = lambda { partial(template, opts) }
|
64
|
+
|
65
|
+
concat(Merb::Cache[_lookup_store(conditions)].fetch(template_key, opts, conditions, &fetch_proc), fetch_proc.binding)
|
66
|
+
end
|
67
|
+
|
68
|
+
def fetch_fragment(opts = {}, conditions = {}, &proc)
|
69
|
+
|
70
|
+
if opts[:cache_key].blank?
|
71
|
+
file, line = proc.to_s.scan(%r{^#<Proc:0x\w+@(.+):(\d+)>$}).first
|
72
|
+
fragment_key = "#{file}[#{line}]"
|
73
|
+
else
|
74
|
+
fragment_key = opts.delete(:cache_key)
|
75
|
+
end
|
76
|
+
|
77
|
+
concat(Merb::Cache[_lookup_store(conditions)].fetch(fragment_key, opts, conditions) { capture(&proc) }, proc.binding)
|
78
|
+
end
|
79
|
+
|
80
|
+
def _cache_before(conditions = {})
|
81
|
+
unless @_force_cache
|
82
|
+
if @_skip_cache.nil? && data = Merb::Cache[_lookup_store(conditions)].read(self, _parameters_and_conditions(conditions).first)
|
83
|
+
throw(:halt, data)
|
84
|
+
@_cache_hit = true
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def _cache_after(conditions = {})
|
90
|
+
if @_skip_cache.nil? && Merb::Cache[_lookup_store(conditions)].write(self, nil, *_parameters_and_conditions(conditions))
|
91
|
+
@_cache_write = true
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def _eager_cache_after(klass, action, conditions = {}, blk = nil)
|
96
|
+
if @_skip_cache.nil?
|
97
|
+
run_later do
|
98
|
+
controller = klass.eager_dispatch(action, request.env, blk)
|
99
|
+
|
100
|
+
Merb::Cache[controller._lookup_store(conditions)].write(controller, nil, *controller._parameters_and_conditions(conditions))
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def eager_cache(action, conditions = {}, env = request.env.dup, &blk)
|
106
|
+
unless @_skip_cache
|
107
|
+
if action.is_a?(Array)
|
108
|
+
klass, action = *action
|
109
|
+
else
|
110
|
+
klass = self.class
|
111
|
+
end
|
112
|
+
|
113
|
+
run_later do
|
114
|
+
controller = klass.eager_dispatch(action, env, blk)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def _set_skip_cache
|
120
|
+
@_skip_cache = true
|
121
|
+
end
|
122
|
+
|
123
|
+
def skip_cache!
|
124
|
+
_set_skip_cache
|
125
|
+
end
|
126
|
+
|
127
|
+
def force_cache!
|
128
|
+
@_force_cache = true
|
129
|
+
end
|
130
|
+
|
131
|
+
def _lookup_store(conditions = {})
|
132
|
+
conditions[:store] || conditions[:stores] || default_cache_store
|
133
|
+
end
|
134
|
+
|
135
|
+
# Overwrite this in your controller to change the default store for a given controller
|
136
|
+
def default_cache_store
|
137
|
+
Merb::Cache.default_store_name
|
138
|
+
end
|
139
|
+
|
140
|
+
#ugly, please make me purdy'er
|
141
|
+
def _parameters_and_conditions(conditions)
|
142
|
+
parameters = {}
|
143
|
+
|
144
|
+
if self.class.respond_to? :action_argument_list
|
145
|
+
arguments, defaults = self.class.action_argument_list[action_name]
|
146
|
+
arguments.inject(parameters) do |parameters, arg|
|
147
|
+
if defaults.include?(arg.first)
|
148
|
+
parameters[arg.first] = self.params[arg.first] || arg.last
|
149
|
+
else
|
150
|
+
parameters[arg.first] = self.params[arg.first]
|
151
|
+
end
|
152
|
+
parameters
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
case conditions[:params]
|
157
|
+
when Symbol
|
158
|
+
parameters[conditions[:params]] = self.params[conditions[:params]]
|
159
|
+
when Array
|
160
|
+
conditions[:params].each do |param|
|
161
|
+
parameters[param] = self.params[param]
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
return parameters, conditions.except(:params, :store, :stores)
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
class Merb::Cache::AbstractStore
|
2
|
+
|
3
|
+
def initialize(config = {}); end
|
4
|
+
|
5
|
+
# determines if the store is able to persist data identified by the key & parameters
|
6
|
+
# with the given conditions.
|
7
|
+
#
|
8
|
+
# @param [#to_s] key the key used to identify an entry
|
9
|
+
# @param [Hash] parameters optional parameters used to identify an entry
|
10
|
+
# @param [Hash] conditions optional conditions that place constraints or detail instructions for storing an entry
|
11
|
+
# @return [TrueClass] the ability of the store to write an entry based on the key, parameters, and conditions
|
12
|
+
# @raise [NotImplementedError] API method has not been implemented
|
13
|
+
def writable?(key, parameters = {}, conditions = {})
|
14
|
+
raise NotImplementedError
|
15
|
+
end
|
16
|
+
|
17
|
+
# gets the data from the store identified by the key & parameters.
|
18
|
+
# return nil if the entry does not exist.
|
19
|
+
#
|
20
|
+
# @param [#to_s] key the key used to identify an entry
|
21
|
+
# @param [Hash] parameters optional parameters used to identify an entry
|
22
|
+
# @return [Object, NilClass] the match entry, or nil if no entry exists matching the key and parameters
|
23
|
+
# @raise [NotImplementedError] API method has not been implemented
|
24
|
+
def read(key, parameters = {})
|
25
|
+
raise NotImplementedError
|
26
|
+
end
|
27
|
+
|
28
|
+
# persists the data so that it can be retrieved by the key & parameters.
|
29
|
+
# returns nil if it is unable to persist the data.
|
30
|
+
# returns true if successful.
|
31
|
+
#
|
32
|
+
# @param [#to_s] key the key used to identify an entry
|
33
|
+
# @param data the object to persist as an entry
|
34
|
+
# @param [Hash] parameters optional parameters used to identify an entry
|
35
|
+
# @param [Hash] conditions optional conditions that place constraints or detail instructions for storing an entry
|
36
|
+
# @return [TrueClass, NilClass] true if the entry was successfully written, otherwise nil
|
37
|
+
# @raise [NotImplementedError] API method has not been implemented
|
38
|
+
def write(key, data = nil, parameters = {}, conditions = {})
|
39
|
+
raise NotImplementedError
|
40
|
+
end
|
41
|
+
|
42
|
+
# @param [#to_s] key the key used to identify an entry
|
43
|
+
# @param data the object to persist as an entry
|
44
|
+
# @param [Hash] parameters optional parameters used to identify an entry
|
45
|
+
# @param [Hash] conditions optional conditions that place constraints or detail instructions for storing an entry
|
46
|
+
# @return [TrueClass, NilClass] true if the entry was successfully written, otherwise nil
|
47
|
+
# @raise [NotImplementedError] API method has not been implemented
|
48
|
+
def write_all(key, data = nil, parameters = {}, conditions = {})
|
49
|
+
write(key, data, parameters, conditions)
|
50
|
+
end
|
51
|
+
|
52
|
+
# tries to read the data from the store. If that fails, it calls
|
53
|
+
# the block parameter and persists the result.
|
54
|
+
#
|
55
|
+
# @param [#to_s] key the key used to identify an entry
|
56
|
+
# @param [Hash] parameters optional parameters used to identify an entry
|
57
|
+
# @param [Hash] conditions optional conditions that place constraints or detail instructions for storing an entry
|
58
|
+
# @return [Object, NilClass] the match entry or the result of the block call, or nil if the entry is not successfully written
|
59
|
+
# @raise [NotImplementedError] API method has not been implemented
|
60
|
+
def fetch(key, parameters = {}, conditions = {}, &blk)
|
61
|
+
raise NotImplementedError
|
62
|
+
end
|
63
|
+
|
64
|
+
# returns true/false/nil based on if data identified by the key & parameters
|
65
|
+
# is persisted in the store.
|
66
|
+
#
|
67
|
+
# @param [#to_s] key the key used to identify an entry
|
68
|
+
# @param [Hash] parameters optional parameters used to identify an entry
|
69
|
+
# @return [TrueClass] true if the key and parameters match an entry in the store, false otherwise
|
70
|
+
# @raise [NotImplementedError] API method has not been implemented
|
71
|
+
def exists?(key, parameters = {})
|
72
|
+
raise NotImplementedError
|
73
|
+
end
|
74
|
+
|
75
|
+
# deletes the entry for the key & parameter from the store.
|
76
|
+
#
|
77
|
+
# @param [#to_s] key the key used to identify an entry
|
78
|
+
# @param [Hash] parameters optional parameters used to identify an entry
|
79
|
+
# @raise [TrueClass] true if the an entry matching the key and parameters is successfully deleted, false otherwise
|
80
|
+
# @raise [NotImplementedError] API method has not been implemented
|
81
|
+
def delete(key, parameters = {})
|
82
|
+
raise NotImplementedError
|
83
|
+
end
|
84
|
+
|
85
|
+
# deletes all entries for the key & parameters for the store.
|
86
|
+
#
|
87
|
+
# @return [TrueClass] true if all entries in the store are erased, false otherwise
|
88
|
+
# @raise [NotImplementedError] API method has not been implemented
|
89
|
+
def delete_all
|
90
|
+
raise NotImplementedError
|
91
|
+
end
|
92
|
+
|
93
|
+
# dangerous version of delete_all. Used by strategy stores, which may delete entries not associated with the strategy
|
94
|
+
# store making the call.
|
95
|
+
#
|
96
|
+
# @return [TrueClass] true if all entries in the store are erased, false otherwise
|
97
|
+
# @raise [NotImplementedError] API method has not been implemented
|
98
|
+
def delete_all!
|
99
|
+
delete_all
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
module Merb::Cache
|
2
|
+
# Cache store that uses files. Usually this is good for fragment
|
3
|
+
# and page caching but not object caching.
|
4
|
+
#
|
5
|
+
# By default cached files are stored in tmp/cache under Merb.root directory.
|
6
|
+
# To use other location pass :dir option to constructor.
|
7
|
+
#
|
8
|
+
# File caching does not support expiration time.
|
9
|
+
class FileStore < AbstractStore
|
10
|
+
attr_accessor :dir
|
11
|
+
|
12
|
+
# Creates directory for cached files unless it exists.
|
13
|
+
def initialize(config = {})
|
14
|
+
@dir = config[:dir] || Merb.root_path(:tmp / :cache)
|
15
|
+
|
16
|
+
create_path(@dir)
|
17
|
+
end
|
18
|
+
|
19
|
+
# File caching does not support expiration time.
|
20
|
+
def writable?(key, parameters = {}, conditions = {})
|
21
|
+
case key
|
22
|
+
when String, Numeric, Symbol
|
23
|
+
!conditions.has_key?(:expire_in)
|
24
|
+
else nil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Reads cached template from disk if it exists.
|
29
|
+
def read(key, parameters = {})
|
30
|
+
if exists?(key, parameters)
|
31
|
+
read_file(pathify(key, parameters))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Writes cached template to disk, creating cache directory
|
36
|
+
# if it does not exist.
|
37
|
+
def write(key, data = nil, parameters = {}, conditions = {})
|
38
|
+
if writable?(key, parameters, conditions)
|
39
|
+
if File.file?(path = pathify(key, parameters))
|
40
|
+
write_file(path, data)
|
41
|
+
else
|
42
|
+
create_path(path) && write_file(path, data)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Fetches cached data by key if it exists. If it does not,
|
48
|
+
# uses passed block to get new cached value and writes it
|
49
|
+
# using given key.
|
50
|
+
def fetch(key, parameters = {}, conditions = {}, &blk)
|
51
|
+
read(key, parameters) || (writable?(key, parameters, conditions) && write(key, value = blk.call, parameters, conditions) && value)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Checks if cached template with given key exists.
|
55
|
+
def exists?(key, parameters = {})
|
56
|
+
File.file?(pathify(key, parameters))
|
57
|
+
end
|
58
|
+
|
59
|
+
# Deletes cached template by key using FileUtils#rm.
|
60
|
+
def delete(key, parameters = {})
|
61
|
+
if File.file?(path = pathify(key, parameters))
|
62
|
+
FileUtils.rm(path)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def delete_all
|
67
|
+
raise NotSupportedError
|
68
|
+
end
|
69
|
+
|
70
|
+
def pathify(key, parameters = {})
|
71
|
+
if key.to_s =~ /^\//
|
72
|
+
path = "#{@dir}#{key}"
|
73
|
+
else
|
74
|
+
path = "#{@dir}/#{key}"
|
75
|
+
end
|
76
|
+
|
77
|
+
path << "--#{parameters.to_sha2}" unless parameters.empty?
|
78
|
+
path
|
79
|
+
end
|
80
|
+
|
81
|
+
protected
|
82
|
+
|
83
|
+
def create_path(path)
|
84
|
+
FileUtils.mkdir_p(File.dirname(path))
|
85
|
+
end
|
86
|
+
|
87
|
+
# Reads file content. Access to the file
|
88
|
+
# uses file locking.
|
89
|
+
def read_file(path)
|
90
|
+
data = nil
|
91
|
+
File.open(path, "r") do |file|
|
92
|
+
file.flock(File::LOCK_EX)
|
93
|
+
data = file.read
|
94
|
+
file.flock(File::LOCK_UN)
|
95
|
+
end
|
96
|
+
|
97
|
+
data
|
98
|
+
end
|
99
|
+
|
100
|
+
# Writes file content. Access to the file
|
101
|
+
# uses file locking.
|
102
|
+
def write_file(path, data)
|
103
|
+
File.open(path, "w+") do |file|
|
104
|
+
file.flock(File::LOCK_EX)
|
105
|
+
file.write(data)
|
106
|
+
file.flock(File::LOCK_UN)
|
107
|
+
end
|
108
|
+
|
109
|
+
true
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'memcached'
|
2
|
+
|
3
|
+
module Merb::Cache
|
4
|
+
# Memcached store uses one or several Memcached
|
5
|
+
# servers for caching. It's flexible and can be used
|
6
|
+
# for fragment caching, action caching, page caching
|
7
|
+
# or object caching.
|
8
|
+
class MemcachedStore < AbstractStore
|
9
|
+
attr_accessor :namespace, :servers, :memcached
|
10
|
+
|
11
|
+
def initialize(config = {})
|
12
|
+
@namespace = config[:namespace]
|
13
|
+
@servers = config[:servers] || ["127.0.0.1:11211"]
|
14
|
+
|
15
|
+
connect(config)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Memcached store consideres all keys and parameters
|
19
|
+
# writable.
|
20
|
+
def writable?(key, parameters = {}, conditions = {})
|
21
|
+
true
|
22
|
+
end
|
23
|
+
|
24
|
+
# Reads key from the cache.
|
25
|
+
def read(key, parameters = {})
|
26
|
+
begin
|
27
|
+
@memcached.get(normalize(key, parameters))
|
28
|
+
rescue Memcached::NotFound, Memcached::Stored
|
29
|
+
nil
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Writes data to the cache using key, parameters and conditions.
|
34
|
+
def write(key, data = nil, parameters = {}, conditions = {})
|
35
|
+
if writable?(key, parameters, conditions)
|
36
|
+
begin
|
37
|
+
@memcached.set(normalize(key, parameters), data, expire_time(conditions))
|
38
|
+
true
|
39
|
+
rescue
|
40
|
+
nil
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Fetches cached data by key if it exists. If it does not,
|
46
|
+
# uses passed block to get new cached value and writes it
|
47
|
+
# using given key.
|
48
|
+
def fetch(key, parameters = {}, conditions = {}, &blk)
|
49
|
+
read(key, parameters) || (writable?(key, parameters, conditions) && write(key, value = blk.call, parameters, conditions) && value)
|
50
|
+
end
|
51
|
+
|
52
|
+
# returns true/false/nil based on if data identified by the key & parameters
|
53
|
+
# is persisted in the store.
|
54
|
+
#
|
55
|
+
# With Memcached 1.2 protocol the only way to
|
56
|
+
# find if key exists in the cache is to read it.
|
57
|
+
# It is very fast and shouldn't be a concern.
|
58
|
+
def exists?(key, parameters = {})
|
59
|
+
begin
|
60
|
+
@memcached.get(normalize(key, parameters)) && true
|
61
|
+
rescue Memcached::Stored
|
62
|
+
true
|
63
|
+
rescue Memcached::NotFound
|
64
|
+
nil
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Deletes entry from cached by key.
|
69
|
+
def delete(key, parameters = {})
|
70
|
+
begin
|
71
|
+
@memcached.delete(normalize(key, parameters))
|
72
|
+
rescue Memcached::NotFound
|
73
|
+
nil
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Flushes the cache.
|
78
|
+
def delete_all
|
79
|
+
@memcached.flush
|
80
|
+
end
|
81
|
+
|
82
|
+
def clone
|
83
|
+
twin = super
|
84
|
+
twin.memcached = @memcached.clone
|
85
|
+
twin
|
86
|
+
end
|
87
|
+
|
88
|
+
# Establishes connection to Memcached.
|
89
|
+
#
|
90
|
+
# Use :buffer_requests option to use bufferring,
|
91
|
+
# :no_block to use non-blocking async I/O.
|
92
|
+
def connect(config = {})
|
93
|
+
@memcached = ::Memcached.new(@servers, config.only(:buffer_requests, :no_block).merge(:namespace => @namespace))
|
94
|
+
end
|
95
|
+
|
96
|
+
# Returns cache key calculated from base key
|
97
|
+
# and SHA2 hex from parameters.
|
98
|
+
def normalize(key, parameters = {})
|
99
|
+
parameters.empty? ? "#{key}" : "#{key}--#{parameters.to_sha2}"
|
100
|
+
end
|
101
|
+
|
102
|
+
# Returns expiration timestamp if :expire_in key is
|
103
|
+
# given.
|
104
|
+
def expire_time(conditions = {})
|
105
|
+
if t = conditions[:expire_in]
|
106
|
+
Time.now + t
|
107
|
+
else
|
108
|
+
0
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# -*- coding: undecided -*-
|
2
|
+
module Merb::Cache
|
3
|
+
# A strategy store wraps one or
|
4
|
+
# more fundamental stores, acting as a middle man between caching
|
5
|
+
# requests.
|
6
|
+
#
|
7
|
+
# For example, if you need to save memory on your Memcache server,
|
8
|
+
# you could wrap your MemcachedStore with a GzipStore. This would
|
9
|
+
# automatically compress the cached data when put into the cache, and
|
10
|
+
# decompress it on the way out. You can even wrap strategy caches
|
11
|
+
# with other strategy caches. If your key was comprised of sensitive
|
12
|
+
# information, like a SSN, you might want to encrypt the key before
|
13
|
+
# storage. Wrapping your GzipStore in a SHA1Store would take
|
14
|
+
# care of that for you.
|
15
|
+
#
|
16
|
+
# The AbstractStore class defines 9 methods as the API:
|
17
|
+
#
|
18
|
+
# writable?(key, parameters = {}, conditions = {})
|
19
|
+
# exists?(key, parameters = {})
|
20
|
+
# read(key, parameters = {})
|
21
|
+
# write(key, data = nil, parameters = {}, conditions = {})
|
22
|
+
# write_all(key, data = nil, parameters = {}, conditions = {})
|
23
|
+
# fetch(key, parameters = {}, conditions = {}, &blk)
|
24
|
+
# delete(key, parameters = {})
|
25
|
+
# delete_all
|
26
|
+
# delete_all!
|
27
|
+
#
|
28
|
+
# AbstractStrategyStore implements all of these with the exception
|
29
|
+
# of delete_all. If a strategy store can guarantee that calling
|
30
|
+
# delete_all on it’s wrapped store(s) will only delete entries
|
31
|
+
# populated by the strategy store, it may define the safe
|
32
|
+
# version of delete_all. However, this is usually not the
|
33
|
+
# case, hence delete_all is not part of the
|
34
|
+
# public API for AbstractStrategyStore.
|
35
|
+
class AbstractStrategyStore < AbstractStore
|
36
|
+
# START: interface for creating strategy stores. This should/might change.
|
37
|
+
def self.contextualize(*stores)
|
38
|
+
Class.new(self) do
|
39
|
+
cattr_accessor :contextualized_stores
|
40
|
+
|
41
|
+
self.contextualized_stores = stores
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class << self
|
46
|
+
alias_method :[], :contextualize
|
47
|
+
end
|
48
|
+
|
49
|
+
attr_accessor :stores
|
50
|
+
|
51
|
+
def initialize(config = {})
|
52
|
+
@stores = contextualized_stores.map do |cs|
|
53
|
+
case cs
|
54
|
+
when Symbol
|
55
|
+
Merb::Cache[cs]
|
56
|
+
when Class
|
57
|
+
cs.new(config)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# END: interface for creating strategy stores.
|
63
|
+
|
64
|
+
attr_accessor :stores
|
65
|
+
|
66
|
+
# determines if the store is able to persist data identified by the key & parameters
|
67
|
+
# with the given conditions.
|
68
|
+
def writable?(key, parameters = {}, conditions = {})
|
69
|
+
raise NotImplementedError
|
70
|
+
end
|
71
|
+
|
72
|
+
# gets the data from the store identified by the key & parameters.
|
73
|
+
# return nil if the entry does not exist.
|
74
|
+
def read(key, parameters = {})
|
75
|
+
raise NotImplementedError
|
76
|
+
end
|
77
|
+
|
78
|
+
# persists the data so that it can be retrieved by the key & parameters.
|
79
|
+
# returns nil if it is unable to persist the data.
|
80
|
+
# returns true if successful.
|
81
|
+
def write(key, data = nil, parameters = {}, conditions = {})
|
82
|
+
raise NotImplementedError
|
83
|
+
end
|
84
|
+
|
85
|
+
# persists the data to all context stores.
|
86
|
+
# returns nil if none of the stores were able to persist the data.
|
87
|
+
# returns true if at least one write was successful.
|
88
|
+
def write_all(key, data = nil, parameters = {}, conditions = {})
|
89
|
+
raise NotImplementedError
|
90
|
+
end
|
91
|
+
|
92
|
+
# tries to read the data from the store. If that fails, it calls
|
93
|
+
# the block parameter and persists the result.
|
94
|
+
def fetch(key, parameters = {}, conditions = {}, &blk)
|
95
|
+
raise NotImplementedError
|
96
|
+
end
|
97
|
+
|
98
|
+
# returns true/false/nil based on if data identified by the key & parameters
|
99
|
+
# is persisted in the store.
|
100
|
+
def exists?(key, parameters = {})
|
101
|
+
raise NotImplementedError
|
102
|
+
end
|
103
|
+
|
104
|
+
# deletes the entry for the key & parameter from the store.
|
105
|
+
def delete(key, parameters = {})
|
106
|
+
raise NotImplementedError
|
107
|
+
end
|
108
|
+
|
109
|
+
# deletes all entries for the key & parameters for the store.
|
110
|
+
# considered dangerous because strategy stores which call delete_all!
|
111
|
+
# on their context stores could delete other store's entrees.
|
112
|
+
def delete_all!
|
113
|
+
raise NotImplementedError
|
114
|
+
end
|
115
|
+
|
116
|
+
def clone
|
117
|
+
twin = super
|
118
|
+
twin.stores = self.stores.map {|s| s.clone}
|
119
|
+
twin
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Merb::Cache
|
2
|
+
# Store well suited for action caching.
|
3
|
+
class ActionStore < AbstractStrategyStore
|
4
|
+
def writable?(dispatch, parameters = {}, conditions = {})
|
5
|
+
case dispatch
|
6
|
+
when Merb::Controller
|
7
|
+
@stores.any?{|s| s.writable?(normalize(dispatch), parameters, conditions)}
|
8
|
+
else false
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def read(dispatch, parameters = {})
|
13
|
+
if writable?(dispatch, parameters)
|
14
|
+
@stores.capture_first {|s| s.read(normalize(dispatch), parameters)}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def write(dispatch, data = nil, parameters = {}, conditions = {})
|
19
|
+
if writable?(dispatch, parameters, conditions)
|
20
|
+
@stores.capture_first {|s| s.write(normalize(dispatch), data || dispatch.body, parameters, conditions)}
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def write_all(dispatch, data = nil, parameters = {}, conditions = {})
|
25
|
+
if writable?(dispatch, parameters, conditions)
|
26
|
+
@stores.map {|s| s.write_all(normalize(dispatch), data || dispatch.body, parameters, conditions)}.all?
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def fetch(dispatch, parameters = {}, conditions = {}, &blk)
|
31
|
+
if writable?(dispatch, parameters, conditions)
|
32
|
+
read(dispatch, parameters) || @stores.capture_first {|s| s.fetch(normalize(dispatch), data || dispatch.body, parameters, conditions, &blk)}
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def exists?(dispatch, parameters = {})
|
37
|
+
if writable?(dispatch, parameters)
|
38
|
+
@stores.capture_first {|s| s.exists?(normalize(dispatch), parameters)}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def delete(dispatch, parameters = {})
|
43
|
+
if writable?(dispatch, parameters)
|
44
|
+
@stores.map {|s| s.delete(normalize(dispatch), parameters)}.any?
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def delete_all!
|
49
|
+
@stores.map {|s| s.delete_all!}.all?
|
50
|
+
end
|
51
|
+
|
52
|
+
def normalize(dispatch)
|
53
|
+
"#{dispatch.class.name}##{dispatch.action_name}" unless dispatch.class.name.blank? || dispatch.action_name.blank?
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|