smr/src/lua/cache.lua

226 lines
5.5 KiB
Lua

--[[ md
@name lua/cache
Implements a simple in memory read through cache.
The cache has no upper size limit, and may cause out-of-memory errors.
When this happens, the OS will kill the kore worker process,
and the kore parent process will restart with a fresh, empty cache.
]]
local sql = require("lsqlite3")
local queries = require("queries")
local util = require("util")
local db = require("db")
local ret = {}
local stmnt_cache, stmnt_insert_cache, stmnt_dirty_cache
--[[ cat
@name lua/cache
<h3>Schema for Cache</h3>
<p>The cache mechanism is a in-memeory sqlite3 database behind the scenes, it
can ensure consistency and atomic updates & dirtying, though it doesn't today.</p>
<table>
<caption>cache</caption>
<tr>
<th>Attributes</th>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
<tr>
<td>Primary Key</td>
<td>path</td>
<td>TEXT</td>
<td>
The logical path this text was rendered at,
before browser-specific headers (like accept-encoding)
are applied
</td>
</tr>
<tr>
<td></td>
<td>data</td>
<td>BLOB</td>
<td>
The returned result from the function passed into
cache.render(), the result must be a string, and
may contain nulls.
</td>
</tr>
<tr>
<td></td>
<td>updated</td>
<td>INTEGER</td>
<td>
The time this item was rendered at, can be used to set
a minimum update frequency. This is used so that web
scrapers don't constantly trigger re-renders of the
index page.
</td>
</tr>
<tr>
<td></td>
<td>dirty</td>
<td>INTEGER</td>
<td>
Does this page need to be re-rendered the next time it
is called? For example, an author's story could have
multiple hits, which would require rerendering their
author page to show the new hit count, but we don't
actually need to do it until someone requests the
author page. In this case, we keep the old page around
to save time trying to clear it and potentially hit
sqlite's garbage collector.
</td>
</tr>
</table>
]]
local oldconfigure = configure
function configure(...)
ret.cache = db.sqlassert(sql.open_memory())-- Expose db for testing
--A cache table to store rendered pages that do not need to be
--rerendered. In theory this could OOM the program eventually and start
--swapping to disk. TODO
assert(ret.cache:exec([[
CREATE TABLE IF NOT EXISTS cache (
path TEXT PRIMARY KEY,
data BLOB,
updated INTEGER,
dirty INTEGER
);
]]))
stmnt_cache = assert(ret.cache:prepare([[
SELECT data
FROM cache
WHERE
path = :path AND
((dirty = 0) OR (strftime('%s','now') - updated) > 20)
;
]]))
stmnt_insert_cache = assert(ret.cache:prepare([[
INSERT OR REPLACE INTO cache (
path, data, updated, dirty
) VALUES (
:path, :data, strftime('%s','now'), 0
);
]]))
stmnt_dirty_cache = assert(ret.cache:prepare([[
UPDATE OR IGNORE cache
SET dirty = 1
WHERE path = :path;
]]))
return oldconfigure(...)
end
--[[ md
@name lua/cache
### cache.render
Render a page with cacheing.
The callback will be called with no arguments, and must return a string.
Parameters:
0. pagename - {{lua/string}} - A logical string to associate with this
rendered page, this must be passed exactly into render() in order
to (potentially) retrive the cached page.
0. callback - {{lua/function}} - A function that may be called,
if it is called, it is called with no arguments, and must return a string.
The returned string may have embedded nulls.
Returns:
0. {{lua/string}} - Either the return of the passed function, or the cached
string.
Example:
cache = require("cache")
func = function()
print("Called")
return "Hello, world!"
end
print(cache.render("/test",func)) -- prints "Called", then "Hello, world!"
print(cache.render("/test",func)) -- prints "Hello, world!"
print(cache.render("/test",func)) -- prints "Hello, world!"
]]
function ret.render(pagename,callback)
stmnt_cache:bind_names{path=pagename}
local err = db.do_sql(stmnt_cache)
if err == sql.DONE then
stmnt_cache:reset()
--page is not cached
elseif err == sql.ROW then
local data = stmnt_cache:get_values()
stmnt_cache:reset()
return data[1]
else --sql.ERROR or sql.MISUSE
error("Failed to check cache for page " .. pagename)
end
--We didn't have the paged cached, render it
local text = callback()
--And save the data back into the cache
stmnt_insert_cache:bind_names{
path=pagename,
data=text,
}
err = db.do_sql(stmnt_insert_cache)
if err == sql.ERROR or err == sql.MISUSE then
error("Failed to update cache for page " .. pagename)
end
stmnt_insert_cache:reset()
return text
end
--[[ md
@name lua/cache
### cache.dirty
Dirty a cached page, causing it to be re-rendered the next time it's
requested. Doesn't actually delete it or free memory, just sets its dirty bit.
If the page does not exists or has not been rendered yet, this function does
not error.
Parameters:
0. url - {{lua/string}} - `pagename` from the render function, the logical
string associcated with this rendered page.
No returns
Example:
cache = require("cache")
func = function()
print("Called")
return "Hello, world!"
end
print(cache.render("/test",func)) -- prints "Called", then "Hello, world!"
print(cache.render("/test",func)) -- prints "Hello, world!")
cache.dirty("/test")
print(cache.render("/test",func)) -- prints "Called", then "Hello, world!"
print(cache.render("/test",func)) -- prints "Hello, world!"
]]
function ret.dirty(url)
stmnt_dirty_cache:bind_names{
path = url
}
db.do_sql(stmnt_dirty_cache)
stmnt_dirty_cache:reset()
end
function ret.close()
end
return ret