rtomayko-rack-cache 0.3.0 → 0.3.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,40 +1,47 @@
1
- require 'rack/cache/config'
2
1
  require 'rack/cache/options'
3
- require 'rack/cache/core'
4
2
  require 'rack/cache/request'
5
3
  require 'rack/cache/response'
6
4
  require 'rack/cache/storage'
7
5
 
8
6
  module Rack::Cache
9
7
  # Implements Rack's middleware interface and provides the context for all
10
- # cache logic. This class includes the Options, Config, and Core modules
11
- # to provide much of its core functionality.
12
-
8
+ # cache logic, including the core logic engine.
13
9
  class Context
14
10
  include Rack::Cache::Options
15
- include Rack::Cache::Config
16
- include Rack::Cache::Core
11
+
12
+ # Array of trace Symbols
13
+ attr_reader :trace
17
14
 
18
15
  # The Rack application object immediately downstream.
19
16
  attr_reader :backend
20
17
 
21
18
  def initialize(backend, options={}, &block)
22
- @errors = nil
23
- @env = nil
24
19
  @backend = backend
20
+ @trace = []
25
21
  initialize_options options
26
- initialize_core
27
- initialize_config(&block)
22
+ instance_eval(&block) if block_given?
23
+
24
+ @private_header_keys =
25
+ private_headers.map { |name| "HTTP_#{name.upcase.tr('-', '_')}" }
28
26
  end
29
27
 
30
- # The call! method is invoked on the duplicate context instance.
31
- # process_request is defined in Core.
32
- alias_method :call!, :process_request
33
- protected :call!
28
+ # The configured MetaStore instance. Changing the rack-cache.metastore
29
+ # value effects the result of this method immediately.
30
+ def metastore
31
+ uri = options['rack-cache.metastore']
32
+ storage.resolve_metastore_uri(uri)
33
+ end
34
+
35
+ # The configured EntityStore instance. Changing the rack-cache.entitystore
36
+ # value effects the result of this method immediately.
37
+ def entitystore
38
+ uri = options['rack-cache.entitystore']
39
+ storage.resolve_entitystore_uri(uri)
40
+ end
34
41
 
35
- # The Rack call interface. The receiver acts as a prototype and runs each
36
- # request in a duplicate object, unless the +rack.run_once+ variable is set
37
- # in the environment.
42
+ # The Rack call interface. The receiver acts as a prototype and runs
43
+ # each request in a dup object unless the +rack.run_once+ variable is
44
+ # set in the environment.
38
45
  def call(env)
39
46
  if env['rack.run_once']
40
47
  call! env
@@ -43,53 +50,183 @@ module Rack::Cache
43
50
  end
44
51
  end
45
52
 
46
- public
47
- # IO-like object that receives log, warning, and error messages;
48
- # defaults to the rack.errors environment variable.
49
- def errors
50
- @errors || (@env && (@errors = @env['rack.errors'])) || STDERR
53
+ # The real Rack call interface. The caching logic is performed within
54
+ # the context of the receiver.
55
+ def call!(env)
56
+ @trace = []
57
+ @env = @default_options.merge(env)
58
+ @request = Request.new(@env.dup.freeze)
59
+
60
+ response =
61
+ if @request.get? || @request.head?
62
+ if !@env['HTTP_EXPECT']
63
+ lookup
64
+ else
65
+ pass
66
+ end
67
+ else
68
+ invalidate
69
+ end
70
+
71
+ # log trace and set X-Rack-Cache tracing header
72
+ trace = @trace.join(', ')
73
+ response.headers['X-Rack-Cache'] = trace
74
+
75
+ # write log message to rack.errors
76
+ if verbose?
77
+ message = "cache: [%s %s] %s\n" %
78
+ [@request.request_method, @request.fullpath, trace]
79
+ @env['rack.errors'].write(message)
80
+ end
81
+
82
+ # tidy up response a bit
83
+ response.not_modified! if not_modified?(response)
84
+ response.body = [] if @request.head?
85
+ response.to_a
51
86
  end
52
87
 
53
- # Set the output stream for log messages, warnings, and errors.
54
- def errors=(ioish)
55
- fail "stream must respond to :write" if ! ioish.respond_to?(:write)
56
- @errors = ioish
88
+ private
89
+
90
+ # Record that an event took place.
91
+ def record(event)
92
+ @trace << event
57
93
  end
