Object
Implements Rack’s middleware interface and provides the context for all cache logic, including the core logic engine.
# File lib/rack/cache/context.rb, line 18 18: def initialize(backend, options={}) 19: @backend = backend 20: @trace = [] 21: @env = nil 22: 23: initialize_options options 24: yield self if block_given? 25: 26: @private_header_keys = 27: private_headers.map { |name| "HTTP_#{name.upcase.tr('-', '_')}" } 28: end
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 47 47: def call(env) 48: if env['rack.run_once'] 49: call! env 50: else 51: clone.call! env 52: end 53: end
The real Rack call interface. The caching logic is performed within the context of the receiver.
# File lib/rack/cache/context.rb, line 57 57: def call!(env) 58: @trace = [] 59: @default_options.each { |k,v| env[k] ||= v } 60: @env = env 61: @request = Request.new(@env.dup.freeze) 62: 63: response = 64: if @request.get? || @request.head? 65: if !@env['HTTP_EXPECT'] && !@env['rack-cache.force-pass'] 66: lookup 67: else 68: pass 69: end 70: else 71: invalidate 72: end 73: 74: # log trace and set X-Rack-Cache tracing header 75: trace = @trace.join(', ') 76: response.headers['X-Rack-Cache'] = trace 77: 78: # write log message to rack.errors 79: if verbose? 80: message = "cache: [%s %s] %s\n" % 81: [@request.request_method, @request.fullpath, trace] 82: @env['rack.errors'].write(message) 83: end 84: 85: # tidy up response a bit 86: if (@request.get? || @request.head?) && not_modified?(response) 87: response.not_modified! 88: end 89: 90: if @request.head? 91: response.body.close if response.body.respond_to?(:close) 92: response.body = [] 93: end 94: response.to_a 95: end
The configured EntityStore instance. Changing the rack-cache.entitystore value effects the result of this method immediately.
# File lib/rack/cache/context.rb, line 39 39: def entitystore 40: uri = options['rack-cache.entitystore'] 41: storage.resolve_entitystore_uri(uri) 42: end
The configured MetaStore instance. Changing the rack-cache.metastore value effects the result of this method immediately.
# File lib/rack/cache/context.rb, line 32 32: def metastore 33: uri = options['rack-cache.metastore'] 34: storage.resolve_metastore_uri(uri) 35: end
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 241 241: def fetch 242: # send no head requests because we want content 243: @env['REQUEST_METHOD'] = 'GET' 244: 245: response = forward 246: 247: # Mark the response as explicitly private if any of the private 248: # request headers are present and the response was not explicitly 249: # declared public. 250: if private_request? && !response.cache_control.public? 251: response.private = true 252: elsif default_ttl > 0 && response.ttl.nil? && !response.cache_control.must_revalidate? 253: # assign a default TTL for the cache entry if none was specified in 254: # the response; the must-revalidate cache control directive disables 255: # default ttl assigment. 256: response.ttl = default_ttl 257: end 258: 259: store(response) if response.cacheable? 260: 261: response 262: end
Delegate the request to the backend and create the response.
# File lib/rack/cache/context.rb, line 135 135: def forward 136: Response.new(*backend.call(@env)) 137: end
Whether the cache entry is “fresh enough” to satisfy the request.
# File lib/rack/cache/context.rb, line 124 124: def fresh_enough?(entry) 125: if entry.fresh? 126: if allow_revalidate? && max_age = @request.cache_control.max_age 127: max_age > 0 && max_age >= entry.age 128: else 129: true 130: end 131: end 132: end
Invalidate POST, PUT, DELETE and all methods not understood by this cache See RFC2616 13.10
# File lib/rack/cache/context.rb, line 148 148: def invalidate 149: metastore.invalidate(@request, entitystore) 150: rescue Exception => e 151: log_error(e) 152: pass 153: else 154: record :invalidate 155: pass 156: end
# File lib/rack/cache/context.rb, line 282 282: def log_error(exception) 283: @env['rack.errors'].write("cache error: #{exception.message}\n#{exception.backtrace.join("\n")}\n") 284: end
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 # the entry with the backend using conditional GET. When no matching cache entry is found, trigger # processing.
# File lib/rack/cache/context.rb, line 163 163: def lookup 164: if @request.no_cache? && allow_reload? 165: record :reload 166: fetch 167: else 168: begin 169: entry = metastore.lookup(@request, entitystore) 170: rescue Exception => e 171: log_error(e) 172: return pass 173: end 174: if entry 175: if fresh_enough?(entry) 176: record :fresh 177: entry.headers['Age'] = entry.age.to_s 178: entry 179: else 180: record :stale 181: validate(entry) 182: end 183: else 184: record :miss 185: fetch 186: end 187: end 188: end
Determine if the # validators (ETag, Last-Modified) matches a conditional value specified in #.
# File lib/rack/cache/context.rb, line 113 113: def not_modified?(response) 114: last_modified = @request.env['HTTP_IF_MODIFIED_SINCE'] 115: if etags = @request.env['HTTP_IF_NONE_MATCH'] 116: etags = etags.split(/\s*,\s*/) 117: (etags.include?(response.etag) || etags.include?('*')) && (!last_modified || response.last_modified == last_modified) 118: elsif last_modified 119: response.last_modified == last_modified 120: end 121: end
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 141 141: def pass 142: record :pass 143: forward 144: end
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 107 107: def private_request? 108: @private_header_keys.any? { |key| @env.key?(key) } 109: end
Record that an event took place.
# File lib/rack/cache/context.rb, line 100 100: def record(event) 101: @trace << event 102: end
Write the response to the cache.
# File lib/rack/cache/context.rb, line 265 265: def store(response) 266: strip_ignore_headers(response) 267: metastore.store(@request, response, entitystore) 268: response.headers['Age'] = response.age.to_s 269: rescue Exception => e 270: log_error(e) 271: nil 272: else 273: record :store 274: end
Remove all ignored response headers before writing to the cache.
# File lib/rack/cache/context.rb, line 277 277: def strip_ignore_headers(response) 278: stripped_values = ignore_headers.map { |name| response.headers.delete(name) } 279: record :ignore if stripped_values.any? 280: end
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 192 192: def validate(entry) 193: # send no head requests because we want content 194: @env['REQUEST_METHOD'] = 'GET' 195: 196: # add our cached last-modified validator to the environment 197: @env['HTTP_IF_MODIFIED_SINCE'] = entry.last_modified 198: 199: # Add our cached etag validator to the environment. 200: # We keep the etags from the client to handle the case when the client 201: # has a different private valid entry which is not cached here. 202: cached_etags = entry.etag.to_s.split(/\s*,\s*/) 203: request_etags = @request.env['HTTP_IF_NONE_MATCH'].to_s.split(/\s*,\s*/) 204: etags = (cached_etags + request_etags).uniq 205: @env['HTTP_IF_NONE_MATCH'] = etags.empty? ? nil : etags.join(', ') 206: 207: response = forward 208: 209: if response.status == 304 210: record :valid 211: 212: # Check if the response validated which is not cached here 213: etag = response.headers['ETag'] 214: return response if etag && request_etags.include?(etag) && !cached_etags.include?(etag) 215: 216: entry = entry.dup 217: entry.headers.delete('Date') 218: ]Date Expires Cache-Control ETag Last-Modified].each do |name| 219: next unless value = response.headers[name] 220: entry.headers[name] = value 221: end 222: 223: # even though it's empty, be sure to close the response body from upstream 224: # because middleware use close to signal end of response 225: response.body.close if response.body.respond_to?(:close) 226: 227: response = entry 228: else 229: record :invalid 230: end 231: 232: store(response) if response.cacheable? 233: 234: response 235: end
Disabled; run with --debug to generate this.
Generated with the Darkfish Rdoc Generator 1.1.6.