rack-cache 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of rack-cache might be problematic. Click here for more details.

data/CHANGES CHANGED
@@ -1,3 +1,61 @@
1
+ ## 0.3.0 / December 2008
2
+
3
+ * Add support for public and private cache control directives. Responses
4
+ marked as explicitly public are cached even when the request includes
5
+ an Authorization or Cookie header. Responses marked as explicitly private
6
+ are considered uncacheable.
7
+
8
+ * Added a "private_headers" option that dictates which request headers
9
+ trigger default "private" cache control processing. By default, the
10
+ Cookie and Authorization headers are included. Headers may be added or
11
+ removed as necessary to change the default private logic.
12
+
13
+ * Adhere to must-revalidate/proxy-revalidate cache control directives by
14
+ not assigning the default_ttl to responses that don't include freshness
15
+ information. This should let us begin using default_ttl more liberally
16
+ since we can control it using the must-revalidate/proxy-revalidate directives.
17
+
18
+ * Use the s-maxage Cache-Control value in preference to max-age when
19
+ present. The ttl= method now sets the s-maxage value instead of max-age.
20
+ Code that used ttl= to control freshness at the client needs to change
21
+ to set the max-age directive explicitly.
22
+
23
+ * Enable support for X-Sendfile middleware by responding to #to_path on
24
+ bodies served from disk storage. Adding the Rack::Sendfile component
25
+ upstream from Rack::Cache will result in cached bodies being served
26
+ directly by the web server (instead of being read in Ruby).
27
+
28
+ * BUG: MetaStore hits but EntityStore misses. This would 500 previously; now
29
+ we detect it and act as if the MetaStore missed as well.
30
+
31
+ * Implement low level #purge method on all concrete entity store
32
+ classes -- removes the entity body corresponding to the SHA1 key
33
+ provided and returns nil.
34
+
35
+ * Basically sane handling of HEAD requests. A HEAD request is never passed
36
+ through to the backend except when transitioning with pass!. This means
37
+ that the cache responds to HEAD requests without invoking the backend at
38
+ all when the cached entry is fresh. When no cache entry exists, or the
39
+ cached entry is stale and can be validated, the backend is invoked with
40
+ a GET request and the HEAD is handled right before the response
41
+ is delivered upstream.
42
+
43
+ * BUG: The Age response header was not being set properly when a stale
44
+ entry was validated. This would result in Age values that exceeded
45
+ the freshness lifetime in responses.
46
+
47
+ * BUG: A cached entry in a heap meta store could be unintentionally
48
+ modified by request processing since the cached objects were being
49
+ returned directly. The result was typically missing/incorrect header
50
+ values (e.g., missing Content-Type header). [dkubb]
51
+
52
+ * BUG: 304 responses should not include entity headers (especially
53
+ Content-Length). This is causing Safari/WebKit weirdness on 304
54
+ responses.
55
+
56
+ * BUG: The If-None-Match header was being ignored, causing the cache
57
+ to send 200 responses to matching conditional GET requests.
58
+
1
59
  ## 0.2.0 / 2008-10-24 / Initial Release
2
60
 
3
61
  * Document events and transitions in `rack/cache/config/default.rb`
data/README CHANGED
@@ -3,16 +3,14 @@ Rack::Cache
3
3
 
4
4
  Rack::Cache is suitable as a quick drop-in component to enable HTTP caching for
5
5
  Rack-based applications that produce freshness (Expires, Cache-Control) and/or
6
- validation (Last-Modified, ETag) information.
7
-
8
- We strive to implement those portions of RFC 2616 Section 13 relevant to gateway
9
- (i.e., "reverse proxy") cache scenarios with a system for specifying cache
10
- policy:
6
+ validation (Last-Modified, ETag) information:
11
7
 
12
8
  * Standards-based (RFC 2616)
13
9
  * Freshness/expiration based caching
14
- * Validation
10
+ * Validation (If-Modified-Since / If-None-Match)
15
11
  * Vary support
12
+ * Cache-Control: public, private, max-age, s-maxage, must-revalidate,
13
+ and proxy-revalidate.
16
14
  * Portable: 100% Ruby / works with any Rack-enabled framework
17
15
  * Configuration language for advanced caching policies
18
16
  * Disk, memcached, and heap memory storage backends
@@ -68,10 +66,17 @@ Assuming you've designed your backend application to take advantage of HTTP's
68
66
  caching features, no further code or configuration is required for basic
