rtomayko-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.
Files changed (44) hide show
  1. data/CHANGES +50 -0
  2. data/COPYING +18 -0
  3. data/README +96 -0
  4. data/Rakefile +144 -0
  5. data/TODO +42 -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/config/busters.rb +16 -0
  15. data/lib/rack/cache/config/default.rb +134 -0
  16. data/lib/rack/cache/config/no-cache.rb +13 -0
  17. data/lib/rack/cache/config.rb +65 -0
  18. data/lib/rack/cache/context.rb +95 -0
  19. data/lib/rack/cache/core.rb +271 -0
  20. data/lib/rack/cache/entitystore.rb +224 -0
  21. data/lib/rack/cache/headers.rb +277 -0
  22. data/lib/rack/cache/metastore.rb +292 -0
  23. data/lib/rack/cache/options.rb +119 -0
  24. data/lib/rack/cache/request.rb +37 -0
  25. data/lib/rack/cache/response.rb +76 -0
  26. data/lib/rack/cache/storage.rb +50 -0
  27. data/lib/rack/cache.rb +51 -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 +505 -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 +222 -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 +122 -0
@@ -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 validators (ETag, Last-Modified) matches
105
+ # a conditional value specified in #original_request.
106
+ def not_modified?
107
+ response.etag_matches?(original_request.if_none_match) ||
108
+ response.last_modified_at?(original_request.if_modified_since)
109
+ end
110
+
111
+ # Delegate the request to the backend and create the response.
112
+ def fetch_from_backend
113
+ status, headers, body = backend.call(request.env)
114
+ response = Response.new(status, headers, body)
115
+ @response = response.dup
116
+ @original_response = response.freeze
117
+ end
118
+
119
+ private
120
+ def perform_receive
121
+ @original_request = Request.new(@env.dup.freeze)
122
+ @request = Request.new(@env)
123
+ info "%s %s", @original_request.request_method, @original_request.fullpath
124
+ transition(from=:receive, to=[:pass, :lookup, :error])
125
+ end
126
+
127
+ def perform_pass
128
+ trace 'passing'
129
+ fetch_from_backend
130
+ transition(from=:pass, to=[:pass, :finish, :error]) do |event|
131
+ if event == :pass
132
+ :finish
133
+ else
134
+ event
135
+ end
136
+ end
137
+ end
138
+
139
+ def perform_error(code=500, headers={}, body=nil)
140
+ body, headers = headers, {} unless headers.is_a?(Hash)
141
+ headers = {} if headers.nil?
142
+ body = [] if body.nil? || body == ''
143
+ @response = Rack::Cache::Response.new(code, headers, body)
144
+ transition(from=:error, to=[:finish])
145
+ end
146
+
147
+ def perform_lookup
148
+ if @entry = metastore.lookup(original_request, entitystore)
149
+ if @entry.fresh?
150
+ trace 'cache hit (ttl: %ds)', @entry.ttl
151
+ transition(from=:hit, to=[:deliver, :pass, :error]) do |event|
152
+ @response = @entry if event == :deliver
153
+ event
154
+ end
155
+ else
156
+ trace 'cache stale (ttl: %ds), validating...', @entry.ttl
157
+ perform_validate
158
+ end
159
+ else
160
+ trace 'cache miss'
161
+ transition(from=:miss, to=[:fetch, :pass, :error])
162
+ end
163
+ end
164
+
165
+ def perform_validate
166
+ # add our cached validators to the backend request
167
+ request.headers['If-Modified-Since'] = entry.last_modified
168
+ request.headers['If-None-Match'] = entry.etag
169
+ fetch_from_backend
170
+
171
+ if original_response.status == 304
172
+ trace "cache entry valid"
173
+ @response = entry.dup
174
+ @response.headers.delete('Age')
175
+ @response.headers.delete('Date')
176
+ @response.headers['X-Origin-Status'] = '304'
177
+ %w[Date Expires Cache-Control Etag Last-Modified].each do |name|
178
+ next unless value = original_response.headers[name]
179
+ @response[name] = value
180
+ end
181
+ @response.activate!
182
+ else
183
+ trace "cache entry invalid"
184
+ @entry = nil
185
+ end
186
+ transition(from=:fetch, to=[:store, :deliver, :error])
187
+ end
188
+
189
+ def perform_fetch
190
+ trace "fetching response from backend"
191
+ request.env.delete('HTTP_IF_MODIFIED_SINCE')
192
+ request.env.delete('HTTP_IF_NONE_MATCH')
193
+ fetch_from_backend
194
+ transition(from=:fetch, to=[:store, :deliver, :error])
195
+ end
196
+
197
+ def perform_store
198
+ @entry = @response
199
+ transition(from=:store, to=[:persist, :deliver, :error]) do |event|
200
+ if event == :persist
201
+ trace "writing response to cache"
202
+ metastore.store(original_request, @entry, entitystore)
203
+ @response = @entry
204
+ :deliver
205
+ else
206
+ event
207
+ end
208
+ end
209
+ end
210
+
211
+ def perform_deliver
212
+ trace "delivering response ..."
213
+ response.not_modified! if not_modified?
214
+ transition(from=:deliver, to=[:finish, :error])
215
+ end
216
+
217
+ def perform_finish
218
+ response.headers.delete 'X-Status'
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