58
94
 
59
- # The configured MetaStore instance. Changing the rack-cache.metastore
60
- # environment variable effects the result of this method immediately.
61
- def metastore
62
- uri = options['rack-cache.metastore']
63
- storage.resolve_metastore_uri(uri)
95
+ # Does the request include authorization or other sensitive information
96
+ # that should cause the response to be considered private by default?
97
+ # Private responses are not stored in the cache.
98
+ def private_request?
99
+ @private_header_keys.any? { |key| @env.key?(key) }
64
100
  end
65
101
 
66
- # The configured EntityStore instance. Changing the rack-cache.entitystore
67
- # environment variable effects the result of this method immediately.
68
- def entitystore
69
- uri = options['rack-cache.entitystore']
70
- storage.resolve_entitystore_uri(uri)
102
+ # Determine if the #response validators (ETag, Last-Modified) matches
103
+ # a conditional value specified in #request.
104
+ def not_modified?(response)
105
+ response.etag_matches?(@request.env['HTTP_IF_NONE_MATCH']) ||
106
+ response.last_modified_at?(@request.env['HTTP_IF_MODIFIED_SINCE'])
71
107
  end
72
108
 
73
- protected
74
- # Write a log message to the errors stream. +level+ is a symbol
75
- # such as :error, :warn, :info, or :trace.
76
- def log(level, message=nil, *params)
77
- errors.write("[cache] #{level}: #{message}\n" % params)
78
- errors.flush
109
+ # Whether the cache entry is "fresh enough" to satisfy the request.
110
+ def fresh_enough?(entry)
111
+ if entry.fresh?
112
+ if max_age = @request.cache_control.max_age
113
+ max_age > 0 && max_age >= entry.age
114
+ else
115
+ true
116
+ end
117
+ end
79
118
  end
80
119
 
81
- def info(*message, &bk)
82
- log :info, *message, &bk
120
+ # Delegate the request to the backend and create the response.
121
+ def forward
122
+ Response.new(*backend.call(@env))
83
123
  end
84
124
 
85
- def warn(*message, &bk)
86
- log :warn, *message, &bk
125
+ # The request is sent to the backend, and the backend's response is sent
126
+ # to the client, but is not entered into the cache.
127
+ def pass
128
+ record :pass
129
+ forward
87
130
  end
88
131
 
89
- def trace(*message, &bk)
90
- return unless verbose?
91
- log :trace, *message, &bk
132
+ # Invalidate POST, PUT, DELETE and all methods not understood by this cache
133
+ # See RFC2616 13.10
134
+ def invalidate
135
+ record :invalidate
136
+ metastore.invalidate(@request, entitystore)
137
+ pass
92
138
  end
93
- end
94
139
 
