class Rack::Cache::Context

Implements Rack's middleware interface and provides the context for all cache logic, including the core logic engine.

Attributes

backend[R]

The Rack application object immediately downstream.

trace[R]

Array of trace Symbols

Public Class Methods

new(backend, options={}) { |self| ... } click to toggle source
   # File lib/rack/cache/context.rb
18 def initialize(backend, options={})
19   @backend = backend
20   @trace = []
21   @env = nil
22   @options = options
23 
24   initialize_options options
25   yield self if block_given?
26 
27   @private_header_keys =
28     private_headers.map { |name| "HTTP_#{name.upcase.tr('-', '_')}" }
29 end

Public Instance Methods

call(env) click to toggle source

The Rack call interface. The receiver acts as a prototype and runs each request in a dup object unless the rack.run_once variable is set in the environment.

   # File lib/rack/cache/context.rb
48 def call(env)
49   if env['rack.run_once'] && !env['rack.multithread']
50     call! env
51   else
52     clone.call! env
53   end
54 end
call!(env) click to toggle source

The real Rack call interface. The caching logic is performed within the context of the receiver.

    # File lib/rack/cache/context.rb
 58 def call!(env)
 59   @trace = []
 60   @default_options.each { |k,v| env[k] ||= v }
 61   @env = env
 62   @request = Request.new(@env.dup.freeze)
 63 
 64   response =
 65     if @request.get? || @request.head?
 66       if !@env['HTTP_EXPECT'] && !@env['rack-cache.force-pass']
 67         lookup
 68       else
 69         pass
 70       end
 71     else
 72       if @request.options?
 73         pass
 74       else
 75         invalidate
 76       end
 77     end
 78 
 79   # log trace and set X-Rack-Cache tracing header
 80   trace = @trace.join(', ')
 81   response.headers['X-Rack-Cache'] = trace
 82 
 83   # write log message to rack.errors
 84   if verbose?
 85     message = "cache: [%s %s] %s\n" %
 86       [@request.request_method, @request.fullpath, trace]
 87     log_info(message)
 88   end
 89 
 90   # tidy up response a bit
 91   if (@request.get? || @request.head?) && not_modified?(response)
 92     response.not_modified!
 93   end
 94 
 95   if @request.head?
 96     response.body.close if response.body.respond_to?(:close)
 97     response.body = []
 98   end
 99   response.to_a
100 end
entitystore() click to toggle source

The configured EntityStore instance. Changing the rack-cache.entitystore value effects the result of this method immediately.

   # File lib/rack/cache/context.rb
40 def entitystore
41   uri = options['rack-cache.entitystore']
42   storage.resolve_entitystore_uri(uri, @options)
43 end
metastore() click to toggle source

The configured MetaStore instance. Changing the rack-cache.metastore value effects the result of this method immediately.

   # File lib/rack/cache/context.rb
33 def metastore
34   uri = options['rack-cache.metastore']
35   storage.resolve_metastore_uri(uri, @options)
36 end

Private Instance Methods

convert_head_to_get!() click to toggle source

send no head requests because we want content

    # File lib/rack/cache/context.rb
322 def convert_head_to_get!
323   if @env['REQUEST_METHOD'] == 'HEAD'
324     @env['REQUEST_METHOD'] = 'GET'
325     @env['rack.methodoverride.original_method'] = 'HEAD'
326   end
327 end
fetch() click to toggle source

The cache missed or a reload is required. Forward the request to the backend and determine whether the response should be stored. This allows conditional / validation requests through to the backend but performs no caching of the response when the backend returns a 304.

    # File lib/rack/cache/context.rb
263 def fetch
264   # send no head requests because we want content
265   convert_head_to_get!
266 
267   response = forward
268 
269   # Mark the response as explicitly private if any of the private
270   # request headers are present and the response was not explicitly
271   # declared public.
272   if private_request? && !response.cache_control.public?
273     response.private = true
274   elsif default_ttl > 0 && response.ttl.nil? && !response.cache_control.must_revalidate?
275     # assign a default TTL for the cache entry if none was specified in
276     # the response; the must-revalidate cache control directive disables
277     # default ttl assigment.
278     response.ttl = default_ttl
279   end
280 
281   store(response) if response.cacheable?
282 
283   response
284 end
forward() click to toggle source

Delegate the request to the backend and create the response.

    # File lib/rack/cache/context.rb
140 def forward
141   Response.new(*backend.call(@env))
142 end
fresh_enough?(entry) click to toggle source

Whether the cache entry is “fresh enough” to satisfy the request.

    # File lib/rack/cache/context.rb
129 def fresh_enough?(entry)
130   if entry.fresh?
131     if allow_revalidate? && max_age = @request.cache_control.max_age
132       max_age > 0 && max_age >= entry.age
133     else
134       true
135     end
136   end
137 end
invalidate() click to toggle source

Invalidate POST, PUT, DELETE and all methods not understood by this cache See RFC2616 13.10

    # File lib/rack/cache/context.rb
153 def invalidate
154   metastore.invalidate(@request, entitystore)
155 rescue => e
156   log_error(e)
157   pass
158 else
159   record :invalidate
160   pass
161 end
log(level, message) click to toggle source
    # File lib/rack/cache/context.rb
313 def log(level, message)
314   if @env['rack.logger']
315     @env['rack.logger'].send(level, message)
316   else
317     @env['rack.errors'].write(message)
318   end
319 end
log_error(exception) click to toggle source
    # File lib/rack/cache/context.rb
