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.
- data/CHANGES +50 -0
- data/COPYING +18 -0
- data/README +96 -0
- data/Rakefile +144 -0
- data/TODO +42 -0
- data/doc/configuration.markdown +224 -0
- data/doc/events.dot +27 -0
- data/doc/faq.markdown +133 -0
- data/doc/index.markdown +113 -0
- data/doc/layout.html.erb +33 -0
- data/doc/license.markdown +24 -0
- data/doc/rack-cache.css +362 -0
- data/doc/storage.markdown +162 -0
- data/lib/rack/cache/config/busters.rb +16 -0
- data/lib/rack/cache/config/default.rb +134 -0
- data/lib/rack/cache/config/no-cache.rb +13 -0
- data/lib/rack/cache/config.rb +65 -0
- data/lib/rack/cache/context.rb +95 -0
- data/lib/rack/cache/core.rb +271 -0
- data/lib/rack/cache/entitystore.rb +224 -0
- data/lib/rack/cache/headers.rb +277 -0
- data/lib/rack/cache/metastore.rb +292 -0
- data/lib/rack/cache/options.rb +119 -0
- data/lib/rack/cache/request.rb +37 -0
- data/lib/rack/cache/response.rb +76 -0
- data/lib/rack/cache/storage.rb +50 -0
- data/lib/rack/cache.rb +51 -0
- data/lib/rack/utils/environment_headers.rb +78 -0
- data/rack-cache.gemspec +74 -0
- data/test/cache_test.rb +35 -0
- data/test/config_test.rb +66 -0
- data/test/context_test.rb +505 -0
- data/test/core_test.rb +84 -0
- data/test/entitystore_test.rb +176 -0
- data/test/environment_headers_test.rb +71 -0
- data/test/headers_test.rb +222 -0
- data/test/logging_test.rb +45 -0
- data/test/metastore_test.rb +210 -0
- data/test/options_test.rb +64 -0
- data/test/pony.jpg +0 -0
- data/test/response_test.rb +37 -0
- data/test/spec_setup.rb +189 -0
- data/test/storage_test.rb +94 -0
- metadata +122 -0
@@ -0,0 +1,277 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'rack/utils/environment_headers'
|
3
|
+
|
4
|
+
module Rack::Cache
|
5
|
+
# Generic HTTP header helper methods. Provides access to headers that can be
|
6
|
+
# included in requests and responses. This can be mixed into any object that
|
7
|
+
# responds to #headers by returning a Hash.
|
8
|
+
|
9
|
+
module Headers
|
10
|
+
# Determine if any of the header names exist:
|
11
|
+
# if header?('Authorization', 'Cookie')
|
12
|
+
# ...
|
13
|
+
# end
|
14
|
+
def header?(*names)
|
15
|
+
names.any? { |name| headers.include?(name) }
|
16
|
+
end
|
17
|
+
|
18
|
+
# A Hash of name=value pairs that correspond to the Cache-Control header.
|
19
|
+
# Valueless parameters (e.g., must-revalidate, no-store) have a Hash value
|
20
|
+
# of true. This method always returns a Hash, empty if no Cache-Control
|
21
|
+
# header is present.
|
22
|
+
def cache_control
|
23
|
+
@cache_control ||=
|
24
|
+
(headers['Cache-Control'] || '').split(/\s*,\s*/).inject({}) {|hash,token|
|
25
|
+
name, value = token.split(/\s*=\s*/, 2)
|
26
|
+
hash[name.downcase] = (value || true) unless name.empty?
|
27
|
+
hash
|
28
|
+
}.freeze
|
29
|
+
end
|
30
|
+
|
31
|
+
# Set the Cache-Control header to the values specified by the Hash. See
|
32
|
+
# the #cache_control method for information on expected Hash structure.
|
33
|
+
def cache_control=(hash)
|
34
|
+
value =
|
35
|
+
hash.collect { |key,value|
|
36
|
+
next nil unless value
|
37
|
+
next key if value == true
|
38
|
+
"#{key}=#{value}"
|
39
|
+
}.compact.join(', ')
|
40
|
+
if value.empty?
|
41
|
+
headers.delete('Cache-Control')
|
42
|
+
@cache_control = {}
|
43
|
+
else
|
44
|
+
headers['Cache-Control'] = value
|
45
|
+
@cache_control = hash.dup.freeze
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# The literal value of the ETag HTTP header or nil if no ETag is specified.
|
50
|
+
def etag
|
51
|
+
headers['Etag']
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# HTTP request header helpers. When included in Rack::Cache::Request, headers
|
56
|
+
# may be accessed by their standard RFC 2616 names using the #headers Hash.
|
57
|
+
module RequestHeaders
|
58
|
+
include Rack::Cache::Headers
|
59
|
+
|
60
|
+
# A Hash-like object providing access to HTTP request headers.
|
61
|
+
def headers
|
62
|
+
@headers ||= Rack::Utils::EnvironmentHeaders.new(env)
|
63
|
+
end
|
64
|
+
|
65
|
+
# The literal value of the If-Modified-Since request header or nil when
|
66
|
+
# no If-Modified-Since header is present.
|
67
|
+
def if_modified_since
|
68
|
+
headers['If-Modified-Since']
|
69
|
+
end
|
70
|
+
|
71
|
+
# The literal value of the If-None-Match request header or nil when
|
72
|
+
# no If-None-Match header is present.
|
73
|
+
def if_none_match
|
74
|
+
headers['If-None-Match']
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# HTTP response header helper methods.
|
79
|
+
module ResponseHeaders
|
80
|
+
include Rack::Cache::Headers
|
81
|
+
|
82
|
+
# Status codes of responses that MAY be stored by a cache or used in reply
|
83
|
+
# to a subsequent request.
|
84
|
+
#
|
85
|
+
# http://tools.ietf.org/html/rfc2616#section-13.4
|
86
|
+
CACHEABLE_RESPONSE_CODES = [
|
87
|
+
200, # OK
|
88
|
+
203, # Non-Authoritative Information
|
89
|
+
300, # Multiple Choices
|
90
|
+
301, # Moved Permanently
|
91
|
+
302, # Found
|
92
|
+
404, # Not Found
|
93
|
+
410 # Gone
|
94
|
+
].to_set
|
95
|
+
|
96
|
+
# Determine if the response is "fresh". Fresh responses may be served from
|
97
|
+
# cache without any interaction with the origin. A response is considered
|
98
|
+
# fresh when it includes a Cache-Control/max-age indicator or Expiration
|
99
|
+
# header and the calculated age is less than the freshness lifetime.
|
100
|
+
def fresh?
|
101
|
+
ttl && ttl > 0
|
102
|
+
end
|
103
|
+
|
104
|
+
# Determine if the response is "stale". Stale responses must be validated
|
105
|
+
# with the origin before use. This is the inverse of #fresh?.
|
106
|
+
def stale?
|
107
|
+
!fresh?
|
108
|
+
end
|
109
|
+
|
110
|
+
# Determine if the response is worth caching under any circumstance. An
|
111
|
+
# object that is cacheable may not necessary be served from cache without
|
112
|
+
# first validating the response with the origin.
|
113
|
+
#
|
114
|
+
# An object that includes no freshness lifetime (Expires, max-age) and that
|
115
|
+
# does not include a validator (Last-Modified, Etag) serves no purpose in a
|
116
|
+
# cache that only serves fresh or valid objects.
|
117
|
+
def cacheable?
|
118
|
+
return false unless CACHEABLE_RESPONSE_CODES.include?(status)
|
119
|
+
return false if no_store?
|
120
|
+
validateable? || fresh?
|
121
|
+
end
|
122
|
+
|
123
|
+
# The response includes specific information about its freshness. True when
|
124
|
+
# a +Cache-Control+ header with +max-age+ value is present or when the
|
125
|
+
# +Expires+ header is set.
|
126
|
+
def freshness_information?
|
127
|
+
header?('Expires') || !cache_control['max-age'].nil?
|
128
|
+
end
|
129
|
+
|
130
|
+
# Determine if the response includes headers that can be used to validate
|
131
|
+
# the response with the origin using a conditional GET request.
|
132
|
+
def validateable?
|
133
|
+
header?('Last-Modified') || header?('Etag')
|
134
|
+
end
|
135
|
+
|
136
|
+
# Indicates that the response should not be served from cache without first
|
137
|
+
# revalidating with the origin. Note that this does not necessary imply that
|
138
|
+
# a caching agent ought not store the response in its cache.
|
139
|
+
def no_cache?
|
140
|
+
!cache_control['no-cache'].nil?
|
141
|
+
end
|
142
|
+
|
143
|
+
# Indicates that the response should not be stored under any circumstances.
|
144
|
+
def no_store?
|
145
|
+
cache_control['no-store']
|
146
|
+
end
|
147
|
+
|
148
|
+
# The date, as specified by the Date header. When no Date header is present,
|
149
|
+
# set the Date header to Time.now and return.
|
150
|
+
def date
|
151
|
+
if date = headers['Date']
|
152
|
+
Time.httpdate(date)
|
153
|
+
else
|
154
|
+
headers['Date'] = now.httpdate unless headers.frozen?
|
155
|
+
now
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# The age of the response.
|
160
|
+
def age
|
161
|
+
[(now - date).to_i, 0].max
|
162
|
+
end
|
163
|
+
|
164
|
+
# The number of seconds after the time specified in the response's Date
|
165
|
+
# header when the the response should no longer be considered fresh. First
|
166
|
+
# check for a Cache-Control max-age value, and fall back on an expires
|
167
|
+
# header; return nil when no maximum age can be established.
|
168
|
+
def max_age
|
169
|
+
if age = cache_control['max-age']
|
170
|
+
age.to_i
|
171
|
+
elsif headers['Expires']
|
172
|
+
Time.httpdate(headers['Expires']) - date
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# Sets the number of seconds after which the response should no longer
|
177
|
+
# be considered fresh. This sets the Cache-Control max-age value.
|
178
|
+
def max_age=(value)
|
179
|
+
self.cache_control = cache_control.merge('max-age' => value.to_s)
|
180
|
+
end
|
181
|
+
|
182
|
+
# The Time when the response should be considered stale. With a
|
183
|
+
# Cache-Control/max-age value is present, this is calculated by adding the
|
184
|
+
# number of seconds specified to the responses #date value. Falls back to
|
185
|
+
# the time specified in the Expires header or returns nil if neither is
|
186
|
+
# present.
|
187
|
+
def expires_at
|
188
|
+
if max_age = cache_control['max-age']
|
189
|
+
date + max_age.to_i
|
190
|
+
elsif time = headers['Expires']
|
191
|
+
Time.httpdate(time)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
# The response's time-to-live in seconds, or nil when no freshness
|
196
|
+
# information is present in the response. When the responses #ttl
|
197
|
+
# is <= 0, the response may not be served from cache without first
|
198
|
+
# revalidating with the origin.
|
199
|
+
def ttl
|
200
|
+
max_age - age if max_age
|
201
|
+
end
|
202
|
+
|
203
|
+
# Set the response's time-to-live to the specified number of seconds. This
|
204
|
+
# adjusts the Cache-Control/max-age value.
|
205
|
+
def ttl=(seconds)
|
206
|
+
self.max_age = age + seconds
|
207
|
+
end
|
208
|
+
|
209
|
+
# The String value of the Last-Modified header exactly as it appears
|
210
|
+
# in the response (i.e., no date parsing / conversion is performed).
|
211
|
+
def last_modified
|
212
|
+
headers['Last-Modified']
|
213
|
+
end
|
214
|
+
|
215
|
+
# Determine if the response was last modified at the time provided.
|
216
|
+
# time_value is the exact string provided in an origin response's
|
217
|
+
# Last-Modified header.
|
218
|
+
def last_modified_at?(time_value)
|
219
|
+
time_value && last_modified == time_value
|
220
|
+
end
|
221
|
+
|
222
|
+
# Determine if response's ETag matches the etag value provided. Return
|
223
|
+
# false when either value is nil.
|
224
|
+
def etag_matches?(etag)
|
225
|
+
etag && self.etag == etag
|
226
|
+
end
|
227
|
+
|
228
|
+
# Headers that MUST NOT be included with 304 Not Modified responses.
|
229
|
+
#
|
230
|
+
# http://tools.ietf.org/html/rfc2616#section-10.3.5
|
231
|
+
NOT_MODIFIED_OMIT_HEADERS = %w[
|
232
|
+
Allow
|
233
|
+
Content-Encoding
|
234
|
+
Content-Language
|
235
|
+
Content-Length
|
236
|
+
Content-Md5
|
237
|
+
Content-Type
|
238
|
+
Last-Modified
|
239
|
+
].to_set
|
240
|
+
|
241
|
+
# Modify the response so that it conforms to the rules defined for
|
242
|
+
# '304 Not Modified'. This sets the status, removes the body, and
|
243
|
+
# discards any headers that MUST NOT be included in 304 responses.
|
244
|
+
#
|
245
|
+
# http://tools.ietf.org/html/rfc2616#section-10.3.5
|
246
|
+
def not_modified!
|
247
|
+
self.status = 304
|
248
|
+
self.body = []
|
249
|
+
NOT_MODIFIED_OMIT_HEADERS.each { |name| headers.delete(name) }
|
250
|
+
nil
|
251
|
+
end
|
252
|
+
|
253
|
+
# The literal value of the Vary header, or nil when no Vary header is
|
254
|
+
# present.
|
255
|
+
def vary
|
256
|
+
headers['Vary']
|
257
|
+
end
|
258
|
+
|
259
|
+
# Does the response include a Vary header?
|
260
|
+
def vary?
|
261
|
+
! vary.nil?
|
262
|
+
end
|
263
|
+
|
264
|
+
# An array of header names given in the Vary header or an empty
|
265
|
+
# array when no Vary header is present.
|
266
|
+
def vary_header_names
|
267
|
+
return [] unless vary = headers['Vary']
|
268
|
+
vary.split(/[\s,]+/)
|
269
|
+
end
|
270
|
+
|
271
|
+
private
|
272
|
+
def now
|
273
|
+
@now ||= Time.now
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
end
|
@@ -0,0 +1,292 @@
|
|
1
|
+
require 'rack'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'digest/sha1'
|
4
|
+
|
5
|
+
module Rack::Cache
|
6
|
+
|
7
|
+
# The MetaStore is responsible for storing meta information about a
|
8
|
+
# request/response pair keyed by the request's URL.
|
9
|
+
#
|
10
|
+
# The meta store keeps a list of request/response pairs for each canonical
|
11
|
+
# request URL. A request/response pair is a two element Array of the form:
|
12
|
+
# [request, response]
|
13
|
+
#
|
14
|
+
# The +request+ element is a Hash of Rack environment keys. Only protocol
|
15
|
+
# keys (i.e., those that start with "HTTP_") are stored. The +response+
|
16
|
+
# element is a Hash of cached HTTP response headers for the paired request.
|
17
|
+
#
|
18
|
+
# The MetaStore class is abstract and should not be instanstiated
|
19
|
+
# directly. Concrete subclasses should implement the protected #read,
|
20
|
+
# #write, and #purge methods. Care has been taken to keep these low-level
|
21
|
+
# methods dumb and straight-forward to implement.
|
22
|
+
class MetaStore
|
23
|
+
|
24
|
+
# Locate a cached response for the request provided. Returns a
|
25
|
+
# Rack::Cache::Response object if the cache hits or nil if no cache entry
|
26
|
+
# was found.
|
27
|
+
def lookup(request, entity_store)
|
28
|
+
entries = read(request.fullpath)
|
29
|
+
|
30
|
+
# bail out if we have nothing cached
|
31
|
+
return nil if entries.empty?
|
32
|
+
|
33
|
+
# find a cached entry that matches the request.
|
34
|
+
env = request.env
|
35
|
+
match = entries.detect{ |req,res| requests_match?(res['Vary'], env, req)}
|
36
|
+
if match
|
37
|
+
# TODO what if body doesn't exist in entity store?
|
38
|
+
# reconstruct response object
|
39
|
+
req, res = match
|
40
|
+
status = res['X-Status']
|
41
|
+
body = entity_store.open(res['X-Content-Digest'])
|
42
|
+
response = Rack::Cache::Response.new(status.to_i, res, body)
|
43
|
+
response.activate!
|
44
|
+
|
45
|
+
# Return the cached response
|
46
|
+
response
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Write a cache entry to the store under the given key. Existing
|
51
|
+
# entries are read and any that match the response are removed.
|
52
|
+
# This method calls #write with the new list of cache entries.
|
53
|
+
#--
|
54
|
+
# TODO canonicalize URL key
|
55
|
+
def store(request, response, entity_store)
|
56
|
+
key = request.fullpath
|
57
|
+
stored_env = persist_request(request)
|
58
|
+
|
59
|
+
# write the response body to the entity store if this is the
|
60
|
+
# original response.
|
61
|
+
response['X-Status'] = response.status.to_s
|
62
|
+
if response['X-Content-Digest'].nil?
|
63
|
+
digest, size = entity_store.write(response.body)
|
64
|
+
response['X-Content-Digest'] = digest
|
65
|
+
response['Content-Length'] = size.to_s unless response['Transfer-Encoding']
|
66
|
+
response.body = entity_store.open(digest)
|
67
|
+
response.activate!
|
68
|
+
end
|
69
|
+
|
70
|
+
# read existing cache entries, remove non-varying, and add this one to
|
71
|
+
# the list
|
72
|
+
vary = response.vary
|
73
|
+
entries =
|
74
|
+
read(key).reject do |env,res|
|
75
|
+
(vary == res['Vary']) &&
|
76
|
+
requests_match?(vary, env, stored_env)
|
77
|
+
end
|
78
|
+
entries.unshift [stored_env, response.headers.dup]
|
79
|
+
write key, entries
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
# Extract the environment Hash from +request+ while making any
|
84
|
+
# necessary modifications in preparation for persistence. The Hash
|
85
|
+
# returned must be marshalable.
|
86
|
+
def persist_request(request)
|
87
|
+
env = request.env.dup
|
88
|
+
env.reject! { |key,val| key =~ /[^0-9A-Z_]/ }
|
89
|
+
env
|
90
|
+
end
|
91
|
+
|
92
|
+
# Determine whether the two environment hashes are non-varying based on
|
93
|
+
# the vary response header value provided.
|
94
|
+
def requests_match?(vary, env1, env2)
|
95
|
+
return true if vary.nil? || vary == ''
|
96
|
+
vary.split(/[\s,]+/).all? do |header|
|
97
|
+
key = "HTTP_#{header.upcase.tr('-', '_')}"
|
98
|
+
env1[key] == env2[key]
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
protected
|
103
|
+
# Locate all cached request/response pairs that match the specified
|
104
|
+
# URL key. The result must be an Array of all cached request/response
|
105
|
+
# pairs. An empty Array must be returned if nothing is cached for
|
106
|
+
# the specified key.
|
107
|
+
def read(key)
|
108
|
+
raise NotImplemented
|
109
|
+
end
|
110
|
+
|
111
|
+
# Store an Array of request/response pairs for the given key. Concrete
|
112
|
+
# implementations should not attempt to filter or concatenate the
|
113
|
+
# list in any way.
|
114
|
+
def write(key, negotiations)
|
115
|
+
raise NotImplemented
|
116
|
+
end
|
117
|
+
|
118
|
+
# Remove all cached entries at the key specified. No error is raised
|
119
|
+
# when the key does not exist.
|
120
|
+
def purge(key)
|
121
|
+
raise NotImplemented
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
# Generate a SHA1 hex digest for the specified string. This is a
|
127
|
+
# simple utility method for meta store implementations.
|
128
|
+
def hexdigest(data)
|
129
|
+
Digest::SHA1.hexdigest(data)
|
130
|
+
end
|
131
|
+
|
132
|
+
public
|
133
|
+
|
134
|
+
# Concrete MetaStore implementation that uses a simple Hash to store
|
135
|
+
# request/response pairs on the heap.
|
136
|
+
class Heap < MetaStore
|
137
|
+
def initialize(hash={})
|
138
|
+
@hash = hash
|
139
|
+
end
|
140
|
+
|
141
|
+
def read(key)
|
142
|
+
@hash.fetch(key, []).collect do |req,res|
|
143
|
+
[req.dup, res.dup]
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def write(key, entries)
|
148
|
+
@hash[key] = entries
|
149
|
+
end
|
150
|
+
|
151
|
+
def purge(key)
|
152
|
+
@hash.delete(key)
|
153
|
+
nil
|
154
|
+
end
|
155
|
+
|
156
|
+
def to_hash
|
157
|
+
@hash
|
158
|
+
end
|
159
|
+
|
160
|
+
def self.resolve(uri)
|
161
|
+
new
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
HEAP = Heap
|
166
|
+
MEM = HEAP
|
167
|
+
|
168
|
+
# Concrete MetaStore implementation that stores request/response
|
169
|
+
# pairs on disk.
|
170
|
+
class Disk < MetaStore
|
171
|
+
attr_reader :root
|
172
|
+
|
173
|
+
def initialize(root="/tmp/rack-cache/meta-#{ARGV[0]}")
|
174
|
+
@root = File.expand_path(root)
|
175
|
+
FileUtils.mkdir_p(root, :mode => 0755)
|
176
|
+
end
|
177
|
+
|
178
|
+
def read(key)
|
179
|
+
path = key_path(key)
|
180
|
+
File.open(path, 'rb') { |io| Marshal.load(io) }
|
181
|
+
rescue Errno::ENOENT
|
182
|
+
[]
|
183
|
+
end
|
184
|
+
|
185
|
+
def write(key, entries)
|
186
|
+
path = key_path(key)
|
187
|
+
File.open(path, 'wb') { |io| Marshal.dump(entries, io, -1) }
|
188
|
+
rescue Errno::ENOENT
|
189
|
+
Dir.mkdir(File.dirname(path), 0755)
|
190
|
+
retry
|
191
|
+
end
|
192
|
+
|
193
|
+
def purge(key)
|
194
|
+
path = key_path(key)
|
195
|
+
File.unlink(path)
|
196
|
+
nil
|
197
|
+
rescue Errno::ENOENT
|
198
|
+
nil
|
199
|
+
end
|
200
|
+
|
201
|
+
private
|
202
|
+
def key_path(key)
|
203
|
+
File.join(root, spread(hexdigest(key)))
|
204
|
+
end
|
205
|
+
|
206
|
+
def spread(sha, n=2)
|
207
|
+
sha = sha.dup
|
208
|
+
sha[n,0] = '/'
|
209
|
+
sha
|
210
|
+
end
|
211
|
+
|
212
|
+
public
|
213
|
+
def self.resolve(uri)
|
214
|
+
path = File.expand_path(uri.opaque || uri.path)
|
215
|
+
new path
|
216
|
+
end
|
217
|
+
|
218
|
+
end
|
219
|
+
|
220
|
+
DISK = Disk
|
221
|
+
FILE = Disk
|
222
|
+
|
223
|
+
# Stores request/response pairs in memcached. Keys are not stored
|
224
|
+
# directly since memcached has a 250-byte limit on key names. Instead,
|
225
|
+
# the SHA1 hexdigest of the key is used.
|
226
|
+
class MemCache < MetaStore
|
227
|
+
|
228
|
+
# The Memcached instance used to communicated with the memcached
|
229
|
+
# daemon.
|
230
|
+
attr_reader :cache
|
231
|
+
|
232
|
+
def initialize(server="localhost:11211", options={})
|
233
|
+
@cache =
|
234
|
+
if server.respond_to?(:stats)
|
235
|
+
server
|
236
|
+
else
|
237
|
+
require 'memcached'
|
238
|
+
Memcached.new(server, options)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def read(key)
|
243
|
+
key = hexdigest(key)
|
244
|
+
cache.get(key)
|
245
|
+
rescue Memcached::NotFound
|
246
|
+
[]
|
247
|
+
end
|
248
|
+
|
249
|
+
def write(key, entries)
|
250
|
+
key = hexdigest(key)
|
251
|
+
cache.set(key, entries)
|
252
|
+
end
|
253
|
+
|
254
|
+
def purge(key)
|
255
|
+
key = hexdigest(key)
|
256
|
+
cache.delete(key)
|
257
|
+
nil
|
258
|
+
rescue Memcached::NotFound
|
259
|
+
nil
|
260
|
+
end
|
261
|
+
|
262
|
+
extend Rack::Utils
|
263
|
+
|
264
|
+
# Create MemCache store for the given URI. The URI must specify
|
265
|
+
# a host and may specify a port, namespace, and options:
|
266
|
+
#
|
267
|
+
# memcached://example.com:11211/namespace?opt1=val1&opt2=val2
|
268
|
+
#
|
269
|
+
# Query parameter names and values are documented with the memcached
|
270
|
+
# library: http://tinyurl.com/4upqnd
|
271
|
+
def self.resolve(uri)
|
272
|
+
server = "#{uri.host}:#{uri.port || '11211'}"
|
273
|
+
options = parse_query(uri.query)
|
274
|
+
options.keys.each do |key|
|
275
|
+
value =
|
276
|
+
case value = options.delete(key)
|
277
|
+
when 'true' ; true
|
278
|
+
when 'false' ; false
|
279
|
+
else value.to_sym
|
280
|
+
end
|
281
|
+
options[k.to_sym] = value
|
282
|
+
end
|
283
|
+
options[:namespace] = uri.path.sub(/^\//, '')
|
284
|
+
new server, options
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
MEMCACHE = MemCache
|
289
|
+
MEMCACHED = MemCache
|
290
|
+
end
|
291
|
+
|
292
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'rack'
|
2
|
+
require 'rack/cache/storage'
|
3
|
+
|
4
|
+
module Rack::Cache
|
5
|
+
# Configuration options and utility methods for option access. Rack::Cache
|
6
|
+
# uses the Rack Environment to store option values. All options documented
|
7
|
+
# below are stored in the Rack Environment as "rack-cache.<option>", where
|
8
|
+
# <option> is the option name.
|
9
|
+
#
|
10
|
+
# The #set method can be used within an event or a top-level configuration
|
11
|
+
# block to configure a option values. When #set is called at the top-level,
|
12
|
+
# the value applies to all requests; when called from within an event, the
|
13
|
+
# values applies only to the request being processed.
|
14
|
+
|
15
|
+
module Options
|
16
|
+
class << self
|
17
|
+
private
|
18
|
+
def option_accessor(key)
|
19
|
+
define_method(key) { || read_option(key) }
|
20
|
+
define_method("#{key}=") { |value| write_option(key, value) }
|
21
|
+
define_method("#{key}?") { || !! read_option(key) }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Enable verbose trace logging. This option is currently enabled by
|
26
|
+
# default but is likely to be disabled in a future release.
|
27
|
+
option_accessor :verbose
|
28
|
+
|
29
|
+
# The storage resolver. Defaults to the Rack::Cache.storage singleton instance
|
30
|
+
# of Rack::Cache::Storage. This object is responsible for resolving metastore
|
31
|
+
# and entitystore URIs to an implementation instances.
|
32
|
+
option_accessor :storage
|
33
|
+
|
34
|
+
# A URI specifying the meta-store implementation that should be used to store
|
35
|
+
# request/response meta information. The following URIs schemes are
|
36
|
+
# supported:
|
37
|
+
#
|
38
|
+
# * heap:/
|
39
|
+
# * file:/absolute/path or file:relative/path
|
40
|
+
# * memcached://localhost:11211[/namespace]
|
41
|
+
#
|
42
|
+
# If no meta store is specified the 'heap:/' store is assumed. This
|
43
|
+
# implementation has significant draw-backs so explicit configuration is
|
44
|
+
# recommended.
|
45
|
+
option_accessor :metastore
|
46
|
+
|
47
|
+
# A URI specifying the entity-store implement that should be used to store
|
48
|
+
# response bodies. See the metastore option for information on supported URI
|
49
|
+
# schemes.
|
50
|
+
#
|
51
|
+
# If no entity store is specified the 'heap:/' store is assumed. This
|
52
|
+
# implementation has significant draw-backs so explicit configuration is
|
53
|
+
# recommended.
|
54
|
+
option_accessor :entitystore
|
55
|
+
|
56
|
+
# The number of seconds that a cache entry should be considered
|
57
|
+
# "fresh" when no explicit freshness information is provided in
|
58
|
+
# a response. Explicit Cache-Control or Expires headers
|
59
|
+
# override this value.
|
60
|
+
#
|
61
|
+
# Default: 0
|
62
|
+
option_accessor :default_ttl
|
63
|
+
|
64
|
+
# The underlying options Hash. During initialization (or outside of a
|
65
|
+
# request), this is a default values Hash. During a request, this is the
|
66
|
+
# Rack environment Hash. The default values Hash is merged in underneath
|
67
|
+
# the Rack environment before each request is processed.
|
68
|
+
def options
|
69
|
+
@env || @default_options
|
70
|
+
end
|
71
|
+
|
72
|
+
# Set multiple options.
|
73
|
+
def options=(hash={})
|
74
|
+
hash.each { |key,value| write_option(key, value) }
|
75
|
+
end
|
76
|
+
|
77
|
+
# Set an option. When +option+ is a Symbol, it is set in the Rack
|
78
|
+
# Environment as "rack-cache.option". When +option+ is a String, it
|
79
|
+
# exactly as specified. The +option+ argument may also be a Hash in
|
80
|
+
# which case each key/value pair is merged into the environment as if
|
81
|
+
# the #set method were called on each.
|
82
|
+
def set(option, value=self)
|
83
|
+
if value == self
|
84
|
+
self.options = option.to_hash
|
85
|
+
else
|
86
|
+
write_option option, value
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
def read_option(key)
|
92
|
+
options[option_name(key)]
|
93
|
+
end
|
94
|
+
|
95
|
+
def write_option(key, value)
|
96
|
+
options[option_name(key)] = value
|
97
|
+
end
|
98
|
+
|
99
|
+
def option_name(key)
|
100
|
+
case key
|
101
|
+
when Symbol ; "rack-cache.#{key}"
|
102
|
+
when String ; key
|
103
|
+
else raise ArgumentError
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
def initialize_options(options={})
|
109
|
+
@default_options = {
|
110
|
+
'rack-cache.verbose' => true,
|
111
|
+
'rack-cache.storage' => Rack::Cache::Storage.instance,
|
112
|
+
'rack-cache.metastore' => 'heap:/',
|
113
|
+
'rack-cache.entitystore' => 'heap:/',
|
114
|
+
'rack-cache.default_ttl' => 0
|
115
|
+
}
|
116
|
+
self.options = options
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|