josh-rack-cache 0.5.1
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/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,253 @@
|
|
1
|
+
require 'rack/cache/options'
|
2
|
+
require 'rack/cache/request'
|
3
|
+
require 'rack/cache/response'
|
4
|
+
require 'rack/cache/storage'
|
5
|
+
|
6
|
+
module Rack::Cache
|
7
|
+
# Implements Rack's middleware interface and provides the context for all
|
8
|
+
# cache logic, including the core logic engine.
|
9
|
+
class Context
|
10
|
+
include Rack::Cache::Options
|
11
|
+
|
12
|
+
# Array of trace Symbols
|
13
|
+
attr_reader :trace
|
14
|
+
|
15
|
+
# The Rack application object immediately downstream.
|
16
|
+
attr_reader :backend
|
17
|
+
|
18
|
+
def initialize(backend, options={})
|
19
|
+
@backend = backend
|
20
|
+
@trace = []
|
21
|
+
|
22
|
+
initialize_options options
|
23
|
+
yield self if block_given?
|
24
|
+
|
25
|
+
@private_header_keys =
|
26
|
+
private_headers.map { |name| "HTTP_#{name.upcase.tr('-', '_')}" }
|
27
|
+
end
|
28
|
+
|
29
|
+
# The configured MetaStore instance. Changing the rack-cache.metastore
|
30
|
+
# value effects the result of this method immediately.
|
31
|
+
def metastore
|
32
|
+
uri = options['rack-cache.metastore']
|
33
|
+
storage.resolve_metastore_uri(uri)
|
34
|
+
end
|
35
|
+
|
36
|
+
# The configured EntityStore instance. Changing the rack-cache.entitystore
|
37
|
+
# value effects the result of this method immediately.
|
38
|
+
def entitystore
|
39
|
+
uri = options['rack-cache.entitystore']
|
40
|
+
storage.resolve_entitystore_uri(uri)
|
41
|
+
end
|
42
|
+
|
43
|
+
# The Rack call interface. The receiver acts as a prototype and runs
|
44
|
+
# each request in a dup object unless the +rack.run_once+ variable is
|
45
|
+
# set in the environment.
|
46
|
+
def call(env)
|
47
|
+
if env['rack.run_once']
|
48
|
+
call! env
|
49
|
+
else
|
50
|
+
clone.call! env
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# The real Rack call interface. The caching logic is performed within
|
55
|
+
# the context of the receiver.
|
56
|
+
def call!(env)
|
57
|
+
@trace = []
|
58
|
+
@env = @default_options.merge(env)
|
59
|
+
@request = Request.new(@env.dup.freeze)
|
60
|
+
|
61
|
+
response =
|
62
|
+
if @request.get? || @request.head?
|
63
|
+
if !@env['HTTP_EXPECT']
|
64
|
+
lookup
|
65
|
+
else
|
66
|
+
pass
|
67
|
+
end
|
68
|
+
else
|
69
|
+
invalidate
|
70
|
+
end
|
71
|
+
|
72
|
+
# log trace and set X-Rack-Cache tracing header
|
73
|
+
trace = @trace.join(', ')
|
74
|
+
response.headers['X-Rack-Cache'] = trace
|
75
|
+
|
76
|
+
# write log message to rack.errors
|
77
|
+
if verbose?
|
78
|
+
message = "cache: [%s %s] %s\n" %
|
79
|
+
[@request.request_method, @request.fullpath, trace]
|
80
|
+
@env['rack.errors'].write(message)
|
81
|
+
end
|
82
|
+
|
83
|
+
# tidy up response a bit
|
84
|
+
response.not_modified! if not_modified?(response)
|
85
|
+
response.body = [] if @request.head?
|
86
|
+
response.to_a
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
# Record that an event took place.
|
92
|
+
def record(event)
|
93
|
+
@trace << event
|
94
|
+
end
|
95
|
+
|
96
|
+
# Does the request include authorization or other sensitive information
|
97
|
+
# that should cause the response to be considered private by default?
|
98
|
+
# Private responses are not stored in the cache.
|
99
|
+
def private_request?
|
100
|
+
@private_header_keys.any? { |key| @env.key?(key) }
|
101
|
+
end
|
102
|
+
|
103
|
+
# Determine if the #response validators (ETag, Last-Modified) matches
|
104
|
+
# a conditional value specified in #request.
|
105
|
+
def not_modified?(response)
|
106
|
+
response.etag_matches?(@request.env['HTTP_IF_NONE_MATCH']) ||
|
107
|
+
response.last_modified_at?(@request.env['HTTP_IF_MODIFIED_SINCE'])
|
108
|
+
end
|
109
|
+
|
110
|
+
# Whether the cache entry is "fresh enough" to satisfy the request.
|
111
|
+
def fresh_enough?(entry)
|
112
|
+
if entry.fresh?
|
113
|
+
if allow_revalidate? && max_age = @request.cache_control.max_age
|
114
|
+
max_age > 0 && max_age >= entry.age
|
115
|
+
else
|
116
|
+
true
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Delegate the request to the backend and create the response.
|
122
|
+
def forward
|
123
|
+
Response.new(*backend.call(@env))
|
124
|
+
end
|
125
|
+
|
126
|
+
# The request is sent to the backend, and the backend's response is sent
|
127
|
+
# to the client, but is not entered into the cache.
|
128
|
+
def pass
|
129
|
+
record :pass
|
130
|
+
forward
|
131
|
+
end
|
132
|
+
|
133
|
+
# Invalidate POST, PUT, DELETE and all methods not understood by this cache
|
134
|
+
# See RFC2616 13.10
|
135
|
+
def invalidate
|
136
|
+
metastore.invalidate(@request, entitystore)
|
137
|
+
rescue Exception => e
|
138
|
+
log_error(e)
|
139
|
+
pass
|
140
|
+
else
|
141
|
+
record :invalidate
|
142
|
+
pass
|
143
|
+
end
|
144
|
+
|
145
|
+
# Try to serve the response from cache. When a matching cache entry is
|
146
|
+
# found and is fresh, use it as the response without forwarding any
|
147
|
+
# request to the backend. When a matching cache entry is found but is
|
148
|
+
# stale, attempt to #validate the entry with the backend using conditional
|
149
|
+
# GET. When no matching cache entry is found, trigger #miss processing.
|
150
|
+
def lookup
|
151
|
+
if @request.no_cache? && allow_reload?
|
152
|
+
record :reload
|
153
|
+
fetch
|
154
|
+
else
|
155
|
+
begin
|
156
|
+
entry = metastore.lookup(@request, entitystore)
|
157
|
+
rescue Exception => e
|
158
|
+
log_error(e)
|
159
|
+
return pass
|
160
|
+
end
|
161
|
+
if entry
|
162
|
+
if fresh_enough?(entry)
|
163
|
+
record :fresh
|
164
|
+
entry.headers['Age'] = entry.age.to_s
|
165
|
+
entry
|
166
|
+
else
|
167
|
+
record :stale
|
168
|
+
validate(entry)
|
169
|
+
end
|
170
|
+
else
|
171
|
+
record :miss
|
172
|
+
fetch
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# Validate that the cache entry is fresh. The original request is used
|
178
|
+
# as a template for a conditional GET request with the backend.
|
179
|
+
def validate(entry)
|
180
|
+
# send no head requests because we want content
|
181
|
+
@env['REQUEST_METHOD'] = 'GET'
|
182
|
+
|
183
|
+
# add our cached validators to the environment
|
184
|
+
@env['HTTP_IF_MODIFIED_SINCE'] = entry.last_modified
|
185
|
+
@env['HTTP_IF_NONE_MATCH'] = entry.etag
|
186
|
+
|
187
|
+
backend_response = forward
|
188
|
+
|
189
|
+
response =
|
190
|
+
if backend_response.status == 304
|
191
|
+
record :valid
|
192
|
+
entry = entry.dup
|
193
|
+
entry.headers.delete('Date')
|
194
|
+
%w[Date Expires Cache-Control ETag Last-Modified].each do |name|
|
195
|
+
next unless value = backend_response.headers[name]
|
196
|
+
entry.headers[name] = value
|
197
|
+
end
|
198
|
+
entry
|
199
|
+
else
|
200
|
+
record :invalid
|
201
|
+
backend_response
|
202
|
+
end
|
203
|
+
|
204
|
+
store(response) if response.cacheable?
|
205
|
+
|
206
|
+
response
|
207
|
+
end
|
208
|
+
|
209
|
+
# The cache missed or a reload is required. Forward the request to the
|
210
|
+
# backend and determine whether the response should be stored.
|
211
|
+
def fetch
|
212
|
+
# send no head requests because we want content
|
213
|
+
@env['REQUEST_METHOD'] = 'GET'
|
214
|
+
|
215
|
+
# avoid that the backend sends no content
|
216
|
+
@env.delete('HTTP_IF_MODIFIED_SINCE')
|
217
|
+
@env.delete('HTTP_IF_NONE_MATCH')
|
218
|
+
|
219
|
+
response = forward
|
220
|
+
|
221
|
+
# Mark the response as explicitly private if any of the private
|
222
|
+
# request headers are present and the response was not explicitly
|
223
|
+
# declared public.
|
224
|
+
if private_request? && !response.cache_control.public?
|
225
|
+
response.private = true
|
226
|
+
elsif default_ttl > 0 && response.ttl.nil? && !response.cache_control.must_revalidate?
|
227
|
+
# assign a default TTL for the cache entry if none was specified in
|
228
|
+
# the response; the must-revalidate cache control directive disables
|
229
|
+
# default ttl assigment.
|
230
|
+
response.ttl = default_ttl
|
231
|
+
end
|
232
|
+
|
233
|
+
store(response) if response.cacheable?
|
234
|
+
|
235
|
+
response
|
236
|
+
end
|
237
|
+
|
238
|
+
# Write the response to the cache.
|
239
|
+
def store(response)
|
240
|
+
metastore.store(@request, response, entitystore)
|
241
|
+
response.headers['Age'] = response.age.to_s
|
242
|
+
rescue Exception => e
|
243
|
+
log_error(e)
|
244
|
+
nil
|
245
|
+
else
|
246
|
+
record :store
|
247
|
+
end
|
248
|
+
|
249
|
+
def log_error(exception)
|
250
|
+
@env['rack.errors'].write("cache error: #{exception.message}\n#{exception.backtrace.join("\n")}\n")
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
@@ -0,0 +1,339 @@
|
|
1
|
+
require 'digest/sha1'
|
2
|
+
|
3
|
+
module Rack::Cache
|
4
|
+
|
5
|
+
# Entity stores are used to cache response bodies across requests. All
|
6
|
+
# Implementations are required to calculate a SHA checksum of the data written
|
7
|
+
# which becomes the response body's key.
|
8
|
+
class EntityStore
|
9
|
+
|
10
|
+
# Read body calculating the SHA1 checksum and size while
|
11
|
+
# yielding each chunk to the block. If the body responds to close,
|
12
|
+
# call it after iteration is complete. Return a two-tuple of the form:
|
13
|
+
# [ hexdigest, size ].
|
14
|
+
def slurp(body)
|
15
|
+
digest, size = Digest::SHA1.new, 0
|
16
|
+
body.each do |part|
|
17
|
+
size += bytesize(part)
|
18
|
+
digest << part
|
19
|
+
yield part
|
20
|
+
end
|
21
|
+
body.close if body.respond_to? :close
|
22
|
+
[digest.hexdigest, size]
|
23
|
+
end
|
24
|
+
|
25
|
+
if ''.respond_to?(:bytesize)
|
26
|
+
def bytesize(string); string.bytesize; end
|
27
|
+
else
|
28
|
+
def bytesize(string); string.size; end
|
29
|
+
end
|
30
|
+
|
31
|
+
private :slurp, :bytesize
|
32
|
+
|
33
|
+
|
34
|
+
# Stores entity bodies on the heap using a Hash object.
|
35
|
+
class Heap < EntityStore
|
36
|
+
|
37
|
+
# Create the store with the specified backing Hash.
|
38
|
+
def initialize(hash={})
|
39
|
+
@hash = hash
|
40
|
+
end
|
41
|
+
|
42
|
+
# Determine whether the response body with the specified key (SHA1)
|
43
|
+
# exists in the store.
|
44
|
+
def exist?(key)
|
45
|
+
@hash.include?(key)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Return an object suitable for use as a Rack response body for the
|
49
|
+
# specified key.
|
50
|
+
def open(key)
|
51
|
+
(body = @hash[key]) && body.dup
|
52
|
+
end
|
53
|
+
|
54
|
+
# Read all data associated with the given key and return as a single
|
55
|
+
# String.
|
56
|
+
def read(key)
|
57
|
+
(body = @hash[key]) && body.join
|
58
|
+
end
|
59
|
+
|
60
|
+
# Write the Rack response body immediately and return the SHA1 key.
|
61
|
+
def write(body)
|
62
|
+
buf = []
|
63
|
+
key, size = slurp(body) { |part| buf << part }
|
64
|
+
@hash[key] = buf
|
65
|
+
[key, size]
|
66
|
+
end
|
67
|
+
|
68
|
+
# Remove the body corresponding to key; return nil.
|
69
|
+
def purge(key)
|
70
|
+
@hash.delete(key)
|
71
|
+
nil
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.resolve(uri)
|
75
|
+
new
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
HEAP = Heap
|
80
|
+
MEM = Heap
|
81
|
+
|
82
|
+
# Stores entity bodies on disk at the specified path.
|
83
|
+
class Disk < EntityStore
|
84
|
+
|
85
|
+
# Path where entities should be stored. This directory is
|
86
|
+
# created the first time the store is instansiated if it does not
|
87
|
+
# already exist.
|
88
|
+
attr_reader :root
|
89
|
+
|
90
|
+
def initialize(root)
|
91
|
+
@root = root
|
92
|
+
FileUtils.mkdir_p root, :mode => 0755
|
93
|
+
end
|
94
|
+
|
95
|
+
def exist?(key)
|
96
|
+
File.exist?(body_path(key))
|
97
|
+
end
|
98
|
+
|
99
|
+
def read(key)
|
100
|
+
File.open(body_path(key), 'rb') { |f| f.read }
|
101
|
+
rescue Errno::ENOENT
|
102
|
+
nil
|
103
|
+
end
|
104
|
+
|
105
|
+
class Body < ::File #:nodoc:
|
106
|
+
def each
|
107
|
+
while part = read(8192)
|
108
|
+
yield part
|
109
|
+
end
|
110
|
+
end
|
111
|
+
alias_method :to_path, :path
|
112
|
+
end
|
113
|
+
|
114
|
+
# Open the entity body and return an IO object. The IO object's
|
115
|
+
# each method is overridden to read 8K chunks instead of lines.
|
116
|
+
def open(key)
|
117
|
+
Body.open(body_path(key), 'rb')
|
118
|
+
rescue Errno::ENOENT
|
119
|
+
nil
|
120
|
+
end
|
121
|
+
|
122
|
+
def write(body)
|
123
|
+
filename = ['buf', $$, Thread.current.object_id].join('-')
|
124
|
+
temp_file = storage_path(filename)
|
125
|
+
key, size =
|
126
|
+
File.open(temp_file, 'wb') { |dest|
|
127
|
+
slurp(body) { |part| dest.write(part) }
|
128
|
+
}
|
129
|
+
|
130
|
+
path = body_path(key)
|
131
|
+
if File.exist?(path)
|
132
|
+
File.unlink temp_file
|
133
|
+
else
|
134
|
+
FileUtils.mkdir_p File.dirname(path), :mode => 0755
|
135
|
+
FileUtils.mv temp_file, path
|
136
|
+
end
|
137
|
+
[key, size]
|
138
|
+
end
|
139
|
+
|
140
|
+
def purge(key)
|
141
|
+
File.unlink body_path(key)
|
142
|
+
nil
|
143
|
+
rescue Errno::ENOENT
|
144
|
+
nil
|
145
|
+
end
|
146
|
+
|
147
|
+
protected
|
148
|
+
def storage_path(stem)
|
149
|
+
File.join root, stem
|
150
|
+
end
|
151
|
+
|
152
|
+
def spread(key)
|
153
|
+
key = key.dup
|
154
|
+
key[2,0] = '/'
|
155
|
+
key
|
156
|
+
end
|
157
|
+
|
158
|
+
def body_path(key)
|
159
|
+
storage_path spread(key)
|
160
|
+
end
|
161
|
+
|
162
|
+
def self.resolve(uri)
|
163
|
+
path = File.expand_path(uri.opaque || uri.path)
|
164
|
+
new path
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
DISK = Disk
|
169
|
+
FILE = Disk
|
170
|
+
|
171
|
+
# Base class for memcached entity stores.
|
172
|
+
class MemCacheBase < EntityStore
|
173
|
+
# The underlying Memcached instance used to communicate with the
|
174
|
+
# memcached daemon.
|
175
|
+
attr_reader :cache
|
176
|
+
|
177
|
+
extend Rack::Utils
|
178
|
+
|
179
|
+
def open(key)
|
180
|
+
data = read(key)
|
181
|
+
data && [data]
|
182
|
+
end
|
183
|
+
|
184
|
+
def self.resolve(uri)
|
185
|
+
if uri.respond_to?(:scheme)
|
186
|
+
server = "#{uri.host}:#{uri.port || '11211'}"
|
187
|
+
options = parse_query(uri.query)
|
188
|
+
options.keys.each do |key|
|
189
|
+
value =
|
190
|
+
case value = options.delete(key)
|
191
|
+
when 'true' ; true
|
192
|
+
when 'false' ; false
|
193
|
+
else value.to_sym
|
194
|
+
end
|
195
|
+
options[k.to_sym] = value
|
196
|
+
end
|
197
|
+
options[:namespace] = uri.path.sub(/^\//, '')
|
198
|
+
new server, options
|
199
|
+
else
|
200
|
+
# if the object provided is not a URI, pass it straight through
|
201
|
+
# to the underlying implementation.
|
202
|
+
new uri
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
# Uses the memcache-client ruby library. This is the default unless
|
208
|
+
# the memcached library has already been required.
|
209
|
+
class MemCache < MemCacheBase
|
210
|
+
def initialize(server="localhost:11211", options={})
|
211
|
+
@cache =
|
212
|
+
if server.respond_to?(:stats)
|
213
|
+
server
|
214
|
+
else
|
215
|
+
require 'memcache'
|
216
|
+
::MemCache.new(server, options)
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def exist?(key)
|
221
|
+
!cache.get(key).nil?
|
222
|
+
end
|
223
|
+
|
224
|
+
def read(key)
|
225
|
+
cache.get(key)
|
226
|
+
end
|
227
|
+
|
228
|
+
def write(body)
|
229
|
+
buf = StringIO.new
|
230
|
+
key, size = slurp(body){|part| buf.write(part) }
|
231
|
+
[key, size] if cache.set(key, buf.string)
|
232
|
+
end
|
233
|
+
|
234
|
+
def purge(key)
|
235
|
+
cache.delete(key)
|
236
|
+
nil
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
# Uses the memcached client library. The ruby based memcache-client is used
|
241
|
+
# in preference to this store unless the memcached library has already been
|
242
|
+
# required.
|
243
|
+
class MemCached < MemCacheBase
|
244
|
+
def initialize(server="localhost:11211", options={})
|
245
|
+
options[:prefix_key] ||= options.delete(:namespace) if options.key?(:namespace)
|
246
|
+
@cache =
|
247
|
+
if server.respond_to?(:stats)
|
248
|
+
server
|
249
|
+
else
|
250
|
+
require 'memcached'
|
251
|
+
::Memcached.new(server, options)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
def exist?(key)
|
256
|
+
cache.append(key, '')
|
257
|
+
true
|
258
|
+
rescue ::Memcached::NotStored
|
259
|
+
false
|
260
|
+
end
|
261
|
+
|
262
|
+
def read(key)
|
263
|
+
cache.get(key, false)
|
264
|
+
rescue ::Memcached::NotFound
|
265
|
+
nil
|
266
|
+
end
|
267
|
+
|
268
|
+
def write(body)
|
269
|
+
buf = StringIO.new
|
270
|
+
key, size = slurp(body){|part| buf.write(part) }
|
271
|
+
cache.set(key, buf.string, 0, false)
|
272
|
+
[key, size]
|
273
|
+
end
|
274
|
+
|
275
|
+
def purge(key)
|
276
|
+
cache.delete(key)
|
277
|
+
nil
|
278
|
+
rescue ::Memcached::NotFound
|
279
|
+
nil
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
MEMCACHE =
|
284
|
+
if defined?(::Memcached)
|
285
|
+
MemCached
|
286
|
+
else
|
287
|
+
MemCache
|
288
|
+
end
|
289
|
+
|
290
|
+
MEMCACHED = MEMCACHE
|
291
|
+
|
292
|
+
class GAEStore < EntityStore
|
293
|
+
attr_reader :cache
|
294
|
+
|
295
|
+
def initialize(options = {})
|
296
|
+
require 'rack/cache/appengine'
|
297
|
+
@cache = Rack::Cache::AppEngine::MemCache.new(options)
|
298
|
+
end
|
299
|
+
|
300
|
+
def exist?(key)
|
301
|
+
cache.contains?(key)
|
302
|
+
end
|
303
|
+
|
304
|
+
def read(key)
|
305
|
+
cache.get(key)
|
306
|
+
end
|
307
|
+
|
308
|
+
def open(key)
|
309
|
+
if data = read(key)
|
310
|
+
[data]
|
311
|
+
else
|
312
|
+
nil
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
def write(body)
|
317
|
+
buf = StringIO.new
|
318
|
+
key, size = slurp(body){|part| buf.write(part) }
|
319
|
+
cache.put(key, buf.string)
|
320
|
+
[key, size]
|
321
|
+
end
|
322
|
+
|
323
|
+
def purge(key)
|
324
|
+
cache.delete(key)
|
325
|
+
nil
|
326
|
+
end
|
327
|
+
|
328
|
+
def self.resolve(uri)
|
329
|
+
self.new(:namespace => uri.host)
|
330
|
+
end
|
331
|
+
|
332
|
+
end
|
333
|
+
|
334
|
+
GAECACHE = GAEStore
|
335
|
+
GAE = GAEStore
|
336
|
+
|
337
|
+
end
|
338
|
+
|
339
|
+
end
|