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, line 18
def initialize(backend, options={})
  @backend = backend
  @trace = []
  @env = nil
  @options = options

  initialize_options options
  yield self if block_given?

  @private_header_keys =
    private_headers.map { |name| "HTTP_#{name.upcase.tr('-', '_')}" }
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, line 48
def call(env)
  if env['rack.run_once'] && !env['rack.multithread']
    call! env
  else
    clone.call! env
  end
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, line 58
def call!(env)
  @trace = []
  @default_options.each { |k,v| env[k] ||= v }
  @env = env
  @request = Request.new(@env.dup.freeze)

  response =
    if @request.get? || @request.head?
      if !@env['HTTP_EXPECT'] && !@env['rack-cache.force-pass']
        lookup
      else
        pass
      end
    else
      if @request.options?
        pass
      else
        invalidate
      end
    end

  # log trace and set x-rack-cache tracing header
  trace = @trace.join(', ')
  response.headers['x-rack-cache'] = trace

  # write log message to rack.errors
  if verbose?
    message = "cache: [%s %s] %s\n" %
      [@request.request_method, @request.fullpath, trace]
    log_info(message)
  end

  # tidy up response a bit
  if (@request.get? || @request.head?) && not_modified?(response)
    response.not_modified!
  end

  if @request.head?
    response.body.close if response.body.respond_to?(:close)
    response.body = []
  end
  response.to_a
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, line 40
def entitystore
  uri = options['rack-cache.entitystore']
  storage.resolve_entitystore_uri(uri, @options)
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, line 33
def metastore
  uri = options['rack-cache.metastore']
  storage.resolve_metastore_uri(uri, @options)
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, line 322
def convert_head_to_get!
  if @env['REQUEST_METHOD'] == 'HEAD'
    @env['REQUEST_METHOD'] = 'GET'
    @env['rack.methodoverride.original_method'] = 'HEAD'
  end
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, line 263
def fetch
  # send no head requests because we want content
  convert_head_to_get!

  response = forward

  # Mark the response as explicitly private if any of the private
  # request headers are present and the response was not explicitly
  # declared public.
  if private_request? && !response.cache_control.public?
    response.private = true
  elsif default_ttl > 0 && response.ttl.nil? && !response.cache_control.must_revalidate?
    # assign a default TTL for the cache entry if none was specified in
    # the response; the must-revalidate cache control directive disables
    # default ttl assigment.
    response.ttl = default_ttl
  end

  store(response) if response.cacheable?

  response
end
forward() click to toggle source

Delegate the request to the backend and create the response.

# File lib/rack/cache/context.rb, line 140
def forward
  Response.new(*backend.call(@env))
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, line 129
def fresh_enough?(entry)
  if entry.fresh?
    if allow_revalidate? && max_age = @request.cache_control.max_age
      max_age > 0 && max_age >= entry.age
    else
      true
    end
  end
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, line 153
def invalidate
  metastore.invalidate(@request, entitystore)
rescue => e
  log_error(e)
  pass
else
  record :invalidate
  pass
end
log(level, message) click to toggle source
# File lib/rack/cache/context.rb, line 313
def log(level, message)
  if @env['rack.logger']
    @env['rack.logger'].send(level, message)
  else
    @env['rack.errors'].write(message)
  end
end
log_error(exception) click to toggle source
# File lib/rack/cache/context.rb, line 304
def log_error(exception)
  message = "cache error: #{exception.message}\n#{exception.backtrace.join("\n")}\n"
  log(:error, message)
end
log_info(message) click to toggle source
# File lib/rack/cache/context.rb, line 309
def log_info(message)
  log(:info, message)
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, line 170
def lookup
  if @request.no_cache? && allow_reload?
    record :reload
    fetch
  else
    begin
      entry = metastore.lookup(@request, entitystore)
    rescue => e
      log_error(e)
      return pass
    end
    if entry
      if fresh_enough?(entry)
        record :fresh
        entry.headers['age'] = entry.age.to_s
        entry
      else
        record :stale
        if fault_tolerant?
          validate_with_stale_cache_failover(entry)
        else
          validate(entry)
        end
      end
    else
      record :miss
      fetch
    end
  end
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, line 118
def not_modified?(response)
  last_modified = @request.env['HTTP_IF_MODIFIED_SINCE']
  if etags = @request.env['HTTP_IF_NONE_MATCH']
    etags = etags.split(/\s*,\s*/)
    (etags.include?(response.etag) || etags.include?('*')) && (!last_modified || response.last_modified == last_modified)
  elsif last_modified
    response.last_modified == last_modified
  end
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, line 146
def pass
  record :pass
  forward
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, line 112
def private_request?
  @private_header_keys.any? { |key| @env.key?(key) }
end
record(event) click to toggle source

Record that an event took place.

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

Write the response to the cache.

# File lib/rack/cache/context.rb, line 287
def store(response)
  strip_ignore_headers(response)
  metastore.store(@request, response, entitystore)
  response.headers['age'] = response.age.to_s
rescue => e
  log_error(e)
  nil
else
  record :store
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, line 299
def strip_ignore_headers(response)
  stripped_values = ignore_headers.map { |name| response.headers.delete(name) }
  record :ignore if stripped_values.any?
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, line 214
def validate(entry)
  # send no head requests because we want content
  convert_head_to_get!

  # add our cached last-modified validator to the environment
  @env['HTTP_IF_MODIFIED_SINCE'] = entry.last_modified

  # Add our cached etag validator to the environment.
  # We keep the etags from the client to handle the case when the client
  # has a different private valid entry which is not cached here.
  cached_etags = entry.etag.to_s.split(/\s*,\s*/)
  request_etags = @request.env['HTTP_IF_NONE_MATCH'].to_s.split(/\s*,\s*/)
  etags = (cached_etags + request_etags).uniq
  @env['HTTP_IF_NONE_MATCH'] = etags.empty? ? nil : etags.join(', ')

  response = forward

  if response.status == 304
    record :valid

    # Check if the response validated which is not cached here
    etag = response.headers['etag']
    return response if etag && request_etags.include?(etag) && !cached_etags.include?(etag)

    entry = entry.dup
    entry.headers.delete('date')
    %w[Date expires cache-control etag last-modified].each do |name|
      next unless value = response.headers[name]
      entry.headers[name] = value
    end

    # even though it's empty, be sure to close the response body from upstream
    # because middleware use close to signal end of response
    response.body.close if response.body.respond_to?(:close)

    response = entry
  else
    record :invalid
  end

  store(response) if response.cacheable?

  response
end
validate_with_stale_cache_failover(entry) click to toggle source

Returns stale cache on exception.

# File lib/rack/cache/context.rb, line 202
def validate_with_stale_cache_failover(entry)
  validate(entry)
rescue => e
  record :connnection_failed
  age = entry.age.to_s
  entry.headers['age'] = age
  record "Fail-over to stale cache data with age #{age} due to #{e.class.name}: #{e}"
  entry
end