1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236
|
require 'logger'
require 'sprockets/digest_utils'
module Sprockets
# Public: Wrapper interface to backend cache stores. Ensures a consistent API
# even when the backend uses get/set or read/write.
#
# Public cache interface
#
# Always assign the backend store instance to Environment#cache=.
#
# environment.cache = Sprockets::Cache::MemoryStore.new(1000)
#
# Environment#cache will always return a wrapped Cache interface. See the
# methods marked public on this class.
#
#
# Backend cache interface
#
# The Backend cache store must implement two methods.
#
# get(key)
#
# key - An opaque String with a length less than 250 characters.
#
# Returns an JSON serializable object.
#
# set(key, value)
#
# Will only be called once per key. Setting a key "foo" with value "bar",
# then later key "foo" with value "baz" is an undefined behavior.
#
# key - An opaque String with a length less than 250 characters.
# value - A JSON serializable object.
#
# Returns argument value.
#
class Cache
# Builtin cache stores.
autoload :FileStore, 'sprockets/cache/file_store'
autoload :MemoryStore, 'sprockets/cache/memory_store'
autoload :NullStore, 'sprockets/cache/null_store'
# Internal: Cache key version for this class. Rarely should have to change
# unless the cache format radically changes. Will be bump on major version
# releases though.
VERSION = '3.0'
def self.default_logger
logger = Logger.new($stderr)
logger.level = Logger::FATAL
logger
end
# Internal: Wrap a backend cache store.
#
# Always assign a backend cache store instance to Environment#cache= and
# use Environment#cache to retreive a wrapped interface.
#
# cache - A compatible backend cache store instance.
def initialize(cache = nil, logger = self.class.default_logger)
@cache_wrapper = get_cache_wrapper(cache)
@fetch_cache = Cache::MemoryStore.new(1024)
@logger = logger
end
# Public: Prefer API to retrieve and set values in the cache store.
#
# key - JSON serializable key
# block -
# Must return a consistent JSON serializable object for the given key.
#
# Examples
#
# cache.fetch("foo") { "bar" }
#
# Returns a JSON serializable object.
def fetch(key)
start = Time.now.to_f
expanded_key = expand_key(key)
value = @fetch_cache.get(expanded_key)
if value.nil?
value = @cache_wrapper.get(expanded_key)
if value.nil?
value = yield
@cache_wrapper.set(expanded_key, value)
@logger.debug do
ms = "(#{((Time.now.to_f - start) * 1000).to_i}ms)"
"Sprockets Cache miss #{peek_key(key)} #{ms}"
end
end
@fetch_cache.set(expanded_key, value)
end
value
end
# Public: Low level API to retrieve item directly from the backend cache
# store.
#
# This API may be used publicly, but may have undefined behavior
# depending on the backend store being used. Prefer the
# Cache#fetch API over using this.
#
# key - JSON serializable key
# local - Check local cache first (default: false)
#
# Returns a JSON serializable object or nil if there was a cache miss.
def get(key, local = false)
expanded_key = expand_key(key)
if local && value = @fetch_cache.get(expanded_key)
return value
end
value = @cache_wrapper.get(expanded_key)
@fetch_cache.set(expanded_key, value) if local
value
end
# Public: Low level API to set item directly to the backend cache store.
#
# This API may be used publicly, but may have undefined behavior
# depending on the backend store being used. Prefer the
# Cache#fetch API over using this.
#
# key - JSON serializable key
# value - A consistent JSON serializable object for the given key. Setting
# a different value for the given key has undefined behavior.
# local - Set on local cache (default: false)
#
# Returns the value argument.
def set(key, value, local = false)
expanded_key = expand_key(key)
@fetch_cache.set(expanded_key, value) if local
@cache_wrapper.set(expanded_key, value)
end
# Public: Pretty inspect
#
# Returns String.
def inspect
"#<#{self.class} local=#{@fetch_cache.inspect} store=#{@cache_wrapper.cache.inspect}>"
end
private
# Internal: Expand object cache key into a short String key.
#
# The String should be under 250 characters so its compatible with
# Memcache.
#
# key - JSON serializable key
#
# Returns a String with a length less than 250 characters.
def expand_key(key)
digest_key = DigestUtils.pack_urlsafe_base64digest(DigestUtils.digest(key))
namespace = digest_key[0, 2]
"sprockets/v#{VERSION}/#{namespace}/#{digest_key}"
end
PEEK_SIZE = 100
# Internal: Show first 100 characters of cache key for logging purposes.
#
# Returns a String with a length less than 100 characters.
def peek_key(key)
case key
when Integer
key.to_s
when String
key[0, PEEK_SIZE].inspect
when Array
str = []
key.each { |k| str << peek_key(k) }
str.join(':')[0, PEEK_SIZE]
else
peek_key(DigestUtils.pack_urlsafe_base64digest(DigestUtils.digest(key)))
end
end
def get_cache_wrapper(cache)
if cache.is_a?(Cache)
cache
# `Cache#get(key)` for Memcache
elsif cache.respond_to?(:get)
GetWrapper.new(cache)
# `Cache#[key]` so `Hash` can be used
elsif cache.respond_to?(:[])
HashWrapper.new(cache)
# `Cache#read(key)` for `ActiveSupport::Cache` support
elsif cache.respond_to?(:read)
ReadWriteWrapper.new(cache)
else
cache = Sprockets::Cache::NullStore.new
GetWrapper.new(cache)
end
end
class Wrapper < Struct.new(:cache)
end
class GetWrapper < Wrapper
def get(key)
cache.get(key)
end
def set(key, value)
cache.set(key, value)
end
end
class HashWrapper < Wrapper
def get(key)
cache[key]
end
def set(key, value)
cache[key] = value
end
end
class ReadWriteWrapper < Wrapper
def get(key)
cache.read(key)
end
def set(key, value)
cache.write(key, value)
end
end
end
end
|