local resty_mlcache = require "kong.resty.mlcache" local buffer = require "string.buffer" local encode = buffer.encode local type = type local pairs = pairs local error = error local max = math.max local ngx = ngx local shared = ngx.shared local ngx_log = ngx.log local get_phase = ngx.get_phase local ERR = ngx.ERR local NOTICE = ngx.NOTICE local DEBUG = ngx.DEBUG local NO_TTL_FLAG = resty_mlcache.NO_TTL_FLAG local CHANNEL_NAME = "mlcache" --[[ Hypothesis ---------- Item size: 1024 bytes Max memory limit: 500 MiBs LRU size must be: (500 * 2^20) / 1024 = 512000 Floored: 500.000 items should be a good default --]] local LRU_SIZE = 5e5 local _init = {} local function log(lvl, ...) return ngx_log(lvl, "[DB cache] ", ...) end local _M = {} local mt = { __index = _M } function _M.new(opts) opts = opts or {} -- opts validation if type(opts.shm_name) ~= "string" then error("opts.shm_name must be a string", 2) end if _init[opts.shm_name] then error("kong.cache (" .. opts.shm_name .. ") was already created", 2) end if not opts.cluster_events then error("opts.cluster_events is required", 2) end if not opts.worker_events then error("opts.worker_events is required", 2) end if opts.ttl and type(opts.ttl) ~= "number" then error("opts.ttl must be a number", 2) end if opts.neg_ttl and type(opts.neg_ttl) ~= "number" then error("opts.neg_ttl must be a number", 2) end if opts.cache_pages and opts.cache_pages ~= 1 and opts.cache_pages ~= 2 then error("opts.cache_pages must be 1 or 2", 2) end if opts.resty_lock_opts and type(opts.resty_lock_opts) ~= "table" then error("opts.resty_lock_opts must be a table", 2) end if opts.invalidation_channel and type(opts.invalidation_channel) ~= "string" then error("opts.invalidation_channel must be a string", 2) end local shm_name = opts.shm_name if not shared[shm_name] then log(ERR, "shared dictionary ", shm_name, " not found") end local shm_miss_name = shm_name .. "_miss" if not shared[shm_miss_name] then log(ERR, "shared dictionary ", shm_miss_name, " not found") end local ttl = max(opts.ttl or 3600, 0) local neg_ttl = max(opts.neg_ttl or 300, 0) local worker_events = opts.worker_events local mlcache, err = resty_mlcache.new(shm_name, shm_name, { shm_miss = shm_miss_name, shm_locks = "kong_locks", shm_set_tries = 3, lru_size = opts.lru_size or LRU_SIZE, ttl = ttl, neg_ttl = neg_ttl, resurrect_ttl = opts.resurrect_ttl or 30, resty_lock_opts = opts.resty_lock_opts, ipc = { register_listeners = function(events) for _, event_t in pairs(events) do worker_events.register(function(data) event_t.handler(data) end, CHANNEL_NAME, event_t.channel) end end, broadcast = function(channel, data) local ok, err = worker_events.post(CHANNEL_NAME, channel, data) if not ok then log(ERR, "failed to post event '", CHANNEL_NAME, "', '", channel, "': ", err) end end } }) if not mlcache then return nil, "failed to instantiate mlcache: " .. err end local cluster_events = opts.cluster_events local invalidation_channel = opts.invalidation_channel or ("invalidations_" .. shm_name) local self = { cluster_events = cluster_events, mlcache = mlcache, dict = shared[shm_name], shm_name = shm_name, ttl = ttl, neg_ttl = neg_ttl, invalidation_channel = invalidation_channel, } local ok, err = cluster_events:subscribe(self.invalidation_channel, function(key) log(DEBUG, self.shm_name .. " received invalidate event from cluster for key: '", key, "'") self:invalidate_local(key) end) if not ok then return nil, "failed to subscribe to invalidations cluster events " .. "channel: " .. err end _init[shm_name] = true return setmetatable(self, mt) end function _M:get(key, opts, cb, ...) if type(key) ~= "string" then error("key must be a string", 2) end local v, err, hit_lvl = self.mlcache:get(key, opts, cb, ...) if err then return nil, "failed to get from node cache: " .. err end return v, nil, hit_lvl end function _M:renew(key, opts, cb, ...) if type(key) ~= "string" then error("key must be a string", 2) end local v, err, ttl = self.mlcache:renew(key, opts, cb, ...) if err then return nil, "failed to renew key in node cache: " .. err end return v, nil, ttl end function _M:get_bulk(bulk, opts) if type(bulk) ~= "table" then error("bulk must be a table", 2) end if opts ~= nil and type(opts) ~= "table" then error("opts must be a table", 2) end local res, err = self.mlcache:get_bulk(bulk, opts) if err then return nil, "failed to get_bulk from node cache: " .. err end return res end function _M:safe_set(key, value) local marshalled, err = encode(value) if err then return nil, err end return self.dict:safe_set(self.shm_name .. key, marshalled, self.ttl, self.ttl == 0 and NO_TTL_FLAG or 0) end function _M:probe(key) if type(key) ~= "string" then error("key must be a string", 2) end local ttl, err, v = self.mlcache:peek(key) if err then return nil, "failed to probe from node cache: " .. err end return ttl, nil, v end function _M:invalidate_local(key) if type(key) ~= "string" then error("key must be a string", 2) end log(DEBUG, self.shm_name, " invalidating (local): '", key, "'") local ok, err = self.mlcache:delete(key) if not ok then log(ERR, "failed to delete entity from node cache: ", err) end end function _M:invalidate(key) if type(key) ~= "string" then error("key must be a string", 2) end self:invalidate_local(key) log(DEBUG, "broadcasting (cluster) invalidation for key: '", key, "'") local ok, err = self.cluster_events:broadcast(self.invalidation_channel, key) if not ok then log(ERR, "failed to broadcast cached entity invalidation: ", err) end end function _M:purge() if get_phase() ~= "timer" then log(NOTICE, "purging (local) cache") end local ok, err = self.mlcache:purge(true) if not ok then log(ERR, "failed to purge cache: ", err) end end return _M