├── .gitignore ├── shcache.gif ├── .github └── workflows │ └── semgrep.yml ├── shcache.dot ├── README.md └── shcache.lua /.gitignore: -------------------------------------------------------------------------------- 1 | *.swo 2 | *.swp 3 | *~ 4 | -------------------------------------------------------------------------------- /shcache.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/lua-resty-shcache/HEAD/shcache.gif -------------------------------------------------------------------------------- /.github/workflows/semgrep.yml: -------------------------------------------------------------------------------- 1 | 2 | on: 3 | pull_request: {} 4 | workflow_dispatch: {} 5 | push: 6 | branches: 7 | - main 8 | - master 9 | name: Semgrep config 10 | jobs: 11 | semgrep: 12 | name: semgrep/ci 13 | runs-on: ubuntu-20.04 14 | env: 15 | SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} 16 | SEMGREP_URL: https://cloudflare.semgrep.dev 17 | SEMGREP_APP_URL: https://cloudflare.semgrep.dev 18 | SEMGREP_VERSION_CHECK_URL: https://cloudflare.semgrep.dev/api/check-version 19 | container: 20 | image: returntocorp/semgrep 21 | steps: 22 | - uses: actions/checkout@v3 23 | - run: semgrep ci 24 | -------------------------------------------------------------------------------- /shcache.dot: -------------------------------------------------------------------------------- 1 | digraph G { 2 | compound=true; 3 | 4 | cache_load [ label = "cache load\n\nshdict:get(key)" ]; 5 | cache_load_stale [ label = "cache load stale\n\ndata, flags = shdict:get_stale(key)" ]; 6 | 7 | cache_load_locked [ label = "cache load 2\n\nshdict:get(key)" ]; 8 | 9 | cache_save_positive [ label = "cache save positive\n\nshdict:set(key, data,\npositive_ttl, positive_flag)" ]; 10 | cache_save_negative [ label = "cache save negative\n\nshdict:set(key, _EMPTY_,\nnegative_ttl, negative_flag)" ]; 11 | 12 | cache_save_actualize [ label = "cache save actualize\n(only positive gets actualized)\n\nshdict:set(key, stale_data,\nactualize_ttl, stale_flag)" ]; 13 | 14 | lock_key [ label = "lock(key)" ]; 15 | 16 | process_hit [ label = "process cache hit" ]; 17 | 18 | { node [ style = filled ]; 19 | start; 20 | ret_data_from_cache [ label = "return data, from_cache = true" ]; 21 | ret_nil_from_cache [ label = "return nil, from_cache = true" ]; 22 | ret_data_not_cache [ label = "return data, from_cache = false" ]; 23 | ret_nil_not_cache [ label = "return nil, from_cache = false" ]; 24 | } 25 | 26 | { node [ shape = box, style = filled, color = lightblue ] 27 | lookup [ label = "external lookup\n(kt / dns / ...)" ]; 28 | encode_data [ label = "serialize data" ]; 29 | decode_data [ label = "de-serialize data" ]; 30 | } 31 | 32 | start -> cache_load; 33 | 34 | cache_load -> lock_key [ label = "miss" ]; 35 | cache_load -> process_hit [ label = "hit", style = bold, color = green ]; 36 | 37 | # fail at locking 38 | lock_key -> error [ label = "fail", style=bold, color=red ]; 39 | error -> lookup [ label = "still lookup, unlock will fail"]; 40 | 41 | # locked (immediate) 42 | lock_key -> lookup [ label = "immediate (elapsed == 0)" ]; 43 | 44 | lookup -> cache_load_stale [ label = "fail", style = bold, color = red ]; 45 | lookup -> encode_data -> cache_save_positive [ label = "succ" ]; 46 | 47 | cache_load_stale -> cache_save_negative [ label = "miss", style = bold, color = red ]; 48 | cache_load_stale -> stale_process [ label = "hit_stale", style = bold, color = orange ]; 49 | 50 | stale_process -> cache_save_actualize [ label = "flags == positive_flag", style = bold, color = orange ] 51 | stale_process -> cache_save_negative [ label = "flags != positive_flag", style = bold, color = red ]; 52 | 53 | cache_save_actualize -> unlock -> process_hit [ label = "hit_stale", style = bold, color = orange ]; 54 | 55 | cache_save_negative -> unlock -> ret_nil_not_cache [ label = "no_data", style = bold, color = red ]; 56 | 57 | cache_save_positive -> unlock -> ret_data_not_cache [ label = "valid data", style = bold, color = blue]; 58 | 59 | # locked (waited) 60 | lock_key -> cache_load_locked [ label = "locked (elapsed > 0)", style = bold, color = blue ]; 61 | 62 | cache_load_locked -> lookup [ label = "miss (weird state)" ]; 63 | cache_load_locked -> unlock [ label = "hit2", style = bold, color = green ]; 64 | unlock -> process_hit [ label = "hit2", style = bold, color = green ]; 65 | 66 | process_hit -> ret_nil_from_cache [ label = "data == _EMPTY_ AND\nflags == negative_flag" ]; 67 | 68 | process_hit -> decode_data -> ret_data_from_cache [ label = "valid data" ]; 69 | 70 | } 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | shcache - simple cache object atop ngx.shared.DICT 2 | ================================================== 3 | 4 | shcache is an attempt at using ngx.shared.DICT with a caching state machine layed on top 5 | 6 | it assumes that you're using some slower external lookup (memc, *sql, redis, etc) that you 7 | want to cache the result of, inside your ngx_lua application code. 8 | 9 | it aims at : 10 | 11 | * being very simple to use from a user perspective 12 | * minimizing the number of external lookup, notably by caching negative lookups and preventing 13 | external lookup stampeded via the usage of locks 14 | * minimizing the amount of serialization / de-serialization to store/load the in cache 15 | * be as fault-tolerant as possible, in case the external lookup fails ( see: cache load stale ) 16 | 17 | 18 | This is based on the lock mechanism devised by Yichun "agentzh" Zhang, and available here: 19 | http://github.com/agentzh/lua-resty-lock 20 | 21 | It assumes that a "locks" shared dict, has been created using the directive 22 | `lua_shared_dict` in your Nginx conf. 23 | 24 | An overview of the state machine is available within this repo, as a graphviz file. 25 | 26 | 27 | Usage 28 | ===== 29 | 30 | local shcache = require("shcache") 31 | local cmsgpack = require("cmsgpack") 32 | 33 | -- use lua-resty-memcached connector to get data 34 | local memc = require("memcached") 35 | 36 | 37 | local function load_from_cache(key) 38 | 39 | -- closure to perform external lookup to memcache 40 | local lookup = function () 41 | local memc, err = memcached:new() 42 | if not memc then 43 | neturn nil, err 44 | end 45 | 46 | local ok, err = memc:connect("127.0.0.1", 11211) 47 | if not ok then 48 | return nil, err 49 | end 50 | 51 | local value, flags, err = memc:get(key) 52 | memc:close() 53 | 54 | if not value then 55 | return nil, err 56 | end 57 | 58 | -- return data in the form of a table 59 | return { value = value, 60 | flags = flags } 61 | end 62 | 63 | local my_cache_table = shcache:new( 64 | ngx.shared.cache_dict 65 | { external_lookup = lookup, 66 | encode = cmsgpack.pack, 67 | decode = cmsgpack.decode, 68 | }, 69 | { positive_ttl = 10, -- cache good data for 10s 70 | negative_ttl = 3, -- cache failed lookup for 3s 71 | name = 'my_cache', -- "named" cache, useful for debug / report 72 | } 73 | ) 74 | 75 | local my_table, from_cache = my_cache_table:load(key) 76 | 77 | if my_table then 78 | if from_cache then 79 | -- cache_status == "HIT" (or "STALE") 80 | print "cache hit" 81 | else 82 | -- cache_status == "MISS" 83 | print "cache miss (valid data)" 84 | end 85 | else 86 | if from_cache then 87 | -- cache_status == "HIT_NEGATIVE" 88 | print "cache hit negative" 89 | else 90 | -- cache_status == "NO_DATA" 91 | print "cache miss (bad data)" 92 | end 93 | end 94 | 95 | end 96 | 97 | 98 | -- example of logging code on a "named" shcache 99 | local function log_shcache() 100 | local shcache_obj = ngx.ctx.shcache['my_cache'] 101 | 102 | print shcache_obj.cache_status -- HIT, MISS, HIT_NEGATIVE, STALE or NO_DATA 103 | print shcache_obj.lock_status -- NO_LOCK, IMMEDIATE or WAITED 104 | end 105 | 106 | 107 | 108 | Methods 109 | ======= 110 | 111 | new 112 | --- 113 | 114 | `syntax: cache_obj = shcache:new(ngx.shared.DICT, callbacks, opts?)` 115 | 116 | Creates an shcache object which implements the caching state machine in the attached documents 117 | 118 | `ngx.shared.DICT` is the shared dictionnary (declared in Nginx conf by `lua_shared_dict` directive) to use 119 | 120 | `callbacks.external_lookup` is the only required function, it's the closure necessary to lookup data. It should return the value if one exists, and optionally an error string to be logged, and/or an optional TTL value which overrides the positive_ttl option when saving a positive lookup. 121 | 122 | `callbacks.encode` and `callbacks.decode` are optional (default to identity), if you intend to store a complex 123 | Lua type (tables for instance), they should be declared as ngx.shared.DICT can only store text. 124 | 125 | The `opts` table accepts the following options : 126 | 127 | * `opts.positive_ttl` 128 | save a valid external loookup for, in seconds 129 | * `opts.positive_ttl` 130 | save a invalid loookup for, in seconds 131 | * `opts.actualize_ttl` 132 | re-actualize a stale record for, in seconds 133 | * `opts.lock_options` 134 | set option to lock see : http://github.com/agentzh/lua-resty-lock for more details. 135 | * `opts.name` 136 | if shcache object is named, it will automatically register itself in ngx.ctx.shcache (useful for logging). 137 | 138 | load 139 | ---- 140 | 141 | `syntax: data, from_cache = shcache:load(key)` 142 | 143 | Use key to load data from cache, if no cache is available `callbacks.external_lookup` will be called 144 | 145 | if data is available from cache `callbacks.decode` will be called before returning the data 146 | 147 | 148 | Author 149 | ====== 150 | 151 | Matthieu Tourne 152 | Rajeev Sharma 153 | John Graham Cumming 154 | 155 | Copyright and License 156 | ===================== 157 | 158 | This module is licensed under the BSD license. 159 | 160 | Copyright (C) 2013-2014, by CloudFlare Inc. 161 | 162 | All rights reserved. 163 | 164 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 165 | 166 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 167 | 168 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 169 | 170 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 171 | -------------------------------------------------------------------------------- /shcache.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (C) 2013 Matthieu Tourne 2 | -- @author Matthieu Tourne 3 | 4 | -- small overlay over shdict, smart cache load mechanism 5 | 6 | -- TODO (rdsharma): remove this line once we have tracked down bugs 7 | jit.off(true, true) 8 | 9 | local _M = {} 10 | 11 | local resty_lock = require("resty.lock") 12 | local conf = require("conf") 13 | local debug = require("debug") 14 | 15 | local DEBUG = conf.DEBUG or false 16 | 17 | -- defaults in secs 18 | local DEFAULT_POSITIVE_TTL = 10 -- cache for, successful lookup 19 | local DEFAULT_NEGATIVE_TTL = 2 -- cache for, failed lookup 20 | local DEFAULT_ACTUALIZE_TTL = 2 -- stale data, actualize data for 21 | 22 | -- default lock options, in secs 23 | local DEFAULT_LOCK_EXPTIME = 1 -- max wait if failing to call unlock() 24 | local DEFAULT_LOCK_TIMEOUT = 0.5 -- max waiting time of lock() 25 | local DEFAULT_LOCK_MAXSTEP = 0.1 -- max sleeping interval 26 | 27 | if conf then 28 | DEFAULT_NEGATIVE_TTL = conf.DEFAULT_NEGATIVE_TTL or DEFAULT_NEGATIVE_TTL 29 | DEFAULT_ACTUALIZE_TTL = conf.DEFAULT_ACTUALIZE_TTL or DEFAULT_ACTUALIZE_TTL 30 | end 31 | 32 | local bit = require("bit") 33 | local band = bit.band 34 | local bor = bit.bor 35 | local st_format = string.format 36 | 37 | -- there are only really 5 states total 38 | -- is_stale is_neg is_from_cache 39 | local MISS_STATE = 0 -- 0 0 0 40 | local HIT_POSITIVE_STATE = 1 -- 0 0 1 41 | local HIT_NEGATIVE_STATE = 3 -- 0 1 1 42 | local STALE_POSITIVE_STATE = 5 -- 1 0 1 43 | 44 | -- stale negative doesn't really make sense, use HIT_NEGATIVE instead 45 | -- local STALE_NEGATIVE_STATE = 7 -- 1 1 1 46 | 47 | -- xor to set 48 | local NEGATIVE_FLAG = 2 49 | local STALE_FLAG = 4 50 | 51 | local STATES = { 52 | [MISS_STATE] = 'MISS', 53 | [HIT_POSITIVE_STATE] = 'HIT', 54 | [HIT_NEGATIVE_STATE] = 'HIT_NEGATIVE', 55 | [STALE_POSITIVE_STATE] = 'STALE', 56 | -- [STALE_NEGATIVE_STATE] = 'STALE_NEGATIVE', 57 | } 58 | 59 | local function get_status(flags) 60 | return STATES[flags] or st_format('UNDEF (0x%x)', flags) 61 | end 62 | 63 | local EMPTY_DATA = '_EMPTY_' 64 | 65 | -- install debug functions 66 | if DEBUG then 67 | local resty_lock_lock = resty_lock.lock 68 | 69 | resty_lock.lock = function (...) 70 | local _, key = ... 71 | print("lock key: ", tostring(key)) 72 | return resty_lock_lock(...) 73 | end 74 | 75 | local resty_lock_unlock = resty_lock.unlock 76 | 77 | resty_lock.unlock = function (...) 78 | print("unlock") 79 | return resty_lock_unlock(...) 80 | end 81 | end 82 | 83 | 84 | -- store the object in the context 85 | -- useful for debugging and tracking cache status 86 | local function _store_object(self, name) 87 | if DEBUG then 88 | print('storing shcache: ', name, ' into ngx.ctx') 89 | end 90 | 91 | local ngx_ctx = ngx.ctx 92 | 93 | if not ngx_ctx.shcache then 94 | ngx_ctx.shcache = {} 95 | end 96 | ngx_ctx.shcache[name] = self 97 | end 98 | 99 | local obj_mt = { 100 | __index = _M, 101 | } 102 | 103 | -- default function for callbacks.encode / decode. 104 | local function _identity(data) 105 | return data 106 | end 107 | 108 | -- shdict: ngx.shared.DICT, created by the lua_shared_dict directive 109 | -- callbacks: see shcache state machine for user defined functions 110 | -- * callbacks.external_lookup is required 111 | -- * callbacks.external_lookup_arg is the (opaque) user argument for 112 | -- external_lookup 113 | -- * callbacks.encode : optional encoding before saving to shmem 114 | -- * callbacks.decode : optional decoding when retreiving from shmem 115 | -- opts: 116 | -- 117 | -- The TTL values are passed directly to the ngx.shared.DICT.set 118 | -- function (see 119 | -- http://wiki.nginx.org/HttpLuaModule#ngx.shared.DICT.set for the 120 | -- documentation). But note that the value 0 does not mean "do not 121 | -- cache at all", it means "no expiry time". So, for example, setting 122 | -- opts.negative_ttl to 0 means that a failed lookup will be cached 123 | -- forever. 124 | -- 125 | -- * opts.positive_ttl : save a valid external lookup for, in seconds 126 | -- * opts.negative_ttl : save a invalid lookup for, in seconds 127 | -- * opts.actualize_ttl : re-actualize a stale record for, in seconds 128 | -- 129 | -- * opts.lock_options : set option to lock see: 130 | -- http://github.com/agentzh/lua-resty-lock 131 | -- for more details. 132 | -- * opts.locks_shdict : specificy the name of the shdict containing 133 | -- the locks 134 | -- (useful if you might have locks key collisions) 135 | -- uses "locks" by default. 136 | -- * opts.name : if shcache object is named, it will automatically 137 | -- register itself in ngx.ctx.shcache 138 | -- (useful for logging). 139 | local function new(self, shdict, callbacks, opts) 140 | if not shdict then 141 | return nil, "shdict does not exist" 142 | end 143 | 144 | -- check that callbacks.external_lookup is set 145 | if not callbacks then 146 | return nil, "no callbacks argument" 147 | end 148 | 149 | local ext_lookup = callbacks.external_lookup 150 | if not ext_lookup then 151 | return nil, "no external_lookup callback specified" 152 | end 153 | 154 | local ext_udata = callbacks.external_lookup_arg 155 | 156 | local encode = callbacks.encode 157 | if not encode then 158 | encode = _identity 159 | end 160 | 161 | local decode = callbacks.decode 162 | if not decode then 163 | decode = _identity 164 | end 165 | 166 | local opts = opts or {} 167 | 168 | -- merge default lock options with the ones passed to new() 169 | local lock_options = opts.lock_options or {} 170 | if not lock_options.exptime then 171 | lock_options.exptime = DEFAULT_LOCK_EXPTIME 172 | end 173 | if not lock_options.timeout then 174 | lock_options.timeout = DEFAULT_LOCK_TIMEOUT 175 | end 176 | if not lock_options.max_step then 177 | lock_options.max_step = DEFAULT_LOCK_MAXSTEP 178 | end 179 | 180 | local name = opts.name 181 | 182 | local obj = { 183 | shdict = shdict, 184 | 185 | encode = encode, 186 | decode = decode, 187 | ext_lookup = ext_lookup, 188 | ext_udata = ext_udata, 189 | 190 | positive_ttl = opts.positive_ttl or DEFAULT_POSITIVE_TTL, 191 | negative_ttl = opts.negative_ttl or DEFAULT_NEGATIVE_TTL, 192 | 193 | -- ttl to actualize stale data to 194 | actualize_ttl = opts.actualize_ttl or DEFAULT_ACTUALIZE_TTL, 195 | 196 | lock_options = lock_options, 197 | 198 | locks_shdict = opts.lock_shdict or "locks", 199 | 200 | -- positive ttl specified by external lookup function 201 | lookup_ttl = nil, 202 | 203 | -- STATUS -- 204 | 205 | from_cache = false, 206 | cache_status = 'UNDEF', 207 | cache_state = MISS_STATE, 208 | lock_status = 'NO_LOCK', 209 | 210 | -- shdict:set() pushed out another value 211 | forcible_set = false, 212 | 213 | -- cache hit on second attempt (post lock) 214 | hit2 = false, 215 | 216 | name = name, 217 | } 218 | 219 | local locks = ngx.shared[obj.locks_shdict] 220 | 221 | -- check for existence, locks is not directly used 222 | if not locks then 223 | ngx.log(ngx.CRIT, 'shared mem locks is missing.\n', 224 | '## add to you lua conf: lua_shared_dict locks 5M; ##') 225 | return nil 226 | end 227 | 228 | local self = setmetatable(obj, obj_mt) 229 | 230 | -- if the shcache object is named 231 | -- keep track of the object in the context 232 | -- (useful for gathering stats at log phase) 233 | if name then 234 | _store_object(self, name) 235 | end 236 | 237 | return self 238 | end 239 | _M.new = new 240 | 241 | local function _enter_critical_section(self, key) 242 | if DEBUG then 243 | print('Entering critical section, shcache: ', self.name or '') 244 | end 245 | 246 | self.in_critical_section = true 247 | 248 | local critical_sections = ngx.ctx.critical_sections 249 | if not critical_sections then 250 | critical_sections = { 251 | count = 1, 252 | die = false, 253 | workers = { [self] = key }, 254 | } 255 | ngx.ctx.critical_sections = critical_sections 256 | return 257 | end 258 | 259 | -- TODO (mtourne): uncomment when ngx.thread.exit api exists. 260 | 261 | -- prevents new thread to enter a critical section if we're set to die. 262 | -- if critical_sections.die then 263 | -- ngx.thread.exit() 264 | -- end 265 | 266 | critical_sections.count = critical_sections.count + 1 267 | critical_sections.workers[self] = key 268 | 269 | if DEBUG then 270 | print('critical sections count: ', critical_sections.count) 271 | end 272 | end 273 | 274 | local function _exit_critical_section(self) 275 | if DEBUG then 276 | print('Leaving critical section, shcache: ', self.name or '') 277 | end 278 | 279 | local critical_sections = ngx.ctx.critical_sections 280 | if not critical_sections then 281 | ngx.log(ngx.ERR, 'weird state: ngx.ctx.critical_sections missing') 282 | return 283 | end 284 | 285 | critical_sections.count = critical_sections.count - 1 286 | critical_sections.workers[self] = nil 287 | 288 | if DEBUG then 289 | print('die: ', critical_sections.die, ', count: ', 290 | critical_sections.count) 291 | end 292 | 293 | local status = critical_sections.die 294 | if status and critical_sections.count <= 0 then 295 | -- safe to exit. 296 | if DEBUG then 297 | print('Last critical section, exiting.') 298 | end 299 | ngx.exit(status) 300 | end 301 | end 302 | 303 | -- acquire a lock 304 | local function _get_lock(self) 305 | local lock = self.lock 306 | if not lock then 307 | lock = resty_lock:new(self.locks_shdict, self.lock_options) 308 | self.lock = lock 309 | end 310 | return lock 311 | end 312 | 313 | -- remove the lock if there is any 314 | local function _unlock(self) 315 | local lock = self.lock 316 | if lock then 317 | local ok, err = lock:unlock() 318 | if not ok then 319 | ngx.log(ngx.ERR, "failed to unlock :" , err) 320 | end 321 | self.lock = nil 322 | end 323 | end 324 | 325 | local function _return(self, data, flags) 326 | -- make sure we remove the locks if any before returning data 327 | _unlock(self) 328 | 329 | -- set cache status 330 | local cache_status = get_status(self.cache_state) 331 | 332 | if cache_status == 'MISS' and not data then 333 | cache_status = 'NO_DATA' 334 | end 335 | 336 | self.cache_status = cache_status 337 | 338 | if self.in_critical_section then 339 | -- data has been cached, and lock on key is removed 340 | -- this is the end of the critical section. 341 | _exit_critical_section(self) 342 | self.in_critical_section = false 343 | end 344 | 345 | return data, self.from_cache 346 | end 347 | 348 | local function _set(self, key, data, ttl, flags) 349 | if DEBUG then 350 | print("saving key: ", key, ", for: ", ttl) 351 | end 352 | 353 | local ok, err, forcible = self.shdict:set(key, data, ttl, flags) 354 | 355 | self.forcible_set = forcible 356 | 357 | if not ok then 358 | ngx.log(ngx.ERR, 'failed to set key: ', key, ', err: ', err) 359 | end 360 | 361 | return ok 362 | end 363 | 364 | -- check if the data returned by :get() is considered empty 365 | local function _is_empty(data, flags) 366 | return flags and band(flags, NEGATIVE_FLAG) and data == EMPTY_DATA 367 | end 368 | 369 | -- save positive, encode the data if needed before :set() 370 | local function _save_positive(self, key, data, ttl) 371 | if DEBUG then 372 | if ttl then 373 | print("key: ", key, ". save positive, lookup ttl: ", ttl) 374 | else 375 | print("key: ", key, ". save positive, ttl: ", self.positive_ttl) 376 | end 377 | end 378 | 379 | data = self.encode(data) 380 | 381 | if ttl then 382 | self.lookup_ttl = ttl 383 | return _set(self, key, data, ttl, HIT_POSITIVE_STATE) 384 | end 385 | 386 | return _set(self, key, data, self.positive_ttl, HIT_POSITIVE_STATE) 387 | end 388 | 389 | -- save negative, no encoding required (no data actually saved) 390 | local function _save_negative(self, key) 391 | if DEBUG then 392 | print("key: ", key, ". save negative, ttl: ", self.negative_ttl) 393 | end 394 | return _set(self, key, EMPTY_DATA, self.negative_ttl, HIT_NEGATIVE_STATE) 395 | end 396 | 397 | -- save actualize, will boost a stale record to a live one 398 | local function _save_actualize(self, key, data, flags) 399 | local new_flags = bor(flags, STALE_FLAG) 400 | 401 | if DEBUG then 402 | print("key: ", key, ". save actualize, ttl: ", self.actualize_ttl, 403 | ". new state: ", get_status(new_flags)) 404 | end 405 | 406 | _set(self, key, data, self.actualize_ttl, new_flags) 407 | return new_flags 408 | end 409 | 410 | local function _process_cached_data(self, data, flags) 411 | if DEBUG then 412 | print("data: ", data, st_format(", flags: %x", flags)) 413 | end 414 | 415 | self.cache_state = flags 416 | self.from_cache = true 417 | 418 | if _is_empty(data, flags) then 419 | -- empty cached data 420 | return nil 421 | else 422 | return self.decode(data) 423 | end 424 | end 425 | 426 | -- wrapper to get data from the shdict 427 | local function _get(self, key) 428 | -- always call get_stale() as it does not free element 429 | -- like get does on each call 430 | local data, flags, stale = self.shdict:get_stale(key) 431 | 432 | if data and stale then 433 | if DEBUG then 434 | print("found stale data for key : ", key) 435 | end 436 | 437 | self.stale_data = { data, flags } 438 | 439 | return nil, nil 440 | end 441 | 442 | return data, flags 443 | end 444 | 445 | local function _get_stale(self) 446 | local stale_data = self.stale_data 447 | if stale_data then 448 | return unpack(stale_data) 449 | end 450 | 451 | return nil, nil 452 | end 453 | 454 | local function load(self, key) 455 | -- start: check for existing cache 456 | -- clear previous data stored in stale_data 457 | self.stale_data = nil 458 | local data, flags = _get(self, key) 459 | 460 | -- hit: process_cache_hit 461 | if data then 462 | data = _process_cached_data(self, data, flags) 463 | return _return(self, data) 464 | end 465 | 466 | -- miss: set lock 467 | 468 | -- lock: set a lock before performing external lookup 469 | local lock = _get_lock(self) 470 | local elapsed, err = lock:lock(key) 471 | 472 | if not elapsed then 473 | -- failed to acquire lock, still proceed normally to external_lookup 474 | -- unlock() might fail. 475 | local timeout 476 | local opts = self.lock_options 477 | if opts then 478 | timeout = opts.timeout 479 | end 480 | ngx.log(ngx.ERR, "failed to acquire the lock on key \"", key, "\" for ", 481 | timeout, " sec: ", err) 482 | self.lock_status = 'ERROR' 483 | -- _unlock won't try to unlock() without a valid lock 484 | self.lock = nil 485 | else 486 | -- lock acquired successfuly 487 | 488 | if elapsed > 0 then 489 | 490 | -- elapsed > 0 => waited lock (other thread might have :set() the data) 491 | -- (more likely to get a HIT on cache_load 2) 492 | self.lock_status = 'WAITED' 493 | 494 | else 495 | 496 | -- elapsed == 0 => immediate lock 497 | -- it is less likely to get a HIT on cache_load 2 498 | -- but still perform it (race condition cases) 499 | self.lock_status = 'IMMEDIATE' 500 | end 501 | 502 | -- perform cache_load 2 503 | data, flags = _get(self, key) 504 | if data then 505 | -- hit2 : process cache hit 506 | 507 | self.hit2 = true 508 | 509 | -- unlock before de-serializing cached data 510 | _unlock(self) 511 | data = _process_cached_data(self, data, flags) 512 | return _return(self, data) 513 | end 514 | 515 | -- continue to external lookup 516 | end 517 | 518 | -- mark the beginning of the critical section 519 | -- (we want to wait for the data to be looked up and cached successfully) 520 | _enter_critical_section(self, key) 521 | 522 | -- perform external lookup 523 | local data, err, ttl = self.ext_lookup(self.ext_udata) 524 | 525 | if data then 526 | -- succ: save positive and return the data 527 | _save_positive(self, key, data, ttl) 528 | return _return(self, data) 529 | else 530 | ngx.log(ngx.WARN, 'external lookup failed: ', err) 531 | end 532 | 533 | -- external lookup failed 534 | -- attempt to load stale data 535 | data, flags = _get_stale(self) 536 | if data and not _is_empty(data, flags) then 537 | -- hit_stale + valid (positive) data 538 | 539 | flags = _save_actualize(self, key, data, flags) 540 | -- unlock before de-serializing data 541 | _unlock(self) 542 | data = _process_cached_data(self, data, flags) 543 | return _return(self, data) 544 | end 545 | 546 | if DEBUG and data then 547 | -- there is data, but it failed _is_empty() => stale negative data 548 | print('STALE_NEGATIVE data => cache as a new HIT_NEGATIVE') 549 | end 550 | 551 | -- nothing has worked, save negative and return empty 552 | _save_negative(self, key) 553 | return _return(self, nil) 554 | end 555 | _M.load = load 556 | 557 | return _M 558 | --------------------------------------------------------------------------------