304 def log_error(exception)
305   message = "cache error: #{exception.message}\n#{exception.backtrace.join("\n")}\n"
306   log(:error, message)
307 end
log_info(message) click to toggle source
    # File lib/rack/cache/context.rb
309 def log_info(message)
310   log(:info, message)
311 end
lookup() click to toggle source

Try to serve the response from cache. When a matching cache entry is found and is fresh, use it as the response without forwarding any request to the backend. When a matching cache entry is found but is stale, attempt to validate the entry with the backend using conditional GET. If validation raises an exception and fault tolerant caching is enabled, serve the stale cache entry. When no matching cache entry is found, trigger miss processing.

    # File lib/rack/cache/context.rb
170 def lookup
171   if @request.no_cache? && allow_reload?
172     record :reload
173     fetch
174   else
175     begin
176       entry = metastore.lookup(@request, entitystore)
177     rescue => e
178       log_error(e)
179       return pass
180     end
181     if entry
182       if fresh_enough?(entry)
183         record :fresh
184         entry.headers['Age'] = entry.age.to_s
185         entry
186       else
187         record :stale
188         if fault_tolerant?
189           validate_with_stale_cache_failover(entry)
190         else
191           validate(entry)
192         end
193       end
194     else
195       record :miss
196       fetch
197     end
198   end
199 end
not_modified?(response) click to toggle source

Determine if the response validators (ETag, Last-Modified) matches a conditional value specified in request.

    # File lib/rack/cache/context.rb
118 def not_modified?(response)
119   last_modified = @request.env['HTTP_IF_MODIFIED_SINCE']
120   if etags = @request.env['HTTP_IF_NONE_MATCH']
121     etags = etags.split(/\s*,\s*/)
122     (etags.include?(response.etag) || etags.include?('*')) && (!last_modified || response.last_modified == last_modified)
123   elsif last_modified
124     response.last_modified == last_modified
125   end
126 end
pass() click to toggle source

The request is sent to the backend, and the backend's response is sent to the client, but is not entered into the cache.

    # File lib/rack/cache/context.rb
146 def pass
147   record :pass
148   forward
149 end
private_request?() click to toggle source

Does the request include authorization or other sensitive information that should cause the response to be considered private by default? Private responses are not stored in the cache.

    # File lib/rack/cache/context.rb
112 def private_request?
113   @private_header_keys.any? { |key| @env.key?(key) }
114 end
record(event) click to toggle source

Record that an event took place.

    # File lib/rack/cache/context.rb
105 def record(event)
106   @trace << event
107 end
store(response) click to toggle source

Write the response to the cache.

    # File lib/rack/cache/context.rb
287 def store(response)
288   strip_ignore_headers(response)
289   metastore.store(@request, response, entitystore)
290   response.headers['Age'] = response.age.to_s
291 rescue => e
292   log_error(e)
293   nil
294 else
295   record :store
296 end
strip_ignore_headers(response) click to toggle source

Remove all ignored response headers before writing to the cache.

    # File lib/rack/cache/context.rb
299 def strip_ignore_headers(response)
300   stripped_values = ignore_headers.map { |name| response.headers.delete(name) }
301   record :ignore if stripped_values.any?
302 end
validate(entry) click to toggle source

Validate that the cache entry is fresh. The original request is used as a template for a conditional GET request with the backend.

    # File lib/rack/cache/context.rb
214 def validate(entry)
215   # send no head requests because we want content
216   convert_head_to_get!
217 
218   # add our cached last-modified validator to the environment
219   @env['HTTP_IF_MODIFIED_SINCE'] = entry.last_modified
220 
221   # Add our cached etag validator to the environment.
222   # We keep the etags from the client to handle the case when the client
223   # has a different private valid entry which is not cached here.
224   cached_etags = entry.etag.to_s.split(/\s*,\s*/)
225   request_etags = @request.env['HTTP_IF_NONE_MATCH'].to_s.split(/\s*,\s*/)
226   etags = (cached_etags + request_etags).uniq
227   @env['HTTP_IF_NONE_MATCH'] = etags.empty? ? nil : etags.join(', ')
228 
229   response = forward
230 
231   if response.status == 304
232     record :valid
233 
234     # Check if the response validated which is not cached here
235     etag = response.headers['ETag']
236     return response if etag && request_etags.include?(etag) && !cached_etags.include?(etag)
237 
238     entry = entry.dup
239     entry.headers.delete('Date')
240     %w[Date Expires Cache-Control ETag Last-Modified].each do |name|
241       next unless value = response.headers[name]
242       entry.headers[name] = value
243     end
244 
245     # even though it's empty, be sure to close the response body from upstream
246     # because middleware use close to signal end of response
247     response.body.close if response.body.respond_to?(:close)
248 
249     response = entry
250   else
251     record :invalid
252   end
253 
254   store(response) if response.cacheable?
255 
256   response
257 end
validate_with_stale_cache_failover(entry) click to toggle source

Returns stale cache on exception.

    # File lib/rack/cache/context.rb
202 def validate_with_stale_cache_failover(entry)
203   validate(entry)
204 rescue => e
205   record :connnection_failed
206   age = entry.age.to_s
207   entry.headers['Age'] = age
208   record "Fail-over to stale cache data with age #{age} due to #{e.class.name}: #{e}"
209   entry
210 end