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

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