josh-rack-cache 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,52 @@
1
+ require 'rack/utils'
2
+
3
+ module Rack::Cache
4
+ class Key
5
+ include Rack::Utils
6
+
7
+ # Implement .call, since it seems like the "Rack-y" thing to do. Plus, it
8
+ # opens the door for cache key generators to just be blocks.
9
+ def self.call(request)
10
+ new(request).generate
11
+ end
12
+
13
+ def initialize(request)
14
+ @request = request
15
+ end
16
+
17
+ # Generate a normalized cache key for the request.
18
+ def generate
19
+ parts = []
20
+ parts << @request.scheme << "://"
21
+ parts << @request.host
22
+
23
+ if @request.scheme == "https" && @request.port != 443 ||
24
+ @request.scheme == "http" && @request.port != 80
25
+ parts << ":" << @request.port.to_s
26
+ end
27
+
28
+ parts << @request.script_name
29
+ parts << @request.path_info
30
+
31
+ if qs = query_string
32
+ parts << "?"
33
+ parts << qs
34
+ end
35
+
36
+ parts.join
37
+ end
38
+
39
+ private
40
+ # Build a normalized query string by alphabetizing all keys/values
41
+ # and applying consistent escaping.
42
+ def query_string
43
+ return nil if @request.query_string.nil?
44
+
45
+ @request.query_string.split(/[&;] */n).
46
+ map { |p| unescape(p).split('=', 2) }.
47
+ sort.
48
+ map { |k,v| "#{escape(k)}=#{escape(v)}" }.
49
+ join('&')
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,407 @@
1
+ require 'fileutils'
2
+ require 'digest/sha1'
3
+ require 'rack/utils'
4
+
5
+ module Rack::Cache
6
+
7
+ # The MetaStore is responsible for storing meta information about a
8
+ # request/response pair keyed by the request's URL.
9
+ #
10
+ # The meta store keeps a list of request/response pairs for each canonical
11
+ # request URL. A request/response pair is a two element Array of the form:
12
+ # [request, response]
13
+ #
14
+ # The +request+ element is a Hash of Rack environment keys. Only protocol
15
+ # keys (i.e., those that start with "HTTP_") are stored. The +response+
16
+ # element is a Hash of cached HTTP response headers for the paired request.
17
+ #
18
+ # The MetaStore class is abstract and should not be instanstiated
19
+ # directly. Concrete subclasses should implement the protected #read,
20
+ # #write, and #purge methods. Care has been taken to keep these low-level
21
+ # methods dumb and straight-forward to implement.
22
+ class MetaStore
23
+
24
+ # Locate a cached response for the request provided. Returns a
25
+ # Rack::Cache::Response object if the cache hits or nil if no cache entry
26
+ # was found.
27
+ def lookup(request, entity_store)
28
+ key = cache_key(request)
29
+ entries = read(key)
30
+
31
+ # bail out if we have nothing cached
32
+ return nil if entries.empty?
33
+
34
+ # find a cached entry that matches the request.
35
+ env = request.env
36
+ match = entries.detect{|req,res| requests_match?(res['Vary'], env, req)}
37
+ return nil if match.nil?
38
+
39
+ req, res = match
40
+ if body = entity_store.open(res['X-Content-Digest'])
41
+ restore_response(res, body)
42
+ else
43
+ # TODO the metastore referenced an entity that doesn't exist in
44
+ # the entitystore. we definitely want to return nil but we should
45
+ # also purge the entry from the meta-store when this is detected.
46
+ end
47
+ end
48
+
49
+ # Write a cache entry to the store under the given key. Existing
50
+ # entries are read and any that match the response are removed.
51
+ # This method calls #write with the new list of cache entries.
52
+ def store(request, response, entity_store)
53
+ key = cache_key(request)
54
+ stored_env = persist_request(request)
55
+
56
+ # write the response body to the entity store if this is the
57
+ # original response.
58
+ if response.headers['X-Content-Digest'].nil?
59
+ digest, size = entity_store.write(response.body)
60
+ response.headers['X-Content-Digest'] = digest
61
+ response.headers['Content-Length'] = size.to_s unless response.headers['Transfer-Encoding']
62
+ response.body = entity_store.open(digest)
63
+ end
64
+
65
+ # read existing cache entries, remove non-varying, and add this one to
66
+ # the list
67
+ vary = response.vary
68
+ entries =
69
+ read(key).reject do |env,res|
70
+ (vary == res['Vary']) &&
71
+ requests_match?(vary, env, stored_env)
72
+ end
73
+
74
+ headers = persist_response(response)
75
+ headers.delete 'Age'
76
+
77
+ entries.unshift [stored_env, headers]
78
+ write key, entries
79
+ key
80
+ end
81
+
82
+ # Generate a cache key for the request.
83
+ def cache_key(request)
84
+ keygen = request.env['rack-cache.cache_key'] || Key
85
+ keygen.call(request)
86
+ end
87
+
88
+ # Invalidate all cache entries that match the request.
89
+ def invalidate(request, entity_store)
90
+ modified = false
91
+ key = cache_key(request)
92
+ entries =
93
+ read(key).map do |req, res|
94
+ response = restore_response(res)
95
+ if response.fresh?
96
+ response.expire!
97
+ modified = true
98
+ [req, persist_response(response)]
99
+ else
100
+ [req, res]
101
+ end
102
+ end
103
+ write key, entries if modified
104
+ end
105
+
106
+ private
107
+
108
+ # Extract the environment Hash from +request+ while making any
109
+ # necessary modifications in preparation for persistence. The Hash
110
+ # returned must be marshalable.
111
+ def persist_request(request)
112
+ env = request.env.dup
113
+ env.reject! { |key,val| key =~ /[^0-9A-Z_]/ }
114
+ env
115
+ end
116
+
117
+ # Converts a stored response hash into a Response object. The caller
118
+ # is responsible for loading and passing the body if needed.
119
+ def restore_response(hash, body=nil)
120
+ status = hash.delete('X-Status').to_i
121
+ Rack::Cache::Response.new(status, hash, body)
122
+ end
123
+
124
+ def persist_response(response)
125
+ hash = response.headers.to_hash
126
+ hash['X-Status'] = response.status.to_s
127
+ hash
128
+ end
129
+
130
+ # Determine whether the two environment hashes are non-varying based on
131
+ # the vary response header value provided.
132
+ def requests_match?(vary, env1, env2)
133
+ return true if vary.nil? || vary == ''
134
+ vary.split(/[\s,]+/).all? do |header|
135
+ key = "HTTP_#{header.upcase.tr('-', '_')}"
136
+ env1[key] == env2[key]
137
+ end
138
+ end
139
+
140
+ protected
141
+ # Locate all cached request/response pairs that match the specified
142
+ # URL key. The result must be an Array of all cached request/response
143
+ # pairs. An empty Array must be returned if nothing is cached for
144
+ # the specified key.
145
+ def read(key)
146
+ raise NotImplemented
147
+ end
148
+
149
+ # Store an Array of request/response pairs for the given key. Concrete
150
+ # implementations should not attempt to filter or concatenate the
151
+ # list in any way.
152
+ def write(key, negotiations)
153
+ raise NotImplemented
154
+ end
155
+
156
+ # Remove all cached entries at the key specified. No error is raised
157
+ # when the key does not exist.
158
+ def purge(key)
159
+ raise NotImplemented
160
+ end
161
+
162
+ private
163
+ # Generate a SHA1 hex digest for the specified string. This is a
164
+ # simple utility method for meta store implementations.
165
+ def hexdigest(data)
166
+ Digest::SHA1.hexdigest(data)
167
+ end
168
+
169
+ public
170
+ # Concrete MetaStore implementation that uses a simple Hash to store
171
+ # request/response pairs on the heap.
172
+ class Heap < MetaStore
173
+ def initialize(hash={})
174
+ @hash = hash
175
+ end
176
+
177
+ def read(key)
178
+ @hash.fetch(key, []).collect do |req,res|
179
+ [req.dup, res.dup]
180
+ end
181
+ end
182
+
183
+ def write(key, entries)
184
+ @hash[key] = entries
185
+ end
186
+
187
+ def purge(key)
188
+ @hash.delete(key)
189
+ nil
190
+ end
191
+
192
+ def to_hash
193
+ @hash
194
+ end
195
+
196
+ def self.resolve(uri)
197
+ new
198
+ end
199
+ end
200
+
201
+ HEAP = Heap
202
+ MEM = HEAP
203
+
204
+ # Concrete MetaStore implementation that stores request/response
205
+ # pairs on disk.
206
+ class Disk < MetaStore
207
+ attr_reader :root
208
+
209
+ def initialize(root="/tmp/rack-cache/meta-#{ARGV[0]}")
210
+ @root = File.expand_path(root)
211
+ FileUtils.mkdir_p(root, :mode => 0755)
212
+ end
213
+
214
+ def read(key)
215
+ path = key_path(key)
216
+ File.open(path, 'rb') { |io| Marshal.load(io) }
217
+ rescue Errno::ENOENT
218
+ []
219
+ end
220
+
221
+ def write(key, entries)
222
+ path = key_path(key)
223
+ File.open(path, 'wb') { |io| Marshal.dump(entries, io, -1) }
224
+ rescue Errno::ENOENT
225
+ Dir.mkdir(File.dirname(path), 0755)
226
+ retry
227
+ end
228
+
229
+ def purge(key)
230
+ path = key_path(key)
231
+ File.unlink(path)
232
+ nil
233
+ rescue Errno::ENOENT
234
+ nil
235
+ end
236
+
237
+ private
238
+ def key_path(key)
239
+ File.join(root, spread(hexdigest(key)))
240
+ end
241
+
242
+ def spread(sha, n=2)
243
+ sha = sha.dup
244
+ sha[n,0] = '/'
245
+ sha
246
+ end
247
+
248
+ public
249
+ def self.resolve(uri)
250
+ path = File.expand_path(uri.opaque || uri.path)
251
+ new path
252
+ end
253
+
254
+ end
255
+
256
+ DISK = Disk
257
+ FILE = Disk
258
+
259
+ # Stores request/response pairs in memcached. Keys are not stored
260
+ # directly since memcached has a 250-byte limit on key names. Instead,
261
+ # the SHA1 hexdigest of the key is used.
262
+ class MemCacheBase < MetaStore
263
+ extend Rack::Utils
264
+
265
+ # The MemCache object used to communicated with the memcached
266
+ # daemon.
267
+ attr_reader :cache
268
+
269
+ # Create MemCache store for the given URI. The URI must specify
270
+ # a host and may specify a port, namespace, and options:
271
+ #
272
+ # memcached://example.com:11211/namespace?opt1=val1&opt2=val2
273
+ #
274
+ # Query parameter names and values are documented with the memcached
275
+ # library: http://tinyurl.com/4upqnd
276
+ def self.resolve(uri)
277
+ if uri.respond_to?(:scheme)
278
+ server = "#{uri.host}:#{uri.port || '11211'}"
279
+ options = parse_query(uri.query)
280
+ options.keys.each do |key|
281
+ value =
282
+ case value = options.delete(key)
283
+ when 'true' ; true
284
+ when 'false' ; false
285
+ else value.to_sym
286
+ end
287
+ options[k.to_sym] = value
288
+ end
289
+
290
+ options[:namespace] = uri.path.to_s.sub(/^\//, '')
291
+
292
+ new server, options
293
+ else
294
+ # if the object provided is not a URI, pass it straight through
295
+ # to the underlying implementation.
296
+ new uri
297
+ end
298
+ end
299
+ end
300
+
301
+ class MemCache < MemCacheBase
302
+ def initialize(server="localhost:11211", options={})
303
+ @cache =
304
+ if server.respond_to?(:stats)
305
+ server
306
+ else
307
+ require 'memcache'
308
+ ::MemCache.new(server, options)
309
+ end
310
+ end
311
+
312
+ def read(key)
313
+ key = hexdigest(key)
314
+ cache.get(key) || []
315
+ end
316
+
317
+ def write(key, entries)
318
+ key = hexdigest(key)
319
+ cache.set(key, entries)
320
+ end
321
+
322
+ def purge(key)
323
+ cache.delete(hexdigest(key))
324
+ nil
325
+ end
326
+ end
327
+
328
+ class MemCached < MemCacheBase
329
+ # The Memcached instance used to communicated with the memcached
330
+ # daemon.
331
+ attr_reader :cache
332
+
333
+ def initialize(server="localhost:11211", options={})
334
+ @cache =
335
+ if server.respond_to?(:stats)
336
+ server
337
+ else
338
+ require 'memcached'
339
+ Memcached.new(server, options)
340
+ end
341
+ end
342
+
343
+ def read(key)
344
+ key = hexdigest(key)
345
+ cache.get(key)
346
+ rescue Memcached::NotFound
347
+ []
348
+ end
349
+
350
+ def write(key, entries)
351
+ key = hexdigest(key)
352
+ cache.set(key, entries)
353
+ end
354
+
355
+ def purge(key)
356
+ key = hexdigest(key)
357
+ cache.delete(key)
358
+ nil
359
+ rescue Memcached::NotFound
360
+ nil
361
+ end
362
+ end
363
+
364
+ MEMCACHE =
365
+ if defined?(::Memcached)
366
+ MemCached
367
+ else
368
+ MemCache
369
+ end
370
+ MEMCACHED = MEMCACHE
371
+
372
+ class GAEStore < MetaStore
373
+ attr_reader :cache
374
+
375
+ def initialize(options = {})
376
+ require 'rack/cache/appengine'
377
+ @cache = Rack::Cache::AppEngine::MemCache.new(options)
378
+ end
379
+
380
+ def read(key)
381
+ key = hexdigest(key)
382
+ cache.get(key) || []
383
+ end
384
+
385
+ def write(key, entries)
386
+ key = hexdigest(key)
387
+ cache.put(key, entries)
388
+ end
389
+
390
+ def purge(key)
391
+ key = hexdigest(key)
392
+ cache.delete(key)
393
+ nil
394
+ end
395
+
396
+ def self.resolve(uri)
397
+ self.new(:namespace => uri.host)
398
+ end
399
+
400
+ end
401
+
402
+ GAECACHE = GAEStore
403
+ GAE = GAEStore
404
+
405
+ end
406
+
407
+ end