1 -- Copyright (C) 2013 Matthieu Tourne
2 -- @author Matthieu Tourne <matthieu@cloudflare.com>
4 -- small overlay over shdict, smart cache load mechanism
8 local resty_lock = require("resty.lock")
13 local DEFAULT_POSITIVE_TTL = 10 -- cache for, successful lookup
14 local DEFAULT_NEGATIVE_TTL = 2 -- cache for, failed lookup
15 local DEFAULT_ACTUALIZE_TTL = 2 -- stale data, actualize data for
17 -- default lock options, in secs
18 local function _get_default_lock_options()
20 exptime = 1, -- max wait if failing to call unlock()
21 timeout = 0.5, -- max waiting time of lock()
22 max_step = 0.1, -- max sleeping interval
26 local function prequire(m)
27 local ok, err_or_module = pcall(require, m)
29 return nil, err_or_module
34 local conf = prequire("conf")
36 DEFAULT_NEGATIVE_TTL = conf.DEFAULT_NEGATIVE_TTL or DEFAULT_NEGATIVE_TTL
37 DEFAULT_ACTUALIZE_TTL = conf.DEFAULT_ACTUALIZE_TTL or DEFAULT_ACTUALIZE_TTL
42 local st_format = string.format
44 -- there are only really 5 states total
45 -- is_stale is_neg is_from_cache
46 local MISS_STATE = 0 -- 0 0 0
47 local HIT_POSITIVE_STATE = 1 -- 0 0 1
48 local HIT_NEGATIVE_STATE = 3 -- 0 1 1
49 local STALE_POSITIVE_STATE = 5 -- 1 0 1
51 -- stale negative doesn't really make sense, use HIT_NEGATIVE instead
52 -- local STALE_NEGATIVE_STATE = 7 -- 1 1 1
55 local NEGATIVE_FLAG = 2
59 [MISS_STATE] = 'MISS',
60 [HIT_POSITIVE_STATE] = 'HIT',
61 [HIT_NEGATIVE_STATE] = 'HIT_NEGATIVE',
62 [STALE_POSITIVE_STATE] = 'STALE',
63 -- [STALE_NEGATIVE_STATE] = 'STALE_NEGATIVE',
66 local function get_status(flags)
67 return STATES[flags] or st_format('UNDEF (0x%x)', flags)
70 local EMPTY_DATA = '_EMPTY_'
72 -- install debug functions
74 local resty_lock_lock = resty_lock.lock
76 resty_lock.lock = function (...)
77 local _, key = unpack({...})
78 print("lock key: ", tostring(key))
79 return resty_lock_lock(...)
82 local resty_lock_unlock = resty_lock.unlock
84 resty_lock.unlock = function (...)
86 return resty_lock_unlock(...)
91 -- store the object in the context
92 -- useful for debugging and tracking cache status
93 local function _store_object(self, name)
95 print('storing shcache: ', name, ' into ngx.ctx')
98 local ngx_ctx = ngx.ctx
100 if not ngx_ctx.shcache then
103 ngx_ctx.shcache[name] = self
110 -- default function for callbacks.encode / decode.
111 local function _identity(data)
115 -- shdict: ngx.shared.DICT, created by the lua_shared_dict directive
116 -- callbacks: see shcache state machine for user defined functions
117 -- * callbacks.external_lookup is required
118 -- * callbacks.encode : optional encoding before saving to shmem
119 -- * callbacks.decode : optional decoding when retreiving from shmem
121 -- * opts.positive_ttl : save a valid external loookup for, in seconds
122 -- * opts.positive_ttl : save a invalid loookup for, in seconds
123 -- * opts.actualize_ttl : re-actualize a stale record for, in seconds
124 -- * opts.lock_options : set option to lock see : http://github.com/agentzh/lua-resty-lock
126 -- * opts.locks_shdict : specificy the name of the shdict containing the locks
127 -- (useful if you might have locks key collisions)
128 -- uses "locks" by default.
129 -- * opts.name : if shcache object is named, it will automatically
130 -- register itself in ngx.ctx.shcache (useful for logging).
131 local function new(self, shdict, callbacks, opts)
133 return nil, "shdict does not exist"
136 -- check that callbacks.external_lookup is set
137 if not callbacks or not callbacks.external_lookup then
138 return nil, "no external_lookup function defined"
141 if not callbacks.encode then
142 callbacks.encode = _identity
145 if not callbacks.decode then
146 callbacks.decode = _identity
149 local opts = opts or {}
151 -- merge default lock options with the ones passed to new()
152 local lock_options = _get_default_lock_options()
153 if opts.lock_options then
154 for k, v in pairs(opts.lock_options) do
159 local name = opts.name
163 callbacks = callbacks,
165 positive_ttl = opts.positive_ttl or DEFAULT_POSITIVE_TTL,
166 negative_ttl = opts.negative_ttl or DEFAULT_NEGATIVE_TTL,
168 -- ttl to actualize stale data to
169 actualize_ttl = opts.actualize_ttl or DEFAULT_ACTUALIZE_TTL,
171 lock_options = lock_options,
173 locks_shdict = opts.lock_shdict or "locks",
178 cache_status = 'UNDEF',
179 cache_state = MISS_STATE,
180 lock_status = 'NO_LOCK',
182 -- shdict:set() pushed out another value
183 forcible_set = false,
185 -- cache hit on second attempt (post lock)
191 local locks = ngx.shared[obj.locks_shdict]
193 -- check for existence, locks is not directly used
195 ngx.log(ngx.CRIT, 'shared mem locks is missing.\n',
196 '## add to you lua conf: lua_shared_dict locks 5M; ##')
200 local self = setmetatable(obj, obj_mt)
202 -- if the shcache object is named
203 -- keep track of the object in the context
204 -- (useful for gathering stats at log phase)
206 _store_object(self, name)
214 local function _get_lock(self)
215 local lock = self.lock
217 lock = resty_lock:new(self.locks_shdict, self.lock_options)
223 -- remove the lock if there is any
224 local function _unlock(self)
225 local lock = self.lock
227 local ok, err = lock:unlock()
229 ngx.log(ngx.ERR, "failed to unlock :" , err)
235 local function _return(self, data, flags)
236 -- make sure we remove the locks if any before returning data
240 local cache_status = get_status(self.cache_state)
242 if cache_status == 'MISS' and not data then
243 cache_status = 'NO_DATA'
246 self.cache_status = cache_status
248 return data, self.from_cache
251 local function _set(self, ...)
253 local key, data, ttl, flags = unpack({...})
254 print("saving key: ", key, ", for: ", ttl)
257 local ok, err, forcible = self.shdict:set(...)
259 self.forcible_set = forcible
262 local key, data, ttl, flags = unpack({...})
263 ngx.log(ngx.ERR, 'failed to set key: ', key, ', err: ', err)
269 -- check if the data returned by :get() is considered empty
270 local function _is_empty(data, flags)
271 return flags and band(flags, NEGATIVE_FLAG) and data == EMPTY_DATA
274 -- save positive, encode the data if needed before :set()
275 local function _save_positive(self, key, data)
277 print("key: ", key, ". save positive, ttl: ", self.positive_ttl)
279 data = self.callbacks.encode(data)
280 return _set(self, key, data, self.positive_ttl, HIT_POSITIVE_STATE)
283 -- save negative, no encoding required (no data actually saved)
284 local function _save_negative(self, key)
286 print("key: ", key, ". save negative, ttl: ", self.negative_ttl)
288 return _set(self, key, EMPTY_DATA, self.negative_ttl, HIT_NEGATIVE_STATE)
291 -- save actualize, will boost a stale record to a live one
292 local function _save_actualize(self, key, data, flags)
293 local new_flags = bor(flags, STALE_FLAG)
296 print("key: ", key, ". save actualize, ttl: ", self.actualize_ttl,
297 ". new state: ", get_status(new_flags))
300 _set(self, key, data, self.actualize_ttl, new_flags)
304 local function _process_cached_data(self, data, flags)
306 print("data: ", data, st_format(", flags: %x", flags))
309 self.cache_state = flags
310 self.from_cache = true
312 if _is_empty(data, flags) then
316 return self.callbacks.decode(data)
320 -- wrapper to get data from the shdict
321 local function _get(self, key)
322 -- always call get_stale() as it does not free element
323 -- like get does on each call
324 local data, flags, stale = self.shdict:get_stale(key)
326 if data and stale then
328 print("found stale data for key : ", key)
331 self.stale_data = { data, flags }
339 local function _get_stale(self)
340 local stale_data = self.stale_data
342 return unpack(stale_data)
348 local function load(self, key)
349 -- start: check for existing cache
350 local data, flags = _get(self, key)
352 -- hit: process_cache_hit
354 data = _process_cached_data(self, data, flags)
355 return _return(self, data)
360 -- lock: set a lock before performing external lookup
361 local lock = _get_lock(self)
362 local elapsed, err = lock:lock(key)
365 -- failed to acquire lock, still proceed normally to external_lookup
366 -- unlock() might fail.
367 ngx.log(ngx.ERR, "failed to acquire the lock: ", err)
368 self.lock_status = 'ERROR'
369 -- _unlock won't try to unlock() without a valid lock
372 -- lock acquired successfuly
376 -- elapsed > 0 => waited lock (other thread might have :set() the data)
377 -- (more likely to get a HIT on cache_load 2)
378 self.lock_status = 'WAITED'
382 -- elapsed == 0 => immediate lock
383 -- it is less likely to get a HIT on cache_load 2
384 -- but still perform it (race condition cases)
385 self.lock_status = 'IMMEDIATE'
388 -- perform cache_load 2
389 data, flags = _get(self, key)
391 -- hit2 : process cache hit
395 -- unlock before de-serializing cached data
397 data = _process_cached_data(self, data, flags)
398 return _return(self, data)
401 -- continue to external lookup
404 -- perform external lookup
405 data, err = self.callbacks.external_lookup()
408 -- succ: save positive and return the data
410 _save_positive(self, key, data)
411 return _return(self, data)
413 ngx.log(ngx.WARN, 'external lookup failed: ', err)
416 -- external lookup failed
417 -- attempt to load stale data
418 data, flags = _get_stale(self)
419 if data and not _is_empty(data, flags) then
420 -- hit_stale + valid (positive) data
422 flags = _save_actualize(self, key, data, flags)
423 -- unlock before de-serializing data
425 data = _process_cached_data(self, data, flags)
426 return _return(self, data)
429 if DEBUG and data then
430 -- there is data, but it failed _is_empty() => stale negative data
431 print('STALE_NEGATIVE data => cache as a new HIT_NEGATIVE')
434 -- nothing has worked, save negative and return empty
435 _save_negative(self, key)
436 return _return(self, nil)