--[[ 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

Schema for Cache

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.

cache
Attributes Field Type Description
Primary Key path TEXT The logical path this text was rendered at, before browser-specific headers (like accept-encoding) are applied
data BLOB The returned result from the function passed into cache.render(), the result must be a string, and may contain nulls.
updated INTEGER 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.
dirty INTEGER 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.
]] 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