josh-rack-cache 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,253 @@
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
+ metastore.invalidate(@request, entitystore)
137
+ rescue Exception => e
138
+ log_error(e)
139
+ pass
140
+ else
141
+ record :invalidate
142
+ pass
143
+ end
144
+
145
+ # Try to serve the response from cache. When a matching cache entry is
146
+ # found and is fresh, use it as the response without forwarding any
147
+ # request to the backend. When a matching cache entry is found but is
148
+ # stale, attempt to #validate the entry with the backend using conditional
149
+ # GET. When no matching cache entry is found, trigger #miss processing.
150
+ def lookup
151
+ if @request.no_cache? && allow_reload?
152
+ record :reload
153
+ fetch
154
+ else
155
+ begin
156
+ entry = metastore.lookup(@request, entitystore)
157
+ rescue Exception => e
158
+ log_error(e)
159
+ return pass
160
+ end
161
+ if entry
162
+ if fresh_enough?(entry)
163
+ record :fresh
164
+ entry.headers['Age'] = entry.age.to_s
165
+ entry
166
+ else
167
+ record :stale
168
+ validate(entry)
169
+ end
170
+ else
171
+ record :miss
172
+ fetch
173
+ end
174
+ end
175
+ end
176
+
177
+ # Validate that the cache entry is fresh. The original request is used
178
+ # as a template for a conditional GET request with the backend.
179
+ def validate(entry)
180
+ # send no head requests because we want content
181
+ @env['REQUEST_METHOD'] = 'GET'
182
+
183
+ # add our cached validators to the environment
184
+ @env['HTTP_IF_MODIFIED_SINCE'] = entry.last_modified
185
+ @env['HTTP_IF_NONE_MATCH'] = entry.etag
186
+
187
+ backend_response = forward
188
+
189
+ response =
190
+ if backend_response.status == 304
191
+ record :valid
192
+ entry = entry.dup
193
+ entry.headers.delete('Date')
194
+ %w[Date Expires Cache-Control ETag Last-Modified].each do |name|
195
+ next unless value = backend_response.headers[name]
196
+ entry.headers[name] = value
197
+ end
198
+ entry
199
+ else
200
+ record :invalid
201
+ backend_response
202
+ end
203
+
204
+ store(response) if response.cacheable?
205
+
206
+ response
207
+ end
208
+
209
+ # The cache missed or a reload is required. Forward the request to the
210
+ # backend and determine whether the response should be stored.
211
+ def fetch
212
+ # send no head requests because we want content
213
+ @env['REQUEST_METHOD'] = 'GET'
214
+
215
+ # avoid that the backend sends no content
216
+ @env.delete('HTTP_IF_MODIFIED_SINCE')
217
+ @env.delete('HTTP_IF_NONE_MATCH')
218
+
219
+ response = forward
220
+
221
+ # Mark the response as explicitly private if any of the private
222
+ # request headers are present and the response was not explicitly
223
+ # declared public.
224
+ if private_request? && !response.cache_control.public?
225
+ response.private = true
226
+ elsif default_ttl > 0 && response.ttl.nil? && !response.cache_control.must_revalidate?
227
+ # assign a default TTL for the cache entry if none was specified in
228
+ # the response; the must-revalidate cache control directive disables
229
+ # default ttl assigment.
230
+ response.ttl = default_ttl
231
+ end
232
+
233
+ store(response) if response.cacheable?
234
+
235
+ response
236
+ end
237
+
238
+ # Write the response to the cache.
239
+ def store(response)
240
+ metastore.store(@request, response, entitystore)
241
+ response.headers['Age'] = response.age.to_s
242
+ rescue Exception => e
243
+ log_error(e)
244
+ nil
245
+ else
246
+ record :store
247
+ end
248
+
249
+ def log_error(exception)
250
+ @env['rack.errors'].write("cache error: #{exception.message}\n#{exception.backtrace.join("\n")}\n")
251
+ end
252
+ end
253
+ end
@@ -0,0 +1,339 @@
1
+ require 'digest/sha1'
2
+
3
+ module Rack::Cache
4
+
5
+ # Entity stores are used to cache response bodies across requests. All
6
+ # Implementations are required to calculate a SHA checksum of the data written
7
+ # which becomes the response body's key.
8
+ class EntityStore
9
+
10
+ # Read body calculating the SHA1 checksum and size while
11
+ # yielding each chunk to the block. If the body responds to close,
12
+ # call it after iteration is complete. Return a two-tuple of the form:
13
+ # [ hexdigest, size ].
14
+ def slurp(body)
15
+ digest, size = Digest::SHA1.new, 0
16
+ body.each do |part|
17
+ size += bytesize(part)
18
+ digest << part
19
+ yield part
20
+ end
21
+ body.close if body.respond_to? :close
22
+ [digest.hexdigest, size]
23
+ end
24
+
25
+ if ''.respond_to?(:bytesize)
26
+ def bytesize(string); string.bytesize; end
27
+ else
28
+ def bytesize(string); string.size; end
29
+ end
30
+
31
+ private :slurp, :bytesize
32
+
33
+
34
+ # Stores entity bodies on the heap using a Hash object.
35
+ class Heap < EntityStore
36
+
37
+ # Create the store with the specified backing Hash.
38
+ def initialize(hash={})
39
+ @hash = hash
40
+ end
41
+
42
+ # Determine whether the response body with the specified key (SHA1)
43
+ # exists in the store.
44
+ def exist?(key)
45
+ @hash.include?(key)
46
+ end
47
+
48
+ # Return an object suitable for use as a Rack response body for the
49
+ # specified key.
50
+ def open(key)
51
+ (body = @hash[key]) && body.dup
52
+ end
53
+
54
+ # Read all data associated with the given key and return as a single
55
+ # String.
56
+ def read(key)
57
+ (body = @hash[key]) && body.join
58
+ end
59
+
60
+ # Write the Rack response body immediately and return the SHA1 key.
61
+ def write(body)
62
+ buf = []
63
+ key, size = slurp(body) { |part| buf << part }
64
+ @hash[key] = buf
65
+ [key, size]
66
+ end
67
+
68
+ # Remove the body corresponding to key; return nil.
69
+ def purge(key)
70
+ @hash.delete(key)
71
+ nil
72
+ end
73
+
74
+ def self.resolve(uri)
75
+ new
76
+ end
77
+ end
78
+
79
+ HEAP = Heap
80
+ MEM = Heap
81
+
82
+ # Stores entity bodies on disk at the specified path.
83
+ class Disk < EntityStore
84
+
85
+ # Path where entities should be stored. This directory is
86
+ # created the first time the store is instansiated if it does not
87
+ # already exist.
88
+ attr_reader :root
89
+
90
+ def initialize(root)
91
+ @root = root
92
+ FileUtils.mkdir_p root, :mode => 0755
93
+ end
94
+
95
+ def exist?(key)
96
+ File.exist?(body_path(key))
97
+ end
98
+
99
+ def read(key)
100
+ File.open(body_path(key), 'rb') { |f| f.read }
101
+ rescue Errno::ENOENT
102
+ nil
103
+ end
104
+
105
+ class Body < ::File #:nodoc:
106
+ def each
107
+ while part = read(8192)
108
+ yield part
109
+ end
110
+ end
111
+ alias_method :to_path, :path
112
+ end
113
+
114
+ # Open the entity body and return an IO object. The IO object's
115
+ # each method is overridden to read 8K chunks instead of lines.
116
+ def open(key)
117
+ Body.open(body_path(key), 'rb')
118
+ rescue Errno::ENOENT
119
+ nil
120
+ end
121
+
122
+ def write(body)
123
+ filename = ['buf', $$, Thread.current.object_id].join('-')
124
+ temp_file = storage_path(filename)
125
+ key, size =
126
+ File.open(temp_file, 'wb') { |dest|
127
+ slurp(body) { |part| dest.write(part) }
128
+ }
129
+
130
+ path = body_path(key)
131
+ if File.exist?(path)
132
+ File.unlink temp_file
133
+ else
134
+ FileUtils.mkdir_p File.dirname(path), :mode => 0755
135
+ FileUtils.mv temp_file, path
136
+ end
137
+ [key, size]
138
+ end
139
+
140
+ def purge(key)
141
+ File.unlink body_path(key)
142
+ nil
143
+ rescue Errno::ENOENT
144
+ nil
145
+ end
146
+
147
+ protected
148
+ def storage_path(stem)
149
+ File.join root, stem
150
+ end
151
+
152
+ def spread(key)
153
+ key = key.dup
154
+ key[2,0] = '/'
155
+ key
156
+ end
157
+
158
+ def body_path(key)
159
+ storage_path spread(key)
160
+ end
161
+
162
+ def self.resolve(uri)
163
+ path = File.expand_path(uri.opaque || uri.path)
164
+ new path
165
+ end
166
+ end
167
+
168
+ DISK = Disk
169
+ FILE = Disk
170
+
171
+ # Base class for memcached entity stores.
172
+ class MemCacheBase < EntityStore
173
+ # The underlying Memcached instance used to communicate with the
174
+ # memcached daemon.
175
+ attr_reader :cache
176
+
177
+ extend Rack::Utils
178
+
179
+ def open(key)
180
+ data = read(key)
181
+ data && [data]
182
+ end
183
+
184
+ def self.resolve(uri)
185
+ if uri.respond_to?(:scheme)
186
+ server = "#{uri.host}:#{uri.port || '11211'}"
187
+ options = parse_query(uri.query)
188
+ options.keys.each do |key|
189
+ value =
190
+ case value = options.delete(key)
191
+ when 'true' ; true
192
+ when 'false' ; false
193
+ else value.to_sym
194
+ end
195
+ options[k.to_sym] = value
196
+ end
197
+ options[:namespace] = uri.path.sub(/^\//, '')
198
+ new server, options
199
+ else
200
+ # if the object provided is not a URI, pass it straight through
201
+ # to the underlying implementation.
202
+ new uri
203
+ end
204
+ end
205
+ end
206
+
207
+ # Uses the memcache-client ruby library. This is the default unless
208
+ # the memcached library has already been required.
209
+ class MemCache < MemCacheBase
210
+ def initialize(server="localhost:11211", options={})
211
+ @cache =
212
+ if server.respond_to?(:stats)
213
+ server
214
+ else
215
+ require 'memcache'
216
+ ::MemCache.new(server, options)
217
+ end
218
+ end
219
+
220
+ def exist?(key)
221
+ !cache.get(key).nil?
222
+ end
223
+
224
+ def read(key)
225
+ cache.get(key)
226
+ end
227
+
228
+ def write(body)
229
+ buf = StringIO.new
230
+ key, size = slurp(body){|part| buf.write(part) }
231
+ [key, size] if cache.set(key, buf.string)
232
+ end
233
+
234
+ def purge(key)
235
+ cache.delete(key)
236
+ nil
237
+ end
238
+ end
239
+
240
+ # Uses the memcached client library. The ruby based memcache-client is used
241
+ # in preference to this store unless the memcached library has already been
242
+ # required.
243
+ class MemCached < MemCacheBase
244
+ def initialize(server="localhost:11211", options={})
245
+ options[:prefix_key] ||= options.delete(:namespace) if options.key?(:namespace)
246
+ @cache =
247
+ if server.respond_to?(:stats)
248
+ server
249
+ else
250
+ require 'memcached'
251
+ ::Memcached.new(server, options)
252
+ end
253
+ end
254
+
255
+ def exist?(key)
256
+ cache.append(key, '')
257
+ true
258
+ rescue ::Memcached::NotStored
259
+ false
260
+ end
261
+
262
+ def read(key)
263
+ cache.get(key, false)
264
+ rescue ::Memcached::NotFound
265
+ nil
266
+ end
267
+
268
+ def write(body)
269
+ buf = StringIO.new
270
+ key, size = slurp(body){|part| buf.write(part) }
271
+ cache.set(key, buf.string, 0, false)
272
+ [key, size]
273
+ end
274
+
275
+ def purge(key)
276
+ cache.delete(key)
277
+ nil
278
+ rescue ::Memcached::NotFound
279
+ nil
280
+ end
281
+ end
282
+
283
+ MEMCACHE =
284
+ if defined?(::Memcached)
285
+ MemCached
286
+ else
287
+ MemCache
288
+ end
289
+
290
+ MEMCACHED = MEMCACHE
291
+
292
+ class GAEStore < EntityStore
293
+ attr_reader :cache
294
+
295
+ def initialize(options = {})
296
+ require 'rack/cache/appengine'
297
+ @cache = Rack::Cache::AppEngine::MemCache.new(options)
298
+ end
299
+
300
+ def exist?(key)
301
+ cache.contains?(key)
302
+ end
303
+
304
+ def read(key)
305
+ cache.get(key)
306
+ end
307
+
308
+ def open(key)
309
+ if data = read(key)
310
+ [data]
311
+ else
312
+ nil
313
+ end
314
+ end
315
+
316
+ def write(body)
317
+ buf = StringIO.new
318
+ key, size = slurp(body){|part| buf.write(part) }
319
+ cache.put(key, buf.string)
320
+ [key, size]
321
+ end
322
+
323
+ def purge(key)
324
+ cache.delete(key)
325
+ nil
326
+ end
327
+
328
+ def self.resolve(uri)
329
+ self.new(:namespace => uri.host)
330
+ end
331
+
332
+ end
333
+
334
+ GAECACHE = GAEStore
335
+ GAE = GAEStore
336
+
337
+ end
338
+
339
+ end