223 lines
5.4 KiB
Lua
223 lines
5.4 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 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
|