EndGame/resty/lrucache.lua

339 lines
6.9 KiB
Lua

-- Copyright (C) Yichun Zhang (agentzh)
local ffi = require "ffi"
local ffi_new = ffi.new
local ffi_sizeof = ffi.sizeof
local ffi_cast = ffi.cast
local ffi_fill = ffi.fill
local ngx_now = ngx.now
local uintptr_t = ffi.typeof("uintptr_t")
local setmetatable = setmetatable
local tonumber = tonumber
local type = type
local new_tab
do
local ok
ok, new_tab = pcall(require, "table.new")
if not ok then
new_tab = function(narr, nrec) return {} end
end
end
if string.find(jit.version, " 2.0", 1, true) then
ngx.log(ngx.ALERT, "use of lua-resty-lrucache with LuaJIT 2.0 is ",
"not recommended; use LuaJIT 2.1+ instead")
end
local ok, tb_clear = pcall(require, "table.clear")
if not ok then
local pairs = pairs
tb_clear = function (tab)
for k, _ in pairs(tab) do
tab[k] = nil
end
end
end
-- queue data types
--
-- this queue is a double-ended queue and the first node
-- is reserved for the queue itself.
-- the implementation is mostly borrowed from nginx's ngx_queue_t data
-- structure.
ffi.cdef[[
typedef struct lrucache_queue_s lrucache_queue_t;
struct lrucache_queue_s {
double expire; /* in seconds */
lrucache_queue_t *prev;
lrucache_queue_t *next;
uint32_t user_flags;
};
]]
local queue_arr_type = ffi.typeof("lrucache_queue_t[?]")
local queue_type = ffi.typeof("lrucache_queue_t")
local NULL = ffi.null
-- queue utility functions
local function queue_insert_tail(h, x)
local last = h[0].prev
x.prev = last
last.next = x
x.next = h
h[0].prev = x
end
local function queue_init(size)
if not size then
size = 0
end
local q = ffi_new(queue_arr_type, size + 1)
ffi_fill(q, ffi_sizeof(queue_type, size + 1), 0)
if size == 0 then
q[0].prev = q
q[0].next = q
else
local prev = q[0]
for i = 1, size do
local e = q + i
e.user_flags = 0
prev.next = e
e.prev = prev
prev = e
end
local last = q[size]
last.next = q
q[0].prev = last
end
return q
end
local function queue_is_empty(q)
-- print("q: ", tostring(q), "q.prev: ", tostring(q), ": ", q == q.prev)
return q == q[0].prev
end
local function queue_remove(x)
local prev = x.prev
local next = x.next
next.prev = prev
prev.next = next
-- for debugging purpose only:
x.prev = NULL
x.next = NULL
end
local function queue_insert_head(h, x)
x.next = h[0].next
x.next.prev = x
x.prev = h
h[0].next = x
end
local function queue_last(h)
return h[0].prev
end
local function queue_head(h)
return h[0].next
end
-- true module stuffs
local _M = {
_VERSION = '0.10'
}
local mt = { __index = _M }
local function ptr2num(ptr)
return tonumber(ffi_cast(uintptr_t, ptr))
end
function _M.new(size)
if size < 1 then
return nil, "size too small"
end
local self = {
hasht = {},
free_queue = queue_init(size),
cache_queue = queue_init(),
key2node = {},
node2key = {},
num_items = 0,
max_items = size,
}
return setmetatable(self, mt)
end
function _M.count(self)
return self.num_items
end
function _M.capacity(self)
return self.max_items
end
function _M.get(self, key)
local hasht = self.hasht
local val = hasht[key]
if val == nil then
return nil
end
local node = self.key2node[key]
-- print(key, ": moving node ", tostring(node), " to cache queue head")
local cache_queue = self.cache_queue
queue_remove(node)
queue_insert_head(cache_queue, node)
if node.expire >= 0 and node.expire < ngx_now() then
-- print("expired: ", node.expire, " > ", ngx_now())
return nil, val, node.user_flags
end
return val, nil, node.user_flags
end
function _M.delete(self, key)
self.hasht[key] = nil
local key2node = self.key2node
local node = key2node[key]
if not node then
return false
end
key2node[key] = nil
self.node2key[ptr2num(node)] = nil
queue_remove(node)
queue_insert_tail(self.free_queue, node)
self.num_items = self.num_items - 1
return true
end
function _M.set(self, key, value, ttl, flags)
local hasht = self.hasht
hasht[key] = value
local key2node = self.key2node
local node = key2node[key]
if not node then
local free_queue = self.free_queue
local node2key = self.node2key
if queue_is_empty(free_queue) then
-- evict the least recently used key
-- assert(not queue_is_empty(self.cache_queue))
node = queue_last(self.cache_queue)
local oldkey = node2key[ptr2num(node)]
-- print(key, ": evicting oldkey: ", oldkey, ", oldnode: ",
-- tostring(node))
if oldkey then
hasht[oldkey] = nil
key2node[oldkey] = nil
end
else
-- take a free queue node
node = queue_head(free_queue)
-- only add count if we are not evicting
self.num_items = self.num_items + 1
-- print(key, ": get a new free node: ", tostring(node))
end
node2key[ptr2num(node)] = key
key2node[key] = node
end
queue_remove(node)
queue_insert_head(self.cache_queue, node)
if ttl then
node.expire = ngx_now() + ttl
else
node.expire = -1
end
if type(flags) == "number" and flags >= 0 then
node.user_flags = flags
else
node.user_flags = 0
end
end
function _M.get_keys(self, max_count, res)
if not max_count or max_count == 0 then
max_count = self.num_items
end
if not res then
res = new_tab(max_count + 1, 0) -- + 1 for trailing hole
end
local cache_queue = self.cache_queue
local node2key = self.node2key
local i = 0
local node = queue_head(cache_queue)
while node ~= cache_queue do
if i >= max_count then
break
end
i = i + 1
res[i] = node2key[ptr2num(node)]
node = node.next
end
res[i + 1] = nil
return res
end
function _M.flush_all(self)
tb_clear(self.hasht)
tb_clear(self.node2key)
tb_clear(self.key2node)
local cache_queue = self.cache_queue
local free_queue = self.free_queue
-- splice the cache_queue into free_queue
if not queue_is_empty(cache_queue) then
local free_head = free_queue[0]
local free_last = free_head.prev
local cache_head = cache_queue[0]
local cache_first = cache_head.next
local cache_last = cache_head.prev
free_last.next = cache_first
cache_first.prev = free_last
cache_last.next = free_head
free_head.prev = cache_last
cache_head.next = cache_queue
cache_head.prev = cache_queue
end
end
return _M