rack-cache 0.2.0 → 0.3.0

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.

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