Initial code import
[msb/apigateway.git] / openresty-ext / src / assembly / resources / openresty / nginx / luaext / vendor / shcache.lua
1 -- Copyright (C) 2013 Matthieu Tourne
2 -- @author Matthieu Tourne <matthieu@cloudflare.com>
3
4 -- small overlay over shdict, smart cache load mechanism
5
6 local M = {}
7
8 local resty_lock = require("resty.lock")
9
10 local DEBUG = false
11
12 -- defaults in secs
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
16
17 -- default lock options, in secs
18 local function _get_default_lock_options()
19    return {
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
23    }
24 end
25
26 local function prequire(m)
27   local ok, err_or_module = pcall(require, m)
28   if not ok then
29      return nil, err_or_module
30   end
31   return err_or_module
32 end
33
34 local conf = prequire("conf")
35 if conf then
36    DEFAULT_NEGATIVE_TTL = conf.DEFAULT_NEGATIVE_TTL or DEFAULT_NEGATIVE_TTL
37    DEFAULT_ACTUALIZE_TTL = conf.DEFAULT_ACTUALIZE_TTL or DEFAULT_ACTUALIZE_TTL
38 end
39
40 local band = bit.band
41 local bor = bit.bor
42 local st_format = string.format
43
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
50
51 -- stale negative doesn't really make sense, use HIT_NEGATIVE instead
52 -- local STALE_NEGATIVE_STATE = 7   -- 1           1       1
53
54 -- xor to set
55 local NEGATIVE_FLAG = 2
56 local STALE_FLAG = 4
57
58 local STATES = {
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',
64 }
65
66 local function get_status(flags)
67    return STATES[flags] or st_format('UNDEF (0x%x)', flags)
68 end
69
70 local EMPTY_DATA = '_EMPTY_'
71
72 -- install debug functions
73 if DEBUG then
74    local resty_lock_lock = resty_lock.lock
75
76    resty_lock.lock = function (...)
77       local _, key = unpack({...})
78       print("lock key: ", tostring(key))
79       return resty_lock_lock(...)
80    end
81
82    local resty_lock_unlock = resty_lock.unlock
83
84    resty_lock.unlock = function (...)
85       print("unlock")
86       return resty_lock_unlock(...)
87    end
88 end
89
90
91 -- store the object in the context
92 -- useful for debugging and tracking cache status
93 local function _store_object(self, name)
94    if DEBUG then
95       print('storing shcache: ', name, ' into ngx.ctx')
96    end
97
98    local ngx_ctx = ngx.ctx
99
100    if not ngx_ctx.shcache then
101       ngx_ctx.shcache = {}
102    end
103    ngx_ctx.shcache[name] = self
104 end
105
106 local obj_mt = {
107    __index = M,
108 }
109
110 -- default function for callbacks.encode / decode.
111 local function _identity(data)
112    return data
113 end
114
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
120 -- opts:
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
125 --                            for more details.
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)
132    if not shdict then
133       return nil, "shdict does not exist"
134    end
135
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"
139    end
140
141    if not callbacks.encode then
142       callbacks.encode = _identity
143    end
144
145    if not callbacks.decode then
146       callbacks.decode = _identity
147    end
148
149    local opts = opts or {}
150
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
155          lock_options[k] = v
156       end
157    end
158
159    local name = opts.name
160
161    local obj = {
162       shdict = shdict,
163       callbacks = callbacks,
164
165       positive_ttl = opts.positive_ttl or DEFAULT_POSITIVE_TTL,
166       negative_ttl = opts.negative_ttl or DEFAULT_NEGATIVE_TTL,
167
168       -- ttl to actualize stale data to
169       actualize_ttl = opts.actualize_ttl or DEFAULT_ACTUALIZE_TTL,
170
171       lock_options = lock_options,
172
173       locks_shdict = opts.lock_shdict or "locks",
174
175       -- STATUS --
176
177       from_cache = false,
178       cache_status = 'UNDEF',
179       cache_state = MISS_STATE,
180       lock_status = 'NO_LOCK',
181
182       -- shdict:set() pushed out another value
183       forcible_set = false,
184
185       -- cache hit on second attempt (post lock)
186       hit2 = false,
187
188       name = name,
189    }
190
191    local locks = ngx.shared[obj.locks_shdict]
192
193    -- check for existence, locks is not directly used
194    if not locks then
195       ngx.log(ngx.CRIT, 'shared mem locks is missing.\n',
196               '## add to you lua conf: lua_shared_dict locks 5M; ##')
197        return nil
198    end
199
200    local self = setmetatable(obj, obj_mt)
201
202    -- if the shcache object is named
203    -- keep track of the object in the context
204    -- (useful for gathering stats at log phase)
205    if name then
206       _store_object(self, name)
207    end
208
209    return self
210 end
211 M.new = new
212
213 -- acquire a lock
214 local function _get_lock(self)
215    local lock = self.lock
216    if not lock then
217       lock = resty_lock:new(self.locks_shdict, self.lock_options)
218       self.lock = lock
219    end
220    return lock
221 end
222
223 -- remove the lock if there is any
224 local function _unlock(self)
225    local lock = self.lock
226    if lock then
227       local ok, err = lock:unlock()
228       if not ok then
229          ngx.log(ngx.ERR, "failed to unlock :" , err)
230       end
231       self.lock = nil
232    end
233 end
234
235 local function _return(self, data, flags)
236    -- make sure we remove the locks if any before returning data
237    _unlock(self)
238
239    -- set cache status
240    local cache_status = get_status(self.cache_state)
241
242    if cache_status == 'MISS' and not data then
243       cache_status = 'NO_DATA'
244    end
245
246    self.cache_status = cache_status
247
248    return data, self.from_cache
249 end
250
251 local function _set(self, ...)
252    if DEBUG then
253       local key, data, ttl, flags = unpack({...})
254       print("saving key: ", key, ", for: ", ttl)
255    end
256
257    local ok, err, forcible = self.shdict:set(...)
258
259    self.forcible_set = forcible
260
261    if not ok then
262       local key, data, ttl, flags = unpack({...})
263       ngx.log(ngx.ERR, 'failed to set key: ', key, ', err: ', err)
264    end
265
266    return ok
267 end
268
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
272 end
273
274 -- save positive, encode the data if needed before :set()
275 local function _save_positive(self, key, data)
276    if DEBUG then
277       print("key: ", key, ". save positive, ttl: ", self.positive_ttl)
278    end
279    data = self.callbacks.encode(data)
280    return _set(self, key, data, self.positive_ttl, HIT_POSITIVE_STATE)
281 end
282
283 -- save negative, no encoding required (no data actually saved)
284 local function _save_negative(self, key)
285    if DEBUG then
286       print("key: ", key, ". save negative, ttl: ", self.negative_ttl)
287    end
288    return _set(self, key, EMPTY_DATA, self.negative_ttl, HIT_NEGATIVE_STATE)
289 end
290
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)
294
295    if DEBUG then
296       print("key: ", key, ". save actualize, ttl: ", self.actualize_ttl,
297             ". new state: ", get_status(new_flags))
298    end
299
300    _set(self, key, data, self.actualize_ttl, new_flags)
301    return new_flags
302 end
303
304 local function _process_cached_data(self, data, flags)
305    if DEBUG then
306       print("data: ", data, st_format(", flags: %x", flags))
307    end
308
309    self.cache_state = flags
310    self.from_cache = true
311
312    if _is_empty(data, flags) then
313       -- empty cached data
314       return nil
315    else
316       return self.callbacks.decode(data)
317    end
318 end
319
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)
325
326    if data and stale then
327       if DEBUG then
328          print("found stale data for key : ", key)
329       end
330
331       self.stale_data = { data, flags }
332
333       return nil, nil
334    end
335
336    return data, flags
337 end
338
339 local function _get_stale(self)
340    local stale_data = self.stale_data
341    if stale_data then
342       return unpack(stale_data)
343    end
344
345    return nil, nil
346 end
347
348 local function load(self, key)
349    -- start: check for existing cache
350    local data, flags = _get(self, key)
351
352    -- hit: process_cache_hit
353    if data then
354       data = _process_cached_data(self, data, flags)
355       return _return(self, data)
356    end
357
358    -- miss: set lock
359
360    -- lock: set a lock before performing external lookup
361    local lock = _get_lock(self)
362    local elapsed, err = lock:lock(key)
363
364    if not elapsed then
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
370       self.lock = nil
371    else
372       -- lock acquired successfuly
373
374       if elapsed > 0 then
375
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'
379
380       else
381
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'
386       end
387
388       -- perform cache_load 2
389       data, flags = _get(self, key)
390       if data then
391          -- hit2 : process cache hit
392
393          self.hit2 = true
394
395          -- unlock before de-serializing cached data
396          _unlock(self)
397          data = _process_cached_data(self, data, flags)
398          return _return(self, data)
399       end
400
401       -- continue to external lookup
402    end
403
404    -- perform external lookup
405    data, err = self.callbacks.external_lookup()
406
407    if data then
408       -- succ: save positive and return the data
409
410       _save_positive(self, key, data)
411       return _return(self, data)
412    else
413       ngx.log(ngx.WARN, 'external lookup failed: ', err)
414    end
415
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
421
422       flags = _save_actualize(self, key, data, flags)
423       -- unlock before de-serializing data
424       _unlock(self)
425       data = _process_cached_data(self, data, flags)
426       return _return(self, data)
427    end
428
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')
432    end
433
434    -- nothing has worked, save negative and return empty
435    _save_negative(self, key)
436    return _return(self, nil)
437 end
438 M.load = load
439
440 return M