69
67
  caching.
70
68
 
71
- Documentation
72
- -------------
69
+ Links
70
+ -----
73
71
 
74
- http://tomayko.com/src/rack-cache/
72
+ Documentation:
73
+ http://tomayko.com/src/rack-cache/
74
+
75
+ Mailing List:
76
+ http://groups.google.com/group/rack-cache
77
+
78
+ GitHub:
79
+ http://github.com/rtomayko/rack-cache/
75
80
 
76
81
  License
77
82
  -------
data/Rakefile CHANGED
@@ -21,7 +21,7 @@ end
21
21
  desc 'Run specs with unit test style output'
22
22
  task :test => FileList['test/*_test.rb'] do |t|
23
23
  suite = t.prerequisites
24
- sh "testrb -Ilib:test #{suite.join(' ')}", :verbose => false
24
+ sh "specrb -Ilib:test #{suite.join(' ')}", :verbose => false
25
25
  end
26
26
 
27
27
  desc 'Generate test coverage report'
@@ -72,12 +72,12 @@ desc 'Build markdown documentation files'
72
72
  task 'doc:markdown'
73
73
  FileList['doc/*.markdown'].each do |source|
74
74
  dest = "doc/#{File.basename(source, '.markdown')}.html"
75
- file dest => source do |f|
75
+ file dest => [source, 'doc/layout.html.erb'] do |f|
76
76
  puts "markdown: #{source} -> #{dest}" if verbose
77
- require 'erb'
78
- require 'rdiscount'
77
+ require 'erb' unless defined? ERB
78
+ require 'rdiscount' unless defined? RDiscount
79
79
  template = File.read(source)
80
- content = Markdown.new(ERB.new(template, 0, "%<>").result(binding)).to_html
80
+ content = Markdown.new(ERB.new(template, 0, "%<>").result(binding), :smart).to_html
81
81
  title = content.match("<h1>(.*)</h1>")[1] rescue ''
82
82
  layout = ERB.new(File.read("doc/layout.html.erb"), 0, "%<>")
83
83
  output = layout.result(binding)
@@ -87,10 +87,16 @@ FileList['doc/*.markdown'].each do |source|
87
87
  CLEAN.include dest
88
88
  end
89
89
 
90
+ desc 'Publish documentation'
90
91
  task 'doc:publish' => :doc do
91
92
  sh 'rsync -avz doc/ gus@tomayko.com:/src/rack-cache'
92
93
  end
93
94
 
95
+ desc 'Start the documentation development server (requires thin)'
96
+ task 'doc:server' do
97
+ sh 'cd doc && thin --rackup server.ru --port 3035 start'
98
+ end
99
+
94
100
  # PACKAGING =================================================================
95
101
 
96
102
  def package(ext='')
data/TODO CHANGED
@@ -1,40 +1,32 @@
1
- ## 0.3
1
+ ## 0.4
2
2
 
3
- - BUG: meta store hits but entity misses
4
- - BUG: HEAD request on invalid entry caches zero-length response
5
- - BUG: Response body written to cache each time validation succeeds
6
- - Are we doing HEAD properly?
7
3
  - liberal, conservative, sane caching configs
8
- - Sample app
9
- - busters.rb doc and tests
10
- - no-cache.rb doc and tests
4
+ - Sample apps: Rack, Rails, Sinatra, Merb, etc.
5
+ - busters.rb and no-cache.rb doc and tests
11
6
  - Canonicalized URL for cache key:
12
7
  - sorts params by key, then value
13
8
  - urlencodes /[^ A-Za-z0-9_.-]/ host, path, and param key/value
14
- - Support server-specific X-Sendfile (or similar) for delivering cached
15
- bodies (file entity stores only).
16
- - Sqlite3 (meta store)
9
+ - Custom cache keys
17
10
  - Cache invalidation on PUT, POST, DELETE.
18
11
  - Invalidate at the request URI; or, anything that's "near" the request URI.
19
12
  - Invalidate at the URI of the Location or Content-Location response header.
20
13
 
21
14
  ## Backlog
22
15
 
16
+ - Add missing Expires header if we have a max-age.
23
17
  - Purge/invalidate specific cache entries
24
18
  - Purge/invalidate everything
25
19
  - Maximum size of cached entity
26
20
  - Last-Modified factor: requests that have a Last-Modified header but no Expires
