Fix. Cache class. (#2755)

* Fix. Cache class.

 * `send_event` raise error so `Cache.del` did not remove key or send any event
 * use `memcache` method by default even if `cache` table does not defined in config
 * `Cache.get` did not return any data when use `memcache` method
 * `Cache.get` did not close file. (Its should not be a big problem because GC should do it by self).
 * `Cache.get` can returns some undefined global value. (if method is `file` and file not exists then method returns global `result` value)
 * `Cache.get` does not need check for file existence
 * Value escaping does not needed for `file` method
 * Needed different key escaping for `memcache` and `file` methods
 * Update self test

* Change. Use random names for temp files.
This commit is contained in:
Alexey Melnichuk 2017-07-26 18:40:53 +03:00 committed by FusionPBX
parent c84fd7ebe4
commit e728cb44ae
2 changed files with 139 additions and 60 deletions

View File

@ -11,7 +11,15 @@ require "resources.functions.config";
-- include functions
require "resources.functions.trim";
require "resources.functions.file_exists";
-- include file class
local File = require "resources.functions.file";
-- get logger
local log = require "resources.functions.log".cache;
-- get method for cache from config
local cache_method = cache and cache.method or 'memcache'
local api = api
if not api then
@ -25,17 +33,16 @@ if not api then
end
end
local function send_event(action, key)
if (cache.method == "memcache") then
local event = freeswitch.Event("CUSTOM", "fusion::memcache");
event:addHeader("API-Command", "memcache");
end
if (cache.method == "file") then
local event = freeswitch.Event("CUSTOM", "fusion::file");
event:addHeader("API-Command", "file");
end
event:addHeader("API-Command-Argument", action .. " " .. key);
local send_event
if freeswitch then
send_event = function (action, key)
local event = freeswitch.Event("CUSTOM", "fusion::" .. cache_method)
event:addHeader("API-Command", cache_method)
event:addHeader("API-Command-Argument", action .. " " .. key)
event:fire()
end
else
send_event = function() end
end
local Cache = {}
@ -54,15 +61,47 @@ local function check_error(result)
return result
end
-- convert cache key to file path
local function key2file(key)
return cache.location .. '/' .. string.gsub(key, '[:\\/]', {
[':'] = '.',
['\\'] = '_',
['/'] = '_',
})
end
-- generate random file name
local function tmp_file(key)
-- @todo may be it worth fallback to `os.tmpname`
local uuid = check_error(api:execute("create_uuid", ""))
if uuid then key = key .. '.' .. uuid end
return key .. '.tmp'
end
-- convert cache key to memcache key
local function key2key(key)
return (string.gsub(key, "\\", "\\\\"))
end
-- encode value to be able store it in memcache
local function memcache_encode(value)
return (string.gsub(value, "'", "'"):gsub("\\", "\\\\"))
end
-- decode value retrived from memcache
local function memcache_decode(value)
return (string.gsub(value, "'", "'"))
end
function Cache.support()
-- assume it is not unloadable
if Cache._support then
return true
end
if (cache.method == "memcache") then
if (cache_method == "memcache") then
Cache._support = (trim(api:execute('module_exists', 'mod_memcache')) == 'true')
else
Cache._support = true;
Cache._support = true;
end
return Cache._support
end
@ -75,77 +114,103 @@ end
-- @return[2] error string `e.g. 'NOT FOUND'
-- @note error string does not contain `-ERR` prefix
function Cache.get(key)
local key = key:gsub(":", ".")
if (cache.method == "memcache") then
local result, err = check_error(api:execute('memcache', 'get ' .. key))
local result, err = nil, 'UNSUPPORTTED'
if (cache_method == "memcache") then
result, err = check_error(api:execute('memcache', 'get ' .. key2key(key)))
if result then
result = memcache_decode(result)
end
end
if (cache.method == "file") then
if (file_exists(cache.location .. "/" .. key)) then
--freeswitch.consoleLog("notice", "[cache] location: " .. cache.location .. "/" .. key .."\n");
local file, err = io.open(cache.location .. "/" .. key, "rb")
result = file:read("*all")
else
if (cache_method == "file") then
key = key2file(key)
-- log.noticef('location: %s', key)
result, err = File.read(key)
if not result then
err = 'NOT FOUND';
end
end
--freeswitch.consoleLog("notice", "[cache] result: " .. result .. "\n");
--file:close()
if not result then return nil, err end
return (result:gsub("'", "'"))
-- log.noticef('result: %s', tostring(result or err))
return result, err
end
function Cache.set(key, value, expire)
key = key:gsub(":", ".")
value = value:gsub("'", "'"):gsub("\\", "\\\\")
--local ok, err = check_error(write_file(cache.location .. "/" .. key, value))
if (cache.method == "file") then
if (not file_exists(cache.location .. "/" .. key .. ".tmp")) then
if (cache_method == "file") then
-- To make write to cache atomic we first write to some
-- temp file with uniq random name. So if there more than
-- one writers all of then write to its own temp file.
-- After it done we do rename and this operation will
-- leave only one file. If rename fail we assume that
-- some one else write this cache item so we just remove our
-- temp file. This should works because all writers should
-- write same values and it does not metter which one do this first.
key = key2file(key)
if (not File.exists(key)) then
-- get random name for the temp file
local key_tmp = tmp_file(key)
--write the temp file
local file, err = io.open(cache.location .. "/" .. key .. ".tmp", "wb")
if not file then
log.err("Can not open file to write:" .. tostring(err))
local ok, err = File.write(key_tmp, value)
if not ok then
log.errf('can not write file `%s`: %s', key_tmp, tostring(err))
return nil, err
end
file:write(value)
file:close()
--move the temp file
os.rename(cache.location .. "/" .. key .. ".tmp", cache.location .. "/" .. key)
ok, err = File.rename(key_tmp, key)
-- if we can not rename file then assume that key already exists,
-- so we have to remove our temp file.
if not ok then File.remove(key_tmp) end
return ok, err
end
--! @todo returns special code to show reuse value?
return true
end
if (cache.method == "memcache") then
if (cache_method == "memcache") then
value = memcache_encode(value)
expire = expire and tostring(expire) or ""
local ok, err = check_error(api:execute("memcache", "set " .. key .. " '" .. value .. "' " .. expire))
local ok, err = check_error(api:execute("memcache", "set " .. key2key(key) .. " '" .. value .. "' " .. expire))
if not ok then return nil, err end
return ok == '+OK'
end
return nil, 'UNSUPPORTTED'
end
function Cache.del(key)
key = key:gsub(":", ".")
send_event('delete', key)
if (cache.method == "memcache") then
local result, err = check_error(api:execute("memcache", "delete " .. key))
end
if (cache.method == "file") then
if (file_exists(cache.location .. "/" .. key)) then
os.remove(cache.location .. "/" .. key)
if (file_exists(cache.location .. "/" .. key .. ".tmp")) then
os.remove(cache.location .. "/" .. key .. ".tmp")
if (cache_method == "memcache") then
local result, err = check_error(api:execute("memcache", "delete " .. key2key(key)))
if not result then
if err == 'NOT FOUND' then
return true
end
else
err = 'NOT FOUND'
return nil, err
end
return result == '+OK'
end
if not result then
if err == 'NOT FOUND' then
return true
if (cache_method == "file") then
key = key2file(key)
--! @todo remove file exists check. This check needs only for return `NOT FOUND` code.
local result, err = not File.exists(key)
if not result then
result, err = File.remove(key)
if not result then
log.errf('can not remove file `%s`: %s', key, tostring(err))
end
end
return nil, err
File.remove(key .. ".tmp")
return result, err
end
return result == '+OK'
return nil, 'UNSUPPORTTED'
end
function Cache._self_test()
print('cache mode: ', cache_method)
assert(Cache.support())
Cache.del("a")
@ -158,10 +223,23 @@ function Cache._self_test()
assert(s == Cache.get("a"))
assert(true == Cache.del("a"))
local k = 'a/b\\c/d'
Cache.del(k)
assert(true == Cache.set(k, s))
assert(s == Cache.get(k))
assert(true == Cache.del(k))
ok, err = Cache.get(k)
assert(nil == ok)
assert(err == "NOT FOUND")
print('done')
end
-- if debug.self_test then
-- Cache._self_test()
-- end
if debug.self_test then
Cache._self_test()
end
return Cache

View File

@ -9,7 +9,7 @@ local base64 = base64
local function write_file(fname, data, mode)
local file, err = io.open(fname, mode or "wb")
if not file then
log.err("Can not open file to write:" .. tostring(err))
-- log.err("Can not open file to write:" .. tostring(err))
return nil, err
end
file:write(data)
@ -25,7 +25,7 @@ end
local function read_file(fname, mode)
local file, err = io.open(fname, mode or "rb")
if not file then
log.err("Can not open file to read:" .. tostring(err))
-- log.err("Can not open file to read:" .. tostring(err))
return nil, err
end
local data = file:read("*all")
@ -54,4 +54,5 @@ return {
write_base64 = write_base64;
exists = file_exists;
remove = os.remove;
rename = os.rename;
}