140
+ # Try to serve the response from cache. When a matching cache entry is
141
+ # found and is fresh, use it as the response without forwarding any
142
+ # request to the backend. When a matching cache entry is found but is
143
+ # stale, attempt to #validate the entry with the backend using conditional
144
+ # GET. When no matching cache entry is found, trigger #miss processing.
145
+ def lookup
146
+ if @request.no_cache?
147
+ record :reload
148
+ fetch
149
+ elsif entry = metastore.lookup(@request, entitystore)
150
+ if fresh_enough?(entry)
151
+ record :fresh
152
+ entry.headers['Age'] = entry.age.to_s
153
+ entry
154
+ else
155
+ record :stale
156
+ validate(entry)
157
+ end
158
+ else
159
+ record :miss
160
+ fetch
161
+ end
162
+ end
163
+
164
+ # Validate that the cache entry is fresh. The original request is used
165
+ # as a template for a conditional GET request with the backend.
166
+ def validate(entry)
167
+ # send no head requests because we want content
168
+ @env['REQUEST_METHOD'] = 'GET'
169
+
170
+ # add our cached validators to the environment
171
+ @env['HTTP_IF_MODIFIED_SINCE'] = entry.last_modified
172
+ @env['HTTP_IF_NONE_MATCH'] = entry.etag
173
+
174
+ backend_response = forward
175
+
176
+ response =
177
+ if backend_response.status == 304
178
+ record :valid
179
+ entry = entry.dup
180
+ entry.headers.delete('Date')
181
+ %w[Date Expires Cache-Control ETag Last-Modified].each do |name|
182
+ next unless value = backend_response.headers[name]
183
+ entry.headers[name] = value
184
+ end
185
+ entry
186
+ else
187
+ record :invalid
188
+ backend_response
189
+ end
190
+
191
+ store(response) if response.cacheable?
192
+
193
+ response
194
+ end
195
+
196
+ # The cache missed or a reload is required. Forward the request to the
197
+ # backend and determine whether the response should be stored.
198
+ def fetch
199
+ # send no head requests because we want content
200
+ @env['REQUEST_METHOD'] = 'GET'
201
+
202
+ # avoid that the backend sends no content
203
+ @env.delete('HTTP_IF_MODIFIED_SINCE')
204
+ @env.delete('HTTP_IF_NONE_MATCH')
205
+
206
+ response = forward
207
+
208
+ # Mark the response as explicitly private if any of the private
209
+ # request headers are present and the response was not explicitly
210
+ # declared public.
211
+ if private_request? && !response.cache_control.public?
212
+ response.private = true
213
+ elsif default_ttl > 0 && response.ttl.nil? && !response.cache_control.must_revalidate?
214
+ # assign a default TTL for the cache entry if none was specified in
215
+ # the response; the must-revalidate cache control directive disables
216
+ # default ttl assigment.
217
+ response.ttl = default_ttl
218
+ end
219
+
220
+ store(response) if response.cacheable?
221
+
222
+ response
223
+ end
224
+
225
+ # Write the response to the cache.
226
+ def store(response)
227
+ record :store
228
+ metastore.store(@request, response, entitystore)
229
+ response.headers['Age'] = response.age.to_s
230
+ end
231
+ end
95
232
  end
@@ -1,6 +1,7 @@
1
1
  require 'digest/sha1'
2
2
 
3
3
  module Rack::Cache
4
+
4
5
  # Entity stores are used to cache response bodies across requests. All
5
6
  # Implementations are required to calculate a SHA checksum of the data written
6
7
  # which becomes the response body's key.
@@ -13,7 +14,7 @@ module Rack::Cache
13
14
  def slurp(body)
14
15
  digest, size = Digest::SHA1.new, 0
15
16
  body.each do |part|
16
- size += part.length
17
+ size += bytesize(part)
17
18
  digest << part
18
19
  yield part
19
20
  end
@@ -21,7 +22,13 @@ module Rack::Cache
21
22
  [ digest.hexdigest, size ]
22
23
  end
23
24
 
24
- private :slurp
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
25
32
 
26
33
 
27
34
  # Stores entity bodies on the heap using a Hash object.
@@ -90,7 +97,7 @@ module Rack::Cache
90
97
  end
91
98
 
92
99
  def read(key)
93
- File.read(body_path(key))
100
+ File.open(body_path(key), 'rb') { |f| f.read }
94
101
  rescue Errno::ENOENT
95
102
  nil
96
103
  end
@@ -241,7 +248,6 @@ module Rack::Cache
241
248
 
242
249
  MEMCACHE = MemCache
243
250
  MEMCACHED = MemCache
244
-
245
251
  end
246
252
 
247
253
  end
@@ -0,0 +1,52 @@
1
+ require 'rack/utils'
2
+
3
+ module Rack::Cache
4
+ class Key
5
+ include Rack::Utils
6
+
7
+ # Implement .call, since it seems like the "Rack-y" thing to do. Plus, it
8
+ # opens the door for cache key generators to just be blocks.
9
+ def self.call(request)
10
+ new(request).generate
11
+ end
12
+
13
+ def initialize(request)
14
+ @request = request
15
+ end
16
+
17
+ # Generate a normalized cache key for the request.
18
+ def generate
19
+ parts = []
20
+ parts << @request.scheme << "://"
21
+ parts << @request.host
22
+
23
+ if @request.scheme == "https" && @request.port != 443 ||
24
+ @request.scheme == "http" && @request.port != 80
25
+ parts << ":" << @request.port.to_s
26
+ end
27
+
28
+ parts << @request.script_name
29
+ parts << @request.path_info
30
+
31
+ if qs = query_string
32
+ parts << "?"
33
+ parts << qs
34
+ end
35
+
36
+ parts.join
37
+ end
38
+
39
+ private
40
+ # Build a normalized query string by alphabetizing all keys/values
41
+ # and applying consistent escaping.
42
+ def query_string
43
+ return nil if @request.query_string.nil?
44
+
45
+ @request.query_string.split(/[&;] */n).
46
+ map { |p| unescape(p).split('=', 2) }.
47
+ sort.
48
+ map { |k,v| "#{escape(k)}=#{escape(v)}" }.
49
+ join('&')
50
+ end
51
+ end
52
+ end
@@ -1,6 +1,6 @@
1
- require 'rack'
2
1
  require 'fileutils'
