josh-rack-cache 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES +167 -0
- data/COPYING +18 -0
- data/README +110 -0
- data/Rakefile +137 -0
- data/TODO +27 -0
- data/doc/configuration.markdown +112 -0
- data/doc/faq.markdown +141 -0
- data/doc/index.markdown +121 -0
- data/doc/layout.html.erb +34 -0
- data/doc/license.markdown +24 -0
- data/doc/rack-cache.css +362 -0
- data/doc/server.ru +34 -0
- data/doc/storage.markdown +164 -0
- data/example/sinatra/app.rb +25 -0
- data/example/sinatra/views/index.erb +44 -0
- data/lib/rack/cache.rb +45 -0
- data/lib/rack/cache/appengine.rb +52 -0
- data/lib/rack/cache/cachecontrol.rb +193 -0
- data/lib/rack/cache/context.rb +253 -0
- data/lib/rack/cache/entitystore.rb +339 -0
- data/lib/rack/cache/key.rb +52 -0
- data/lib/rack/cache/metastore.rb +407 -0
- data/lib/rack/cache/options.rb +150 -0
- data/lib/rack/cache/request.rb +33 -0
- data/lib/rack/cache/response.rb +267 -0
- data/lib/rack/cache/storage.rb +62 -0
- data/rack-cache.gemspec +70 -0
- data/test/cache_test.rb +38 -0
- data/test/cachecontrol_test.rb +139 -0
- data/test/context_test.rb +774 -0
- data/test/entitystore_test.rb +230 -0
- data/test/key_test.rb +50 -0
- data/test/metastore_test.rb +302 -0
- data/test/options_test.rb +77 -0
- data/test/pony.jpg +0 -0
- data/test/request_test.rb +19 -0
- data/test/response_test.rb +178 -0
- data/test/spec_setup.rb +237 -0
- data/test/storage_test.rb +94 -0
- metadata +118 -0
@@ -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
|