rack-cache 0.2.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.

Files changed (44) hide show
  1. data/CHANGES +27 -0
  2. data/COPYING +18 -0
  3. data/README +96 -0
  4. data/Rakefile +144 -0
  5. data/TODO +40 -0
  6. data/doc/configuration.markdown +224 -0
  7. data/doc/events.dot +27 -0
  8. data/doc/faq.markdown +133 -0
  9. data/doc/index.markdown +113 -0
  10. data/doc/layout.html.erb +33 -0
  11. data/doc/license.markdown +24 -0
  12. data/doc/rack-cache.css +362 -0
  13. data/doc/storage.markdown +162 -0
  14. data/lib/rack/cache.rb +51 -0
  15. data/lib/rack/cache/config.rb +65 -0
  16. data/lib/rack/cache/config/busters.rb +16 -0
  17. data/lib/rack/cache/config/default.rb +134 -0
  18. data/lib/rack/cache/config/no-cache.rb +13 -0
  19. data/lib/rack/cache/context.rb +95 -0
  20. data/lib/rack/cache/core.rb +271 -0
  21. data/lib/rack/cache/entitystore.rb +224 -0
  22. data/lib/rack/cache/headers.rb +237 -0
  23. data/lib/rack/cache/metastore.rb +309 -0
  24. data/lib/rack/cache/options.rb +119 -0
  25. data/lib/rack/cache/request.rb +37 -0
  26. data/lib/rack/cache/response.rb +76 -0
  27. data/lib/rack/cache/storage.rb +50 -0
  28. data/lib/rack/utils/environment_headers.rb +78 -0
  29. data/rack-cache.gemspec +74 -0
  30. data/test/cache_test.rb +35 -0
  31. data/test/config_test.rb +66 -0
  32. data/test/context_test.rb +465 -0
  33. data/test/core_test.rb +84 -0
  34. data/test/entitystore_test.rb +176 -0
  35. data/test/environment_headers_test.rb +71 -0
  36. data/test/headers_test.rb +215 -0
  37. data/test/logging_test.rb +45 -0
  38. data/test/metastore_test.rb +210 -0
  39. data/test/options_test.rb +64 -0
  40. data/test/pony.jpg +0 -0
  41. data/test/response_test.rb +37 -0
  42. data/test/spec_setup.rb +189 -0
  43. data/test/storage_test.rb +94 -0
  44. metadata +120 -0
