bchiu-merb_cache_more 0.9.4

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.
@@ -0,0 +1,195 @@
1
+ class Merb::Cache::MemcacheStore
2
+ # Provides the memcache cache store for merb_cache_more
3
+
4
+ def initialize
5
+ @config = Merb::Controller._cache.config
6
+ prepare
7
+ end
8
+
9
+ class NotReady < Exception
10
+ def initialize
11
+ super("Memcache server is not ready")
12
+ end
13
+ end
14
+
15
+ class NotDefined < Exception
16
+ def initialize
17
+ super("Memcache is not defined (require it in init.rb)")
18
+ end
19
+ end
20
+
21
+ # This method is there to ensure minimal requirements are met
22
+ # (directories are accessible, table exists, connected to server, ...)
23
+ def prepare
24
+ namespace = @config[:namespace] || 'merb_cache_more'
25
+ host = @config[:host] || '127.0.0.1:11211'
26
+ @memcache = MemCache.new(host, {:namespace => namespace})
27
+ @tracking_key = "_#{namespace}_keys" unless @config[:no_tracking]
28
+ raise NotReady unless @memcache.active?
29
+ true
30
+ rescue NameError
31
+ raise NotDefined
32
+ end
33
+
34
+ # Checks whether a cache entry exists
35
+ #
36
+ # ==== Parameter
37
+ # key<String>:: The key identifying the cache entry
38
+ #
39
+ # ==== Returns
40
+ # true if the cache entry exists, false otherwise
41
+ def cached?(key)
42
+ not @memcache.get(key).nil?
43
+ end
44
+
45
+ # Capture or restore the data in cache.
46
+ # If the cache entry expired or does not exist, the data are taken
47
+ # from the execution of the block, marshalled and stored in cache.
48
+ # Otherwise, the data are loaded from the cache and returned unmarshalled
49
+ #
50
+ # ==== Parameters
51
+ # _controller<Merb::Controller>:: The instance of the current controller
52
+ # key<String>:: The key identifying the cache entry
53
+ # from_now<~minutes>::
54
+ # The number of minutes (from now) the cache should persist
55
+ # &block:: The template to be used or not
56
+ #
57
+ # ==== Additional information
58
+ # When fetching data (the cache entry exists and has not expired)
59
+ # The data are loaded from the cache and returned unmarshalled.
60
+ # Otherwise:
61
+ # The controller is used to capture the rendered template (from the block).
62
+ # It uses the capture_#{engine} and concat_#{engine} methods to do so.
63
+ # The captured data are then marshalled and stored.
64
+ def cache(_controller, key, from_now = nil, &block)
65
+ _data = @memcache.get(key)
66
+ if _data.nil?
67
+ _expire = from_now ? from_now.minutes.from_now.to_i : 0
68
+ _data = _controller.send(:capture, &block)
69
+ @memcache.set(key, _data, _expire)
70
+ end
71
+ _controller.send(:concat, _data, block.binding)
72
+ true
73
+ end
74
+
75
+ # Store data to memcache using the specified key
76
+ #
77
+ # ==== Parameters
78
+ # key<Sting>:: The key identifying the cache entry
79
+ # data<String>:: The data to be put in cache
80
+ # from_now<~minutes>::
81
+ # The number of minutes (from now) the cache should persist
82
+ def cache_set(key, data, from_now = nil)
83
+ _expire = from_now ? from_now.minutes.from_now.to_i : 0
84
+ @memcache.set(key, data, _expire)
85
+ cache_start_tracking(key)
86
+ Merb.logger.info("cache: set (#{key})")
87
+ true
88
+ end
89
+
90
+ # Fetch data from memcache using the specified key
91
+ # The entry is deleted if it has expired
92
+ #
93
+ # ==== Parameter
94
+ # key<Sting>:: The key identifying the cache entry
95
+ #
96
+ # ==== Returns
97
+ # data<String, NilClass>::
98
+ # nil is returned whether the entry expired or was not found
99
+ def cache_get(key)
100
+ data = @memcache.get(key)
101
+ Merb.logger.info("cache: #{data.nil? ? "miss" : "hit"} (#{key})")
102
+ data
103
+ end
104
+
105
+ # Expire the cache entry identified by the given key
106
+ #
107
+ # ==== Parameter
108
+ # key<Sting>:: The key identifying the cache entry
109
+ def expire(key)
110
+ @memcache.delete(key)
111
+ cache_stop_tracking(key)
112
+ Merb.logger.info("cache: expired (#{key})")
113
+ true
114
+ end
115
+
116
+ # Expire the cache entries matching the given key
117
+ #
118
+ # ==== Parameter
119
+ # key<Sting>:: The key matching the cache entries
120
+ #
121
+ # ==== Additional info
122
+ # In memcache this requires to keep track of all keys (on by default).
123
+ # If you don't need this, set :no_tracking => true in the config.
124
+ def expire_match(key)
125
+ if @tracking_key
126
+ for _key in get_tracked_keys
127
+ expire(_key) if /#{key}/ =~ _key
128
+ end
129
+ else
130
+ Merb.logger.info("cache: expire_match is not supported with memcache (set :no_tracking => false in your config")
131
+ end
132
+ true
133
+ end
134
+
135
+ # Expire all the cache entries
136
+ def expire_all
137
+ @memcache.flush_all
138
+ stop_tracking_keys
139
+ Merb.logger.info("cache: expired all")
140
+ true
141
+ end
142
+
143
+ # Gives info on the current cache store
144
+ #
145
+ # ==== Returns
146
+ # The type of the current cache store
147
+ def cache_store_type
148
+ "memcache"
149
+ end
150
+
151
+ private
152
+
153
+ # Store the tracked keys in memcache (used by expire_match)
154
+ #
155
+ # ==== Parameter
156
+ # keys<Array[String]>:: The keys to keep track of
157
+ def set_tracked_keys(keys)
158
+ @memcache.set(@tracking_key, keys)
159
+ end
160
+
161
+ # Retrieve tracked keys from memcache
162
+ #
163
+ # ==== Returns
164
+ # keys<Array[String]>:: The tracked keys
165
+ def get_tracked_keys
166
+ @memcache.get(@tracking_key) || []
167
+ end
168
+
169
+ # Remove all tracked keys
170
+ def stop_tracking_keys
171
+ @memcache.delete(@tracking_key) if @tracking_key
172
+ end
173
+
174
+ # Add a key in the array of tracked keys (used by expire_match)
175
+ #
176
+ # ==== Parameter
177
+ # key<String>:: the key to add
178
+ def cache_start_tracking(key)
179
+ return unless @tracking_key
180
+ keys = get_tracked_keys
181
+ keys.push(key)
182
+ set_tracked_keys(keys)
183
+ end
184
+
185
+ # Remove a key from the array of tracked keys (used by expire_match)
186
+ #
187
+ # ==== Parameter
188
+ # key<String>:: the key to remove
189
+ def cache_stop_tracking(key)
190
+ return unless @tracking_key
191
+ keys = get_tracked_keys
192
+ keys.delete(key)
193
+ set_tracked_keys(keys)
194
+ end
195
+ end
@@ -0,0 +1,168 @@
1
+ class Merb::Cache::MemoryStore
2
+ # Provides the memory cache store for merb_cache_more
3
+
4
+ def initialize
5
+ @config = Merb::Controller._cache.config
6
+ @cache = {}
7
+ @mutex = Mutex.new
8
+ prepare
9
+ end
10
+
11
+ # This method is there to ensure minimal requirements are met
12
+ # (directories are accessible, table exists, connected to server, ...)
13
+ def prepare
14
+ true
15
+ end
16
+
17
+ # Checks whether a cache entry exists
18
+ #
19
+ # ==== Parameter
20
+ # key<String>:: The key identifying the cache entry
21
+ #
22
+ # ==== Returns
23
+ # true if the cache entry exists, false otherwise
24
+ def cached?(key)
25
+ if @cache.key?(key)
26
+ _data, _expire = *cache_read(key)
27
+ return true if _expire.nil? || Time.now < _expire
28
+ expire(key)
29
+ end
30
+ false
31
+ end
32
+
33
+ # Capture or restore the data in cache.
34
+ # If the cache entry expired or does not exist, the data are taken
35
+ # from the execution of the block, marshalled and stored in cache.
36
+ # Otherwise, the data are loaded from the cache and returned unmarshalled
37
+ #
38
+ # ==== Parameters
39
+ # _controller<Merb::Controller>:: The instance of the current controller
40
+ # key<String>:: The key identifying the cache entry
41
+ # from_now<~minutes>::
42
+ # The number of minutes (from now) the cache should persist
43
+ # &block:: The template to be used or not
44
+ #
45
+ # ==== Additional information
46
+ # When fetching data (the cache entry exists and has not expired)
47
+ # The data are loaded from the cache and returned unmarshalled.
48
+ # Otherwise:
49
+ # The controller is used to capture the rendered template (from the block).
50
+ # It uses the capture_#{engine} and concat_#{engine} methods to do so.
51
+ # The captured data are then marshalled and stored.
52
+ def cache(_controller, key, from_now = nil, &block)
53
+ if @cache.key?(key)
54
+ _data, _expire = *cache_read(key)
55
+ _cache_hit = _expire.nil? || Time.now < _expire
56
+ end
57
+ unless _cache_hit
58
+ _expire = from_now ? from_now.minutes.from_now : nil
59
+ _data = _controller.send(:capture, &block)
60
+ cache_write(key, [_data, _expire])
61
+ end
62
+ _controller.send(:concat, _data, block.binding)
63
+ true
64
+ end
65
+
66
+ # Store data to the file using the specified key
67
+ #
68
+ # ==== Parameters
69
+ # key<Sting>:: The key identifying the cache entry
70
+ # data<String>:: The data to be put in cache
71
+ # from_now<~minutes>::
72
+ # The number of minutes (from now) the cache should persist
73
+ def cache_set(key, data, from_now = nil)
74
+ _expire = from_now ? from_now.minutes.from_now : nil
75
+ cache_write(key, [data, _expire])
76
+ Merb.logger.info("cache: set (#{key})")
77
+ true
78
+ end
79
+
80
+ # Fetch data from the file using the specified key
81
+ # The entry is deleted if it has expired
82
+ #
83
+ # ==== Parameter
84
+ # key<Sting>:: The key identifying the cache entry
85
+ #
86
+ # ==== Returns
87
+ # data<String, NilClass>::
88
+ # nil is returned whether the entry expired or was not found
89
+ def cache_get(key)
90
+ if @cache.key?(key)
91
+ _data, _expire = *cache_read(key)
92
+ if _expire.nil? || Time.now < _expire
93
+ Merb.logger.info("cache: hit (#{key})")
94
+ return _data
95
+ end
96
+ @mutex.synchronize do @cache.delete(key) end
97
+ end
98
+ Merb.logger.info("cache: miss (#{key})")
99
+ nil
100
+ end
101
+
102
+ # Expire the cache entry identified by the given key
103
+ #
104
+ # ==== Parameter
105
+ # key<Sting>:: The key identifying the cache entry
106
+ def expire(key)
107
+ @mutex.synchronize do
108
+ @cache.delete(key)
109
+ end
110
+ Merb.logger.info("cache: expired (#{key})")
111
+ true
112
+ end
113
+
114
+ # Expire the cache entries matching the given key
115
+ #
116
+ # ==== Parameter
117
+ # key<Sting>:: The key matching the cache entries
118
+ def expire_match(key)
119
+ @mutex.synchronize do
120
+ @cache.delete_if do |k,v| k.match(/#{key}/) end
121
+ end
122
+ Merb.logger.info("cache: expired matching (#{key})")
123
+ true
124
+ end
125
+
126
+ # Expire all the cache entries
127
+ def expire_all
128
+ @mutex.synchronize do
129
+ @cache.clear
130
+ end
131
+ Merb.logger.info("cache: expired all")
132
+ true
133
+ end
134
+
135
+ # Gives info on the current cache store
136
+ #
137
+ # ==== Returns
138
+ # The type of the current cache store
139
+ def cache_store_type
140
+ "memory"
141
+ end
142
+
143
+ private
144
+
145
+ # Read data from the memory hash table using mutex
146
+ #
147
+ # ==== Parameters
148
+ # cache_file<String>:: The key identifying the cache entry
149
+ #
150
+ # ==== Returns
151
+ # _data<String>:: The data fetched from the cache
152
+ def cache_read(key)
153
+ @mutex.synchronize do
154
+ @cache[key]
155
+ end
156
+ end
157
+
158
+ # Write data to the memory hash table using mutex
159
+ #
160
+ # ==== Parameters
161
+ # cache_file<String>:: The key identifying the cache entry
162
+ # data<String>:: The data to be put in cache
163
+ def cache_write(key, data)
164
+ @mutex.synchronize do
165
+ @cache[key] = data
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,239 @@
1
+ class Merb::Cache
2
+ attr_reader :config, :store
3
+
4
+ class StoreNotFound < Exception
5
+ def initialize(cache_store)
6
+ super("cache_store (#{cache_store}) not found (not implemented?)")
7
+ end
8
+ end
9
+
10
+ DEFAULT_CONFIG = {
11
+ :cache_html_directory => Merb.dir_for(:public) / "cache",
12
+
13
+ #:store => "database",
14
+ #:table_name => "merb_cache",
15
+
16
+ #:disable => "development", # disable caching for development
17
+ #:disable => true, # disable caching for all environments
18
+
19
+ :store => "file",
20
+ :cache_directory => Merb.root_path("tmp/cache"),
21
+
22
+ #:store => "memcache",
23
+ #:host => "127.0.0.1:11211",
24
+ #:namespace => "merb_cache",
25
+ #:track_keys => true,
26
+
27
+ #:store => "memory",
28
+ # store could be: file, memcache, memory, database, dummy, ...
29
+
30
+ # can be nil|:snake|:tree|:hash|:query or a custom string
31
+ # such as ":paramname1/:paramname2_and_:paramname3"
32
+ #:cache_key_format => :snake,
33
+
34
+ # expiration time in minutes
35
+ #:cache_action_ttl => 10,
36
+ #:cache_page_ttl => 10
37
+ }
38
+
39
+ # Called in the after_app_loads loop and instantiate the right backend
40
+ #
41
+ # ==== Raises
42
+ # Store#NotFound::
43
+ # If the cache_store mentionned in the config is unknown
44
+ def start
45
+ @config = DEFAULT_CONFIG.merge(Merb::Plugins.config[:merb_cache] || {})
46
+ if @config[:disable] == true || Merb.environment == @config[:disable]
47
+ config[:disable_page_caching] = true
48
+ config[:store] = "dummy"
49
+ end
50
+ @config[:cache_html_directory] ||= Merb.dir_for(:public) / "cache"
51
+ require "merb_cache_more/cache-store/#{@config[:store]}"
52
+ @store = Merb::Cache.const_get("#{@config[:store].capitalize}Store").new
53
+ Merb.logger.info("Using #{@config[:store]} cache")
54
+ rescue LoadError
55
+ raise Merb::Cache::StoreNotFound, @config[:store].inspect
56
+ end
57
+
58
+ # Compute a cache key and yield it to the given block
59
+ # It is used by the #expire_page, #expire_action and #expire methods.
60
+ #
61
+ # ==== Parameters
62
+ # options<String, Hash>:: The key or the Hash that will be used to build the key
63
+ # controller<String>:: The name of the controller
64
+ # controller_based<Boolean>:: only used by action and page caching
65
+ #
66
+ # ==== Options (options)
67
+ # :key<String>:: The complete or partial key that will be computed.
68
+ # :action<String>:: The action name that will be used to compute the key
69
+ # :controller<String>:: The controller name that will be part of the key
70
+ # :params<Array[String]>::
71
+ # The params will be joined together (with '/') and added to the key
72
+ # :match<Boolean, String>::
73
+ # true, it will try to match multiple cache entries
74
+ # string, shortcut for {:key => "mykey", :match => true}
75
+ #
76
+ # ==== Examples
77
+ # expire(:key => "root_key", :params => [session[:me], params[:id]])
78
+ # expire(:match => "root_key")
79
+ # expire_action(:action => 'list')
80
+ # expire_page(:action => 'show', :controller => 'news')
81
+ #
82
+ # ==== Returns
83
+ # The result of the given block
84
+ #
85
+ def expire_key_for(options, controller, controller_based = false)
86
+ key = ""
87
+ if options.is_a? Hash
88
+ case
89
+ when key = options[:key]
90
+ when action = options[:action]
91
+ controller = options[:controller] || controller
92
+ key = "/#{controller}/#{action}"
93
+ when match = options[:match]
94
+ key = match
95
+ end
96
+ if _params = options[:params]
97
+ key += "/" + _params.join("/")
98
+ end
99
+ yield key, !options[:match].nil?
100
+ else
101
+ yield controller_based ? "/#{controller}/#{options}" : options, false
102
+ end
103
+ end
104
+
105
+ # Compute a cache key based on the given parameters
106
+ # Only used by the #cached_page?, #cached_action?, #cached?, #cache,
107
+ # #cache_get and #cache_set methods
108
+ #
109
+ # ==== Parameters
110
+ # options<String, Hash>:: The key or the Hash that will be used to build the key
111
+ # controller<String>:: The name of the controller
112
+ # controller_based<Boolean>:: only used by action and page caching
113
+ #
114
+ # ==== Options (options)
115
+ # :key<String>:: The complete or partial key that will be computed.
116
+ # :format<String>:: The formatting style that will be used for the key.
117
+ # :action<String>:: The action name that will be used to compute the key
118
+ # :controller<String>:: The controller name that will be part of the key
119
+ # :params<Array[String]>::
120
+ # The params will be joined together (with '/') and added to the key
121
+ #
122
+ # ==== Examples
123
+ # cache_set("my_key", @data)
124
+ # cache_get(:key => "root_key", :params => [session[:me], params[:id]])
125
+ # cache_get(:key => "root_key", :format => nil|:snake|:hash|:query|:tree)
126
+ # cache_get(:key => "root_key", :format => ":one_:two[a]_and_:three[]")
127
+ #
128
+ # ==== Returns
129
+ # The computed key
130
+ # def key_for(options, controller = nil, controller_based = false)
131
+ # key = ""
132
+ # if options.is_a? Hash
133
+ # case
134
+ # when key = options[:key]
135
+ # when action = options[:action]
136
+ # controller = options[:controller] || controller
137
+ # key = "/#{controller}/#{action}"
138
+ # end
139
+ #
140
+ # _params = options[:params]
141
+ # _format = options[:format]
142
+ #
143
+ # if _params && !_params.empty?
144
+ # vals = _params.to_a.map {|p| String === p ? p : (Hash === p[1] ? p[1].values : p[1]) }.flatten
145
+ # case _format
146
+ # when nil
147
+ # _params = _params.to_a.flatten.join('_').gsub('/','%2F') # key1_val1_key2_val2
148
+ # when :snake
149
+ # _params = vals.join('_').gsub('/','%2F') # val1_val2
150
+ # when :hash
151
+ # _params = Digest::MD5.hexdigest(_params.to_s) # 32-bit md5 hash
152
+ # when :query
153
+ # _params = Merb::Request.params_to_query_string(_params) # key1=val1&key2=val2
154
+ # when :tree
155
+ # _params = vals[0..10].map {|v| v.gsub('/','%2F')}.join('/') # /val1/val2
156
+ # else
157
+ # _params.each do |k,v|
158
+ # _format.sub!(":#{k}", v.gsub('/','%2F')) if String === v
159
+ # _format.sub!(":#{k}[]", v[0].gsub('/','%2F')) if Array === v
160
+ # v.each {|kk,vv| _format.sub!(":#{k}[#{kk}]", vv.gsub('/','%2F')) } if Hash === v
161
+ # end
162
+ # _params = _format
163
+ # end
164
+ # key += '/' + sanitize_cache_key(_params) unless _params.blank?
165
+ # else
166
+ # #key += '/index' # all pages in one dir
167
+ # end
168
+ # else
169
+ # key = controller_based ? "/#{controller}/#{options}" : options
170
+ # end
171
+ # key
172
+ # end
173
+ #
174
+ def key_for(options, controller = nil, controller_based = false)
175
+ key = ""
176
+ if options.is_a? Hash
177
+ case
178
+ when key = options[:key]
179
+ when action = options[:action]
180
+ controller = options[:controller] || controller
181
+ key = "/#{controller}/#{action}"
182
+ end
183
+
184
+ _params = options[:params]
185
+ _format = options[:format]
186
+
187
+ if _params && !_params.empty?
188
+ vals = _params.to_a.map {|p| String === p ? p : (Hash === p[1] ? p[1].values : p[1]) }.flatten
189
+ case _format
190
+ when nil
191
+ _params = _params.to_a.flatten.join('_').gsub('/','%2F') # key1_val1_key2_val2
192
+ when :snake
193
+ _params = vals.join('_').gsub('/','%2F') # val1_val2
194
+ when :hash
195
+ _params = Digest::MD5.hexdigest(_params.to_s) # 32-bit md5 hash
196
+ when :query
197
+ _params = Merb::Request.params_to_query_string(_params) # key1=val1&key2=val2
198
+ when :tree
199
+ _params = vals[0..10].map {|v| v.gsub('/','%2F')}.join('/') # /val1/val2
200
+ else
201
+ _params.each do |k,v|
202
+ _format.sub!(":#{k}", v.gsub('/','%2F')) if String === v
203
+ _format.sub!(":#{k}[]", v[0].gsub('/','%2F')) if Array === v
204
+ v.each {|kk,vv| _format.sub!(":#{k}[#{kk}]", vv.gsub('/','%2F')) } if Hash === v
205
+ end
206
+ _params = _format
207
+ end
208
+ key += '/' + sanitize_cache_key(_params) unless _params.blank?
209
+ else
210
+ #key += '/index' # all pages in one dir
211
+ end
212
+ else
213
+ key = controller_based ? "/#{controller}/#{options}" : options
214
+ end
215
+ key
216
+ end
217
+
218
+ # Removes illegal chars from key and allows cached pages to be safely
219
+ # stored on any filesystem. You may override this method in your app
220
+ # to match how filenames are escaped by your web server.
221
+ #
222
+ # ==== Parameters
223
+ # key<String>:: The key to sanitize
224
+ #
225
+ # ==== Returns
226
+ # The sanitized key
227
+ #
228
+ def sanitize_cache_key(key = "")
229
+ key.gsub(/[\\\:\*\?\"\<\>\|\s]/,'_')
230
+ end
231
+
232
+ module ControllerInstanceMethods
233
+ # Mixed in Merb::Controller and provides expire_all for action and fragment caching.
234
+ def expire_all
235
+ Merb::Controller._cache.store.expire_all
236
+ end
237
+ end
238
+ end
239
+