3
2
  require 'digest/sha1'
3
+ require 'rack/utils'
4
4
 
5
5
  module Rack::Cache
6
6
 
@@ -25,7 +25,8 @@ module Rack::Cache
25
25
  # Rack::Cache::Response object if the cache hits or nil if no cache entry
26
26
  # was found.
27
27
  def lookup(request, entity_store)
28
- entries = read(request.fullpath)
28
+ key = cache_key(request)
29
+ entries = read(key)
29
30
 
30
31
  # bail out if we have nothing cached
31
32
  return nil if entries.empty?
@@ -37,9 +38,7 @@ module Rack::Cache
37
38
 
38
39
  req, res = match
39
40
  if body = entity_store.open(res['X-Content-Digest'])
40
- response = Rack::Cache::Response.new(res['X-Status'].to_i, res, body)
41
- response.activate!
42
- response
41
+ restore_response(res, body)
43
42
  else
44
43
  # TODO the metastore referenced an entity that doesn't exist in
45
44
  # the entitystore. we definitely want to return nil but we should
@@ -50,21 +49,17 @@ module Rack::Cache
50
49
  # Write a cache entry to the store under the given key. Existing
51
50
  # entries are read and any that match the response are removed.
52
51
  # This method calls #write with the new list of cache entries.
53
- #--
54
- # TODO canonicalize URL key
55
52
  def store(request, response, entity_store)
56
- key = request.fullpath
53
+ key = cache_key(request)
57
54
  stored_env = persist_request(request)
58
55
 
59
56
  # write the response body to the entity store if this is the
60
57
  # original response.
61
- response['X-Status'] = response.status.to_s
62
- if response['X-Content-Digest'].nil?
58
+ if response.headers['X-Content-Digest'].nil?
63
59
  digest, size = entity_store.write(response.body)
64
- response['X-Content-Digest'] = digest
65
- response['Content-Length'] = size.to_s unless response['Transfer-Encoding']
60
+ response.headers['X-Content-Digest'] = digest
61
+ response.headers['Content-Length'] = size.to_s unless response.headers['Transfer-Encoding']
66
62
  response.body = entity_store.open(digest)
67
- response.activate!
68
63
  end
69
64
 
70
65
  # read existing cache entries, remove non-varying, and add this one to
@@ -75,11 +70,41 @@ module Rack::Cache
75
70
  (vary == res['Vary']) &&
76
71
  requests_match?(vary, env, stored_env)
77
72
  end
78
- entries.unshift [stored_env, {}.update(response.headers)]
73
+
74
+ headers = persist_response(response)
75
+ headers.delete 'Age'
76
+
77
+ entries.unshift [stored_env, headers]
79
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
80
104
  end
81
105
 
82
106
  private
107
+
83
108
  # Extract the environment Hash from +request+ while making any
84
109
  # necessary modifications in preparation for persistence. The Hash
85
110
  # returned must be marshalable.
@@ -89,6 +114,19 @@ module Rack::Cache
89
114
  env
90
115
  end
91
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
+
92
130
  # Determine whether the two environment hashes are non-varying based on
93
131
  # the vary response header value provided.
94
132
  def requests_match?(vary, env1, env2)
@@ -122,7 +160,6 @@ module Rack::Cache
122
160
  end
123
161
 
124
162
  private
125
-
126
163
  # Generate a SHA1 hex digest for the specified string. This is a
127
164
  # simple utility method for meta store implementations.
128
165
  def hexdigest(data)
@@ -130,7 +167,6 @@ module Rack::Cache
130
167
  end
131
168
 
132
169
  public
133
-
134
170
  # Concrete MetaStore implementation that uses a simple Hash to store
135
171
  # request/response pairs on the heap.
136
172
  class Heap < MetaStore