27
21
  header have a TTL assigned based on the last modified age of the response:
28
22
  TTL = (Age * Factor), or, 1h = (10h * 0.1)
29
- - I wonder if it would be possible to run in threaded mode but with an
30
- option to lock before making requests to the backend. The idea is to be
31
- able to serve requests from cache in separate threads. This should
23
+ - Run under multiple-threads with an option to lock before making requests
24
+ to the backend. The idea is to be able to serve requests from cache in
25
+ separate threads. This should probably be implemented as a separate
26
+ middleware component.
27
+ - Consider implementing ESI (http://www.w3.org/TR/esi-lang). This should
32
28
  probably be implemented as a separate middleware component.
29
+ - Sqlite3 (meta store)
33
30
  - stale-while-revalidate
34
31
  - Serve cached copies when down (see: stale-if-error) - e.g., database
35
32
  connection drops and the cache takes over what it can.
36
- - When a cache misses due to Vary, try to validate using the best match. Note
37
- that you can't do this with a weak validator, so only strong etags can be
38
- used.
39
- - Consider implementing ESI (http://www.w3.org/TR/esi-lang). This should
40
- probably be implemented as a separate middleware component.
@@ -115,6 +115,14 @@ If no entitystore is specified, the `heap:/` store is assumed. This
115
115
  implementation has significant draw-backs so explicit configuration is
116
116
  recommended.
117
117
 
118
+ ### `private_headers`
119
+
120
+ An array of request header names that cause the response to be treated with
121
+ private cache control semantics. The default value is `['Authorization', 'Cookie']`.
122
+ If any of these headers are present in the request, the response is considered
123
+ private and will not be cached _unless_ the response is explicitly marked public
124
+ (e.g., `Cache-Control: public`).
125
+
118
126
  <a id='machinery'></a>
119
127
 
120
128
  Configuration Machinery - Events and Transitions
@@ -184,7 +192,7 @@ cache policy into files for organization and reuse.
184
192
  # more stuff here
185
193
  end
186
194
 
187
- The `breakers` and `mycacheconfig` configuration files are normal Ruby source
195
+ The `busters` and `mycacheconfig` configuration files are normal Ruby source
188
196
  files (i.e., they have a `.rb` extension) situated on the `$LOAD_PATH` - the
189
197
  `import` statement works like Ruby's `require` statement but the contents of the
190
198
  files are evaluated in the context of the configuration machinery, as if
data/doc/index.markdown CHANGED
@@ -2,10 +2,10 @@ __Rack::Cache__ is suitable as a quick drop-in component to enable HTTP caching
2
2
  for [Rack][]-based applications that produce freshness (`Expires`,
3
3
  `Cache-Control`) and/or validation (`Last-Modified`, `ETag`) information.
4
4
 
5
- * Standards-based ([RFC 2616][rfc] / [Section 13][s13]).
5
+ * Standards-based (see [RFC 2616][rfc] / [Section 13][s13]).
6
6
  * Freshness/expiration based caching
7
7
  * Validation
8
- * Vary Support
8
+ * Vary support
9
9
  * Portable: 100% Ruby / works with any [Rack][]-enabled framework.
10
10
  * [Configuration language][config] for advanced caching policies.
11
11
  * Disk, memcached, and heap memory [storage backends][storage].
@@ -47,29 +47,35 @@ simply `require` and `use` as follows:
47
47
 
48
48
  Assuming you've designed your backend application to take advantage of HTTP's
49
49
  caching features, no further code or configuration is required for basic
50
- caching. More sophisticated stuff is possible with [Rack::Cache's Configuration
51
- Language][config].
50
+ caching.
52
51
 
53
- Advanced Usage
54
- --------------
52
+ More
53
+ ----
55
54
 
56
- * [Configuration Language Documentation][config] - How to customize cache
55
+ * [Configuration Language Documentation][config] - how to customize cache
57
56
  policy using the simple event-based configuration system.
58
57
 
59
- * [Cache Storage Documentation][storage] - Detailed information on the various
58
+ * [Cache Storage Documentation][storage] - detailed information on the various
60
59
  storage implementations available in __Rack::Cache__ and how to choose the one
61
60
  that's best for your application.
62
61
 
63
- * [FAQ](./faq) - Frequently Asked Questions about __Rack::Cache__.
62
+ * [Things Caches Do][things] - an illustrated guide to how HTTP gateway
63
+ caches work with pointers to other useful resources on HTTP caching.
64
64
 
65
- * [GitHub Repository](http://github.com/rtomayko/rack-cache/) - Get your
65
+ * [GitHub Repository](http://github.com/rtomayko/rack-cache/) - get your
66
66
  fork on.
67
67
 
68
+ * [Mailing List](http://groups.google.com/group/rack-cache) - for hackers
69
+ and users (`rack-cache@groups.google.com`).
70
+
71
+ * [FAQ](./faq) - Frequently Asked Questions about __Rack::Cache__.
72
+
68
73
  * [RDoc API Documentation](./api/) - Mostly worthless if you just want to use
69
74
  __Rack::Cache__ in your application but mildly insightful if you'd like to
70
75
  get a feel for how the system has been put together; I recommend
71
76
  [reading the source](http://github.com/rtomayko/rack-cache/master/lib/rack/cache).
72
77
 
78
+
73
79
  See Also
74
80
  --------
75
81
 
@@ -99,6 +105,7 @@ and is provided under [the MIT license](./license)
99
105
 
100
106
  [config]: ./configuration "Rack::Cache Configuration Language Documentation"
101
107
  [storage]: ./storage "Rack::Cache Storage Documentation"
108
+ [things]: http://tomayko.com/writings/things-caches-do
102
109
 
103
110
  [rfc]: http://tools.ietf.org/html/rfc2616
104
111
  "RFC 2616 - Hypertext Transfer Protocol -- HTTP/1.1 [ietf.org]"
data/doc/layout.html.erb CHANGED
@@ -14,6 +14,7 @@
14
14
  <p>
15
15
  <a href="./configuration" title='Configuration Language Documentation'>Config</a> |
16
16
  <a href="./storage" title='Cache Storage Documentation'>Storage</a> |
17
+ <a href="http://tomayko.com/writings/things-caches-do" title="Things Caches Do">Things</a> |
17
18
  <a href="./faq" title='Frequently Asked Questions'>FAQ</a> |
18
19
  <a href="./api/" title='Fucking Sucks.'>RDoc</a>
19
20
  </p>
data/doc/server.ru ADDED
@@ -0,0 +1,34 @@
1
+ # Rackup config that serves the contents of Rack::Cache's
2
+ # doc directory. The documentation is rebuilt on each request.
3
+
4
+ # Rewrites URLs like conventional web server configs.
5
+ class Rewriter < Struct.new(:app)
6
+ def call(env)
7
+ if env['PATH_INFO'] =~ /\/$/
8
+ env['PATH_INFO'] += 'index.html'
9
+ elsif env['PATH_INFO'] !~ /\.\w+$/
10
+ env['PATH_INFO'] += '.html'
11
+ end
12
+ app.call(env)
13
+ end
14
+ end
15
+
16
+ # Rebuilds documentation on each request.
17
+ class DocBuilder < Struct.new(:app)
18
+ def call(env)
19
+ if env['PATH_INFO'] !~ /\.(css|js|gif|jpg|png|ico)$/
20
+ env['rack.errors'] << "*** rebuilding documentation (rake -s doc)\n"
21
+ system "rake -s doc"
22
+ end
23
+ app.call(env)
24
+ end
25
+ end
26
+
27
+ use Rack::CommonLogger
28
+ use DocBuilder
29
+ use Rewriter
30
+ use Rack::Static, :root => File.dirname(__FILE__), :urls => ["/"]
31
+
32
+ run(lambda{|env| [404,{},'<h1>Not Found</h1>']})
33
+
34
+ # vim: ft=ruby
@@ -17,7 +17,7 @@
17
17
  #
18
18
  on :receive do
19
19
  pass! unless request.method? 'GET', 'HEAD'
20
- pass! if request.header? 'Cookie', 'Authorization', 'Expect'
20
+ pass! if request.header? 'Expect'
21
21
  lookup!
22
22
  end
23
23
 
@@ -109,7 +109,6 @@ end
109
109
  # * error! - return the error code specified and abandon request.
110
110
  #
111
111
  on :store do
112
- entry.ttl = default_ttl if entry.ttl.nil?
113
112
  trace 'store backend response in cache (ttl: %ds)', entry.ttl
114
113
  persist!
115
114
  end
@@ -74,9 +74,9 @@ module Rack::Cache
74
74
  @triggered.include?(event)
75
75
  end
76
76
 
77
- private
78
77
  # Event handlers.
79
78
  attr_reader :events
79
+ private :events
80
80
 
81
81
  public
82
82
  # Attach custom logic to one or more events.
@@ -101,10 +101,18 @@ module Rack::Cache
101
101
  end
102
102
 
103
103
  private
104
- # Determine if the response's Last-Modified date matches the
105
- # If-Modified-Since value provided in the original request.
104
+ # Does the request include authorization or other sensitive information
105
+ # that should cause the response to be considered private by default?
106
+ # Private responses are not stored in the cache.
107
+ def private_request?
108
+ request.header?(*private_headers)
109
+ end
110
+
111
+ # Determine if the #response validators (ETag, Last-Modified) matches
112
+ # a conditional value specified in #original_request.
106
113
  def not_modified?
107
- response.last_modified_at?(original_request.if_modified_since)
114
+ response.etag_matches?(original_request.if_none_match) ||
115
+ response.last_modified_at?(original_request.if_modified_since)
108
116
  end
109
117
 
110
118
  # Delegate the request to the backend and create the response.
@@ -118,6 +126,7 @@ module Rack::Cache
118
126
  private
119
127
  def perform_receive
120
128
  @original_request = Request.new(@env.dup.freeze)
129
+ @env['REQUEST_METHOD'] = 'GET' if @original_request.head?
121
130
  @request = Request.new(@env)
122
131
  info "%s %s", @original_request.request_method, @original_request.fullpath
123
132
  transition(from=:receive, to=[:pass, :lookup, :error])
@@ -125,6 +134,7 @@ module Rack::Cache
125
134
 
126
135
  def perform_pass
127
136
  trace 'passing'
137
+ request.env['REQUEST_METHOD'] = @original_request.request_method
128
138
  fetch_from_backend
129
139
  transition(from=:pass, to=[:pass, :finish, :error]) do |event|
130
140
  if event == :pass
@@ -171,6 +181,7 @@ module Rack::Cache
171
181
  trace "cache entry valid"
172
182
  @response = entry.dup
173
183
  @response.headers.delete('Age')
184
+ @response.headers.delete('Date')
174
185
  @response.headers['X-Origin-Status'] = '304'
175
186
  %w[Date Expires Cache-Control Etag Last-Modified].each do |name|
176
187
  next unless value = original_response.headers[name]
@@ -189,6 +200,20 @@ module Rack::Cache
189
200
  request.env.delete('HTTP_IF_MODIFIED_SINCE')
190
201
  request.env.delete('HTTP_IF_NONE_MATCH')
191
202
  fetch_from_backend
203
+
204
+ # mark the response as explicitly private if any of the private
205
+ # request headers are present and the response was not explicitly
206
+ # declared public.
207
+ if private_request? && !@response.public?
208
+ @response.private = true
209
+ else
210
+ # assign a default TTL for the cache entry if none was specified in
211
+ # the response; the must-revalidate cache control directive disables
212
+ # default ttl assigment.
213
+ if default_ttl > 0 && @response.ttl.nil? && !@response.must_revalidate?
214
+ @response.ttl = default_ttl
215
+ end
216
+ end
192
217
  transition(from=:fetch, to=[:store, :deliver, :error])
193
218
  end
194
219
 
@@ -196,7 +221,11 @@ module Rack::Cache
196
221
  @entry = @response
197
222
  transition(from=:store, to=[:persist, :deliver, :error]) do |event|
198
223
  if event == :persist
199
- trace "writing response to cache"
224
+ if @response.private?
225
+ warn 'forced to store response marked as private.'
226
+ else
227
+ trace "storing response in cache"
228
+ end
200
229
  metastore.store(original_request, @entry, entitystore)
201
230
  @response = @entry
202
231
  :deliver
@@ -208,14 +237,13 @@ module Rack::Cache
208
237
 
209
238
  def perform_deliver
210
239
  trace "delivering response ..."
211
- if not_modified?
212
- response.status = 304
213
- response.body = []
214
- end
240
+ response.not_modified! if not_modified?
241
+ response.body = [] if @original_request.head?
215
242
  transition(from=:deliver, to=[:finish, :error])
216
243
  end
217
244
 
218
245
  def perform_finish
246
+ response.headers.delete 'X-Status'
219
247
  response.to_a
220
248
  end
221
249