Changesets can be listed by changeset number.
The Git repository is here.
- Revision:
- 373
- Log:
Initial import of Radiant 0.9.1, which is now packaged as a gem. This is an
import of the tagged 0.9.1 source checked out from GitHub, which isn't quite
the same as the gem distribution - but it doesn't seem to be available in an
archived form and the installed gem already has modifications, so this is
the closest I can get.
- Author:
- rool
- Date:
- Mon Mar 21 13:40:05 +0000 2011
- Size:
- 7025 Bytes
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 | record :invalidate |
137 | metastore.invalidate(@request, entitystore) |
138 | pass |
139 | end |
140 | |
141 | # Try to serve the response from cache. When a matching cache entry is |
142 | # found and is fresh, use it as the response without forwarding any |
143 | # request to the backend. When a matching cache entry is found but is |
144 | # stale, attempt to #validate the entry with the backend using conditional |
145 | # GET. When no matching cache entry is found, trigger #miss processing. |
146 | def lookup |
147 | if @request.no_cache? && allow_reload? |
148 | record :reload |
149 | fetch |
150 | elsif entry = metastore.lookup(@request, entitystore) |
151 | if fresh_enough?(entry) |
152 | record :fresh |
153 | entry.headers['Age'] = entry.age.to_s |
154 | entry |
155 | else |
156 | record :stale |
157 | validate(entry) |
158 | end |
159 | else |
160 | record :miss |
161 | fetch |
162 | end |
163 | end |
164 | |
165 | # Validate that the cache entry is fresh. The original request is used |
166 | # as a template for a conditional GET request with the backend. |
167 | def validate(entry) |
168 | # send no head requests because we want content |
169 | @env['REQUEST_METHOD'] = 'GET' |
170 | |
171 | # add our cached validators to the environment |
172 | @env['HTTP_IF_MODIFIED_SINCE'] = entry.last_modified |
173 | @env['HTTP_IF_NONE_MATCH'] = entry.etag |
174 | |
175 | backend_response = forward |
176 | |
177 | response = |
178 | if backend_response.status == 304 |
179 | record :valid |
180 | entry = entry.dup |
181 | entry.headers.delete('Date') |
182 | %w[Date Expires Cache-Control ETag Last-Modified].each do |name| |
183 | next unless value = backend_response.headers[name] |
184 | entry.headers[name] = value |
185 | end |
186 | entry |
187 | else |
188 | record :invalid |
189 | backend_response |
190 | end |
191 | |
192 | store(response) if response.cacheable? |
193 | |
194 | response |
195 | end |
196 | |
197 | # The cache missed or a reload is required. Forward the request to the |
198 | # backend and determine whether the response should be stored. |
199 | def fetch |
200 | # send no head requests because we want content |
201 | @env['REQUEST_METHOD'] = 'GET' |
202 | |
203 | # avoid that the backend sends no content |
204 | @env.delete('HTTP_IF_MODIFIED_SINCE') |
205 | @env.delete('HTTP_IF_NONE_MATCH') |
206 | |
207 | response = forward |
208 | |
209 | # Mark the response as explicitly private if any of the private |
210 | # request headers are present and the response was not explicitly |
211 | # declared public. |
212 | if private_request? && !response.cache_control.public? |
213 | response.private = true |
214 | elsif default_ttl > 0 && response.ttl.nil? && !response.cache_control.must_revalidate? |
215 | # assign a default TTL for the cache entry if none was specified in |
216 | # the response; the must-revalidate cache control directive disables |
217 | # default ttl assigment. |
218 | response.ttl = default_ttl |
219 | end |
220 | |
221 | store(response) if response.cacheable? |
222 | |
223 | response |
224 | end |
225 | |
226 | # Write the response to the cache. |
227 | def store(response) |
228 | record :store |
229 | metastore.store(@request, response, entitystore) |
230 | response.headers['Age'] = response.age.to_s |
231 | end |
232 | end |
233 | end |