josh-rack-cache 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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