@@ -0,0 +1,95 @@
1
+ require 'rack/cache/config'
2
+ require 'rack/cache/options'
3
+ require 'rack/cache/core'
4
+ require 'rack/cache/request'
5
+ require 'rack/cache/response'
6
+ require 'rack/cache/storage'
7
+
8
+ module Rack::Cache
9
+ # 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
+
13
+ class Context
14
+ include Rack::Cache::Options
15
+ include Rack::Cache::Config
16
+ include Rack::Cache::Core
17
+
18
+ # The Rack application object immediately downstream.
19
+ attr_reader :backend
20
+
21
+ def initialize(backend, options={}, &block)
22
+ @errors = nil
23
+ @env = nil
24
+ @backend = backend
25
+ initialize_options options
26
+ initialize_core
27
+ initialize_config(&block)
28
+ end
29
+
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!
34
+
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.
38
+ def call(env)
39
+ if env['rack.run_once']
40
+ call! env
41
+ else
42
+ clone.call! env
43
+ end
44
+ end
45
+
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
51
+ end
52
+
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
57
+ end
58
+
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)
64
+ end
65
+
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)
71
+ end
72
+
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
79
+ end
80
+
81
+ def info(*message, &bk)
82
+ log :info, *message, &bk
83
+ end
84
+
85
+ def warn(*message, &bk)
86
+ log :warn, *message, &bk
87
+ end
88
+
89
+ def trace(*message, &bk)
90
+ return unless verbose?
91
+ log :trace, *message, &bk
92
+ end
93
+ end
94
+
95
+ end
@@ -0,0 +1,271 @@
1
+ require 'rack/cache/request'
2
+ require 'rack/cache/response'
3
+
4
+ module Rack::Cache
5
+ # Raised when an attempt is made to transition to an event that can
6
+ # not be transitioned from the current event.
7
+ class IllegalTransition < Exception
8
+ end
9
+
10
+ # The core logic engine and state machine. When a request is received,
11
+ # the engine begins transitioning from state to state based on the
12
+ # advice given by events. Each transition performs some piece of core
13
+ # logic, calls out to an event handler, and then kicks off the next
14
+ # transition.
15
+ #
16
+ # Five objects of interest are made available during execution:
17
+ #
18
+ # * +original_request+ - The request as originally received. This object
19
+ # is never modified.
20
+ # * +request+ - The request that may eventually be sent downstream in
21
+ # case of pass or miss. This object defaults to the +original_request+
22
+ # but may be modified or replaced entirely.
23
+ # * +original_response+ - The response exactly as specified by the
24
+ # downstream application; +nil+ on cache hit.
25
+ # * +entry+ - The response loaded from cache or stored to cache. This
26
+ # object becomes +response+ if the cached response is valid.
27
+ # * +response+ - The response that will be delivered upstream after
28
+ # processing is complete. This object may be modified as necessary.
29
+ #
30
+ # These objects can be accessed and modified from within event handlers
31
+ # to perform various types of request/response manipulation.
32
+ module Core
33
+
34
+ # The request exactly as received. The object is an instance of the
35
+ # Rack::Cache::Request class, which includes many utility methods for
36
+ # inspecting the state of the request.
37
+ #
38
+ # This object cannot be modified. If the request requires modification
39
+ # before being delivered to the downstream application, use the
40
+ # #request object.
41
+ attr_reader :original_request
42
+
43
+ # The response exactly as received from the downstream application. The
44
+ # object is an instance of the Rack::Cache::Response class, which includes
45
+ # utility methods for inspecting the state of the response.
46
+ #
47
+ # The original response should not be modified. Use the #response object to
48
+ # access the response to be sent back upstream.
49
+ attr_reader :original_response
50
+
51
+ # A response object retrieved from cache, or the response that is to be
52
+ # saved to cache, or nil if no cached response was found. The object is
53
+ # an instance of the Rack::Cache::Response class.
54
+ attr_reader :entry
55
+
56
+ # The request that will be made downstream on the application. This
57
+ # defaults to the request exactly as received (#original_request). The
58
+ # object is an instance of the Rack::Cache::Request class, which includes
59
+ # utility methods for inspecting and modifying various aspects of the
60
+ # HTTP request.
61
+ attr_reader :request
62
+
63
+ # The response that will be sent upstream. Defaults to the response
64
+ # received from the downstream application (#original_response) but
65
+ # is set to the cached #entry when valid. In any case, the object
66
+ # is an instance of the Rack::Cache::Response class, which includes a
67
+ # variety of utility methods for inspecting and modifying the HTTP
68
+ # response.
69
+ attr_reader :response
70
+
71
+ # Has the given event been performed at any time during the
72
+ # request life-cycle? Useful for testing.
73
+ def performed?(event)
74
+ @triggered.include?(event)
75
+ end
76
+
77
+ private
78
+ # Event handlers.
79
+ attr_reader :events
80
+
81
+ public
82
+ # Attach custom logic to one or more events.
83
+ def on(*events, &block)
84
+ events.each { |event| @events[event].unshift(block) }
85
+ nil
86
+ end
87
+
88
+ private
89
+ # Transitioning statements
90
+
91
+ def pass! ; throw(:transition, [:pass]) ; end
92
+ def lookup! ; throw(:transition, [:lookup]) ; end
93
+ def store! ; throw(:transition, [:store]) ; end
94
+ def fetch! ; throw(:transition, [:fetch]) ; end
95
+ def persist! ; throw(:transition, [:persist]) ; end
96
+ def deliver! ; throw(:transition, [:deliver]) ; end
97
+ def finish! ; throw(:transition, [:finish]) ; end
98
+
99
+ def error!(code=500, headers={}, body=nil)
100
+ throw(:transition, [:error, code, headers, body])
101
+ end
102
+
103
+ private
104
+ # Determine if the response's Last-Modified date matches the
105
+ # If-Modified-Since value provided in the original request.
106
+ def not_modified?
107
+ response.last_modified_at?(original_request.if_modified_since)
108
+ end
109
+
110
+ # Delegate the request to the backend and create the response.
111
+ def fetch_from_backend
112
+ status, headers, body = backend.call(request.env)
113
+ response = Response.new(status, headers, body)
114
+ @response = response.dup
115
+ @original_response = response.freeze
116
+ end
117
+
118
+ private
119
+ def perform_receive
120
+ @original_request = Request.new(@env.dup.freeze)
121
+ @request = Request.new(@env)
122
+ info "%s %s", @original_request.request_method, @original_request.fullpath
123
+ transition(from=:receive, to=[:pass, :lookup, :error])
124
+ end
125
+
126
+ def perform_pass
127
+ trace 'passing'
128
+ fetch_from_backend
129
+ transition(from=:pass, to=[:pass, :finish, :error]) do |event|
130
+ if event == :pass
131
+ :finish
132
+ else
133
+ event
134
+ end
135
+ end
136
+ end
137
+
138
+ def perform_error(code=500, headers={}, body=nil)
139
+ body, headers = headers, {} unless headers.is_a?(Hash)
140
+ headers = {} if headers.nil?
141
+ body = [] if body.nil? || body == ''
142
+ @response = Rack::Cache::Response.new(code, headers, body)
143
+ transition(from=:error, to=[:finish])
144
+ end
145
+
146
+ def perform_lookup
147
+ if @entry = metastore.lookup(original_request, entitystore)
148
+ if @entry.fresh?
149
+ trace 'cache hit (ttl: %ds)', @entry.ttl
150
+ transition(from=:hit, to=[:deliver, :pass, :error]) do |event|
151
+ @response = @entry if event == :deliver
152
+ event
153
+ end
154
+ else
155
+ trace 'cache stale (ttl: %ds), validating...', @entry.ttl
156
+ perform_validate
157
+ end
158
+ else
159
+ trace 'cache miss'
160
+ transition(from=:miss, to=[:fetch, :pass, :error])
161
+ end
162
+ end
163
+
164
+ def perform_validate
165
+ # add our cached validators to the backend request
166
+ request.headers['If-Modified-Since'] = entry.last_modified
167
+ request.headers['If-None-Match'] = entry.etag
168
+ fetch_from_backend
169
+
170
+ if original_response.status == 304
171
+ trace "cache entry valid"
172
+ @response = entry.dup
173
+ @response.headers.delete('Age')
174
+ @response.headers['X-Origin-Status'] = '304'
175
+ %w[Date Expires Cache-Control Etag Last-Modified].each do |name|
176
+ next unless value = original_response.headers[name]
177
+ @response[name] = value
178
+ end
179
+ @response.activate!
180
+ else
181
+ trace "cache entry invalid"
182
+ @entry = nil
183
+ end
184
+ transition(from=:fetch, to=[:store, :deliver, :error])
185
+ end
186
+
187
+ def perform_fetch
188
+ trace "fetching response from backend"
189
+ request.env.delete('HTTP_IF_MODIFIED_SINCE')
190
+ request.env.delete('HTTP_IF_NONE_MATCH')
191
+ fetch_from_backend
192
+ transition(from=:fetch, to=[:store, :deliver, :error])
193
+ end
194
+
195
+ def perform_store
196
+ @entry = @response
197
+ transition(from=:store, to=[:persist, :deliver, :error]) do |event|
198
+ if event == :persist
199
+ trace "writing response to cache"
200
+ metastore.store(original_request, @entry, entitystore)
201
+ @response = @entry
202
+ :deliver
203
+ else
204
+ event
205
+ end
206
+ end
207
+ end
208
+
209
+ def perform_deliver
210
+ trace "delivering response ..."
211
+ if not_modified?
212
+ response.status = 304
213
+ response.body = []
214
+ end
215
+ transition(from=:deliver, to=[:finish, :error])
216
+ end
217
+
218
+ def perform_finish
219
+ response.to_a
220
+ end
221
+
222
+ private
223
+ # Transition from the currently processing event to another event
224
+ # after triggering event handlers.
225
+ def transition(from, to)
226
+ ev, *args = trigger(from)
227
+ raise IllegalTransition, "No transition to :#{ev}" unless to.include?(ev)
228
+ ev = yield ev if block_given?
229
+ send "perform_#{ev}", *args
230
+ end
231
+
232
+ # Trigger processing of the event specified and return an array containing
233
+ # the name of the next transition and any arguments provided to the
234
+ # transitioning statement.
235
+ def trigger(event)
236
+ if @events.include? event
237
+ @triggered << event
238
+ catch(:transition) do
239
+ @events[event].each { |block| instance_eval(&block) }
240
+ nil
241
+ end
242
+ else
243
+ raise NameError, "No such event: #{event}"
244
+ end
245
+ end
246
+
247
+ private
248
+ # Setup the core prototype. The object's state after execution
249
+ # of this method will be duped and used for individual request.
250
+ def initialize_core
251
+ @triggered = []
252
+ @events = Hash.new { |h,k| h[k.to_sym] = [] }
253
+
254
+ # initialize some instance variables; we won't use them until we dup to
255
+ # process a request.
256
+ @request = nil
257
+ @response = nil
258
+ @original_request = nil
259
+ @original_response = nil
260
+ @entry = nil
261
+ end
262
+
263
+ # Process a request. This method is compatible with Rack's #call
264
+ # interface.
265
+ def process_request(env)
266
+ @triggered = []
267
+ @env = @default_options.merge(env)
268
+ perform_receive
269
+ end
270
+ end
271
+ end
@@ -0,0 +1,224 @@
1
+ require 'digest/sha1'
2
+
3
+ module Rack::Cache
4
+ # Entity stores are used to cache response bodies across requests. All
5
+ # Implementations are required to calculate a SHA checksum of the data written
6
+ # which becomes the response body's key.
7
+ class EntityStore
8
+
9
+ # Read body calculating the SHA1 checksum and size while
10
+ # yielding each chunk to the block. If the body responds to close,
11
+ # call it after iteration is complete. Return a two-tuple of the form:
12
+ # [ hexdigest, size ].
13
+ def slurp(body)
14
+ digest, size = Digest::SHA1.new, 0
15
+ body.each do |part|
16
+ size += part.length
17
+ digest << part
18
+ yield part
19
+ end
20
+ body.close if body.respond_to? :close
21
+ [ digest.hexdigest, size ]
22
+ end
23
+
24
+ private :slurp
25
+
26
+
27
+ # Stores entity bodies on the heap using a Hash object.
28
+ class Heap < EntityStore
29
+
30
+ # Create the store with the specified backing Hash.
31
+ def initialize(hash={})
32
+ @hash = hash
33
+ end
34
+
35
+ # Determine whether the response body with the specified key (SHA1)
36
+ # exists in the store.
37
+ def exist?(key)
38
+ @hash.include?(key)
39
+ end
40
+
41
+ # Return an object suitable for use as a Rack response body for the
42
+ # specified key.
43
+ def open(key)
44
+ (body = @hash[key]) && body.dup
45
+ end
46
+
47
+ # Read all data associated with the given key and return as a single
48
+ # String.
49
+ def read(key)
50
+ (body = @hash[key]) && body.join
51
+ end
52
+
53
+ # Write the Rack response body immediately and return the SHA1 key.
54
+ def write(body)
55
+ buf = []
56
+ key, size = slurp(body) { |part| buf << part }
57
+ @hash[key] = buf
58
+ [key, size]
59
+ end
60
+
61
+ def self.resolve(uri)
62
+ new
63
+ end
64
+ end
65
+
66
+ HEAP = Heap
67
+ MEM = Heap
68
+
69
+ # Stores entity bodies on disk at the specified path.
70
+ class Disk < EntityStore
71
+
72
+ # Path where entities should be stored. This directory is
73
+ # created the first time the store is instansiated if it does not
74
+ # already exist.
75
+ attr_reader :root
76
+
77
+ def initialize(root)
78
+ @root = root
79
+ FileUtils.mkdir_p root, :mode => 0755
80
+ end
81
+
82
+ def exist?(key)
83
+ File.exist?(body_path(key))
84
+ end
85
+
86
+ def read(key)
87
+ File.read(body_path(key))
88
+ rescue Errno::ENOENT
89
+ nil
90
+ end
91
+
92
+ # Open the entity body and return an IO object. The IO object's
93
+ # each method is overridden to read 8K chunks instead of lines.
94
+ def open(key)
95
+ io = File.open(body_path(key), 'rb')
96
+ def io.each
97
+ while part = read(8192)
98
+ yield part
99
+ end
100
+ end
101
+ io
102
+ rescue Errno::ENOENT
103
+ nil
104
+ end
105
+
106
+ def write(body)
107
+ filename = ['buf', $$, Thread.current.object_id].join('-')
108
+ temp_file = storage_path(filename)
109
+ key, size =
110
+ File.open(temp_file, 'wb') { |dest|
111
+ slurp(body) { |part| dest.write(part) }
112
+ }
113
+
114
+ path = body_path(key)
115
+ if File.exist?(path)
116
+ File.unlink temp_file
117
+ else
118
+ FileUtils.mkdir_p File.dirname(path), :mode => 0755
119
+ FileUtils.mv temp_file, path
120
+ end
121
+ [key, size]
122
+ end
123
+
124
+ protected
125
+ def storage_path(stem)
126
+ File.join root, stem
127
+ end
128
+
129
+ def spread(key)
130
+ key = key.dup
131
+ key[2,0] = '/'
132
+ key
133
+ end
134
+
135
+ def body_path(key)
136
+ storage_path spread(key)
137
+ end
138
+
139
+ def self.resolve(uri)
140
+ path = File.expand_path(uri.opaque || uri.path)
141
+ new path
142
+ end
143
+ end
144
+
145
+ DISK = Disk
146
+ FILE = Disk
147
+
148
+ # Stores entity bodies in memcached.
149
+ class MemCache < EntityStore
150
+
151
+ # The underlying Memcached instance used to communicate with the
152
+ # memcahced daemon.
153
+ attr_reader :cache
154
+
155
+ def initialize(server="localhost:11211", options={})
156
+ @cache =
157
+ if server.respond_to?(:stats)
158
+ server
159
+ else
160
+ require 'memcached'
161
+ Memcached.new(server, options)
162
+ end
163
+ end
164
+
165
+ def exist?(key)
166
+ cache.append(key, '')
167
+ true
168
+ rescue Memcached::NotStored
169
+ false
170
+ end
171
+
172
+ def read(key)
173
+ cache.get(key, false)
174
+ rescue Memcached::NotFound
175
+ nil
176
+ end
177
+
178
+ def open(key)
179
+ if data = read(key)
180
+ [data]
181
+ else
182
+ nil
183
+ end
184
+ end
185
+
186
+ def write(body)
187
+ buf = StringIO.new
188
+ key, size = slurp(body){|part| buf.write(part) }
189
+ cache.set(key, buf.string, 0, false)
190
+ [key, size]
191
+ end
192
+
193
+ extend Rack::Utils
194
+
195
+ # Create MemCache store for the given URI. The URI must specify
196
+ # a host and may specify a port, namespace, and options:
197
+ #
198
+ # memcached://example.com:11211/namespace?opt1=val1&opt2=val2
199
+ #
200
+ # Query parameter names and values are documented with the memcached
201
+ # library: http://tinyurl.com/4upqnd
202
+ def self.resolve(uri)
203
+ server = "#{uri.host}:#{uri.port || '11211'}"
204
+ options = parse_query(uri.query)
205
+ options.keys.each do |key|
206
+ value =
207
+ case value = options.delete(key)
208
+ when 'true' ; true
209
+ when 'false' ; false
210
+ else value.to_sym
211
+ end
212
+ options[k.to_sym] = value
213
+ end
214
+ options[:namespace] = uri.path.sub(/^\//, '')
215
+ new server, options
216
+ end
217
+ end
218
+
219
+ MEMCACHE = MemCache
220
+ MEMCACHED = MemCache
221
+
222
+ end
223
+
224
+ end