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:
- 9291 Bytes
1 | require 'fileutils' |
2 | require 'digest/sha1' |
3 | require 'rack/utils' |
4 | |
5 | module Rack::Cache |
6 | |
7 | # The MetaStore is responsible for storing meta information about a |
8 | # request/response pair keyed by the request's URL. |
9 | # |
10 | # The meta store keeps a list of request/response pairs for each canonical |
11 | # request URL. A request/response pair is a two element Array of the form: |
12 | # [request, response] |
13 | # |
14 | # The +request+ element is a Hash of Rack environment keys. Only protocol |
15 | # keys (i.e., those that start with "HTTP_") are stored. The +response+ |
16 | # element is a Hash of cached HTTP response headers for the paired request. |
17 | # |
18 | # The MetaStore class is abstract and should not be instanstiated |
19 | # directly. Concrete subclasses should implement the protected #read, |
20 | # #write, and #purge methods. Care has been taken to keep these low-level |
21 | # methods dumb and straight-forward to implement. |
22 | class MetaStore |
23 | |
24 | # Locate a cached response for the request provided. Returns a |
25 | # Rack::Cache::Response object if the cache hits or nil if no cache entry |
26 | # was found. |
27 | def lookup(request, entity_store) |
28 | key = cache_key(request) |
29 | entries = read(key) |
30 | |
31 | # bail out if we have nothing cached |
32 | return nil if entries.empty? |
33 | |
34 | # find a cached entry that matches the request. |
35 | env = request.env |
36 | match = entries.detect{|req,res| requests_match?(res['Vary'], env, req)} |
37 | return nil if match.nil? |
38 | |
39 | req, res = match |
40 | if body = entity_store.open(res['X-Content-Digest']) |
41 | restore_response(res, body) |
42 | else |
43 | # TODO the metastore referenced an entity that doesn't exist in |
44 | # the entitystore. we definitely want to return nil but we should |
45 | # also purge the entry from the meta-store when this is detected. |
46 | end |
47 | end |
48 | |
49 | # Write a cache entry to the store under the given key. Existing |
50 | # entries are read and any that match the response are removed. |
51 | # This method calls #write with the new list of cache entries. |
52 | def store(request, response, entity_store) |
53 | key = cache_key(request) |
54 | stored_env = persist_request(request) |
55 | |
56 | # write the response body to the entity store if this is the |
57 | # original response. |
58 | if response.headers['X-Content-Digest'].nil? |
59 | digest, size = entity_store.write(response.body) |
60 | response.headers['X-Content-Digest'] = digest |
61 | response.headers['Content-Length'] = size.to_s unless response.headers['Transfer-Encoding'] |
62 | response.body = entity_store.open(digest) |
63 | end |
64 | |
65 | # read existing cache entries, remove non-varying, and add this one to |
66 | # the list |
67 | vary = response.vary |
68 | entries = |
69 | read(key).reject do |env,res| |
70 | (vary == res['Vary']) && |
71 | requests_match?(vary, env, stored_env) |
72 | end |
73 | |
74 | headers = persist_response(response) |
75 | headers.delete 'Age' |
76 | |
77 | entries.unshift [stored_env, headers] |
78 | write key, entries |
79 | key |
80 | end |
81 | |
82 | # Generate a cache key for the request. |
83 | def cache_key(request) |
84 | keygen = request.env['rack-cache.cache_key'] || Key |
85 | keygen.call(request) |
86 | end |
87 | |
88 | # Invalidate all cache entries that match the request. |
89 | def invalidate(request, entity_store) |
90 | modified = false |
91 | key = cache_key(request) |
92 | entries = |
93 | read(key).map do |req, res| |
94 | response = restore_response(res) |
95 | if response.fresh? |
96 | response.expire! |
97 | modified = true |
98 | [req, persist_response(response)] |
99 | else |
100 | [req, res] |
101 | end |
102 | end |
103 | write key, entries if modified |
104 | end |
105 | |
106 | private |
107 | |
108 | # Extract the environment Hash from +request+ while making any |
109 | # necessary modifications in preparation for persistence. The Hash |
110 | # returned must be marshalable. |
111 | def persist_request(request) |
112 | env = request.env.dup |
113 | env.reject! { |key,val| key =~ /[^0-9A-Z_]/ } |
114 | env |
115 | end |
116 | |
117 | # Converts a stored response hash into a Response object. The caller |
118 | # is responsible for loading and passing the body if needed. |
119 | def restore_response(hash, body=nil) |
120 | status = hash.delete('X-Status').to_i |
121 | Rack::Cache::Response.new(status, hash, body) |
122 | end |
123 | |
124 | def persist_response(response) |
125 | hash = response.headers.to_hash |
126 | hash['X-Status'] = response.status.to_s |
127 | hash |
128 | end |
129 | |
130 | # Determine whether the two environment hashes are non-varying based on |
131 | # the vary response header value provided. |
132 | def requests_match?(vary, env1, env2) |
133 | return true if vary.nil? || vary == '' |
134 | vary.split(/[\s,]+/).all? do |header| |
135 | key = "HTTP_#{header.upcase.tr('-', '_')}" |
136 | env1[key] == env2[key] |
137 | end |
138 | end |
139 | |
140 | protected |
141 | # Locate all cached request/response pairs that match the specified |
142 | # URL key. The result must be an Array of all cached request/response |
143 | # pairs. An empty Array must be returned if nothing is cached for |
144 | # the specified key. |
145 | def read(key) |
146 | raise NotImplemented |
147 | end |
148 | |
149 | # Store an Array of request/response pairs for the given key. Concrete |
150 | # implementations should not attempt to filter or concatenate the |
151 | # list in any way. |
152 | def write(key, negotiations) |
153 | raise NotImplemented |
154 | end |
155 | |
156 | # Remove all cached entries at the key specified. No error is raised |
157 | # when the key does not exist. |
158 | def purge(key) |
159 | raise NotImplemented |
160 | end |
161 | |
162 | private |
163 | # Generate a SHA1 hex digest for the specified string. This is a |
164 | # simple utility method for meta store implementations. |
165 | def hexdigest(data) |
166 | Digest::SHA1.hexdigest(data) |
167 | end |
168 | |
169 | public |
170 | # Concrete MetaStore implementation that uses a simple Hash to store |
171 | # request/response pairs on the heap. |
172 | class Heap < MetaStore |
173 | def initialize(hash={}) |
174 | @hash = hash |
175 | end |
176 | |
177 | def read(key) |
178 | @hash.fetch(key, []).collect do |req,res| |
179 | [req.dup, res.dup] |
180 | end |
181 | end |
182 | |
183 | def write(key, entries) |
184 | @hash[key] = entries |
185 | end |
186 | |
187 | def purge(key) |
188 | @hash.delete(key) |
189 | nil |
190 | end |
191 | |
192 | def to_hash |
193 | @hash |
194 | end |
195 | |
196 | def self.resolve(uri) |
197 | new |
198 | end |
199 | end |
200 | |
201 | HEAP = Heap |
202 | MEM = HEAP |
203 | |
204 | # Concrete MetaStore implementation that stores request/response |
205 | # pairs on disk. |
206 | class Disk < MetaStore |
207 | attr_reader :root |
208 | |
209 | def initialize(root="/tmp/rack-cache/meta-#{ARGV[0]}") |
210 | @root = File.expand_path(root) |
211 | FileUtils.mkdir_p(root, :mode => 0755) |
212 | end |
213 | |
214 | def read(key) |
215 | path = key_path(key) |
216 | File.open(path, 'rb') { |io| Marshal.load(io) } |
217 | rescue Errno::ENOENT |
218 | [] |
219 | end |
220 | |
221 | def write(key, entries) |
222 | path = key_path(key) |
223 | File.open(path, 'wb') { |io| Marshal.dump(entries, io, -1) } |
224 | rescue Errno::ENOENT |
225 | Dir.mkdir(File.dirname(path), 0755) |
226 | retry |
227 | end |
228 | |
229 | def purge(key) |
230 | path = key_path(key) |
231 | File.unlink(path) |
232 | nil |
233 | rescue Errno::ENOENT |
234 | nil |
235 | end |
236 | |
237 | private |
238 | def key_path(key) |
239 | File.join(root, spread(hexdigest(key))) |
240 | end |
241 | |
242 | def spread(sha, n=2) |
243 | sha = sha.dup |
244 | sha[n,0] = '/' |
245 | sha |
246 | end |
247 | |
248 | public |
249 | def self.resolve(uri) |
250 | path = File.expand_path(uri.opaque || uri.path) |
251 | new path |
252 | end |
253 | |
254 | end |
255 | |
256 | DISK = Disk |
257 | FILE = Disk |
258 | |
259 | # Stores request/response pairs in memcached. Keys are not stored |
260 | # directly since memcached has a 250-byte limit on key names. Instead, |
261 | # the SHA1 hexdigest of the key is used. |
262 | class MemCache < MetaStore |
263 | |
264 | # The Memcached instance used to communicated with the memcached |
265 | # daemon. |
266 | attr_reader :cache |
267 | |
268 | def initialize(server="localhost:11211", options={}) |
269 | @cache = |
270 | if server.respond_to?(:stats) |
271 | server |
272 | else |
273 | require 'memcached' |
274 | Memcached.new(server, options) |
275 | end |
276 | end |
277 | |
278 | def read(key) |
279 | key = hexdigest(key) |
280 | cache.get(key) |
281 | rescue Memcached::NotFound |
282 | [] |
283 | end |
284 | |
285 | def write(key, entries) |
286 | key = hexdigest(key) |
287 | cache.set(key, entries) |
288 | end |
289 | |
290 | def purge(key) |
291 | key = hexdigest(key) |
292 | cache.delete(key) |
293 | nil |
294 | rescue Memcached::NotFound |
295 | nil |
296 | end |
297 | |
298 | extend Rack::Utils |
299 | |
300 | # Create MemCache store for the given URI. The URI must specify |
301 | # a host and may specify a port, namespace, and options: |
302 | # |
303 | # memcached://example.com:11211/namespace?opt1=val1&opt2=val2 |
304 | # |
305 | # Query parameter names and values are documented with the memcached |
306 | # library: http://tinyurl.com/4upqnd |
307 | def self.resolve(uri) |
308 | server = "#{uri.host}:#{uri.port || '11211'}" |
309 | options = parse_query(uri.query) |
310 | options.keys.each do |key| |
311 | value = |
312 | case value = options.delete(key) |
313 | when 'true' ; true |
314 | when 'false' ; false |
315 | else value.to_sym |
316 | end |
317 | options[k.to_sym] = value |
318 | end |
319 | options[:namespace] = uri.path.sub(/^\//, '') |
320 | new server, options |
321 | end |
322 | end |
323 | |
324 | MEMCACHE = MemCache |
325 | MEMCACHED = MemCache |
326 | end |
327 | |
328 | end |