mirror of
https://github.com/mailscope/kumomta.git
synced 2026-08-25 05:38:18 +00:00
122 lines
4.8 KiB
Markdown
122 lines
4.8 KiB
Markdown
# kumo.memoize
|
|
|
|
```lua
|
|
kumo.memoize(FUNCTION, { PARAMS })
|
|
```
|
|
|
|
This function allows you to create functions that cache the result of a
|
|
computation. This technique is known as
|
|
[Memoization](https://en.wikipedia.org/wiki/Memoization).
|
|
|
|
Here's a simple example; we have a sqlite lookup database that we intend to use
|
|
for authentication, and we have already defined a helper function for that:
|
|
|
|
```lua
|
|
local sqlite = require 'sqlite'
|
|
|
|
-- Consult a hypothetical sqlite database that has an auth table
|
|
-- with user and pass fields
|
|
function sqlite_auth_check(user, password)
|
|
local db = sqlite.open '/path/to/auth.db'
|
|
local result = db:execute(
|
|
'select user from auth where user=? and pass=?',
|
|
user,
|
|
password
|
|
)
|
|
-- if we return the username, it is because the password matched
|
|
return result[1] == user
|
|
end
|
|
|
|
kumo.on('smtp_server_auth_plain', function(authz, authc, password)
|
|
return sqlite_auth_check(authc, password)
|
|
end)
|
|
```
|
|
|
|
Let's say, for the sake of example, that the query is very expensive: perhaps
|
|
the IO cost is high and the query rate is also high and we want to save the iops
|
|
for other work in the system.
|
|
|
|
It would be nice if we could cache the lookup for some period of time.
|
|
We can use `kumo.memoize` for this:
|
|
|
|
```lua
|
|
local sqlite = require 'sqlite'
|
|
|
|
function sqlite_auth_check(user, password)
|
|
local db = sqlite.open '/tmp/auth.db'
|
|
local result = db:execute(
|
|
'select user from auth where user=? and pass=?',
|
|
user,
|
|
password
|
|
)
|
|
-- if we return the username, it is because the password matched
|
|
return result[1] == user
|
|
end
|
|
|
|
-- This creates a new function called `cached_sqlite_auth_check`
|
|
-- that remembers the results for a given set of parameters for up
|
|
-- to 5 minutes or up to 100 different sets of parameters
|
|
cached_sqlite_auth_check = kumo.memoize(sqlite_auth_check, {
|
|
name = 'sqlite_auth',
|
|
ttl = '5 minutes',
|
|
capacity = 100,
|
|
})
|
|
|
|
kumo.on('smtp_server_auth_plain', function(authz, authc, password)
|
|
return cached_sqlite_auth_check(authc, password)
|
|
end)
|
|
```
|
|
|
|
`kumo.memoize` takes a function or a lambda and wraps it up with some logic
|
|
that will internally cache the result for the same set of parameters, and
|
|
returns a new function that encodes that caching logic. The return value is
|
|
the *memoized function*.
|
|
|
|
The parameters it accepts are:
|
|
|
|
* *FUNCTION* - the function or lambda which will be called when there is a cache miss.
|
|
When it is called, it will be passed the parameters that were passed to the *memoized function* in order to populate the cache.
|
|
* *PARAMS* is a required lua table with the following fields, all of which are required:
|
|
* `name` - the name for the cache. You should create one name per function/purpose.
|
|
* `ttl` - the Time To Live for cache entries; how long a previously computed
|
|
value should remain valid. The duration is expressed as a string like `5
|
|
minutes` or `10 seconds`.
|
|
* `capacity` - the total number of results to retain in the cache. When a new
|
|
entry needs to be inserted, if the cache is at capacity, the eldest entry
|
|
will be evicted to make space.
|
|
* `invalidate_with_epoch` - optional boolean that defaults to `false`.
|
|
If true, anything that bumps the config epoch (eg: config file changes,
|
|
TSA config overrides and so on) will invalidate the cache. {{since('2025.03.19-1d3f1f67', inline=True)}}
|
|
* `populate_timeout` - optional duration string. The effective default
|
|
value is `120 seconds`. Specifies how long to allow the cache population
|
|
function to run before generating a timeout error. {{since('2025.05.06-b29689af', inline=True)}}
|
|
* `retry_on_populate_timeout` - optional boolean that defaults to `false`.
|
|
If `true`, if the `populate_timeout` is reached, then instead of generating
|
|
an error, memoize will retry the population attempt.
|
|
{{since('2025.05.06-b29689af', inline=True)}}
|
|
* `allow_stale_reads` - optional boolean that defaults to `false`. If
|
|
`true` then `invalidate_with_epoch` will be assumed to `false` and a
|
|
cache lookup will be allowed to return with the last populated value in
|
|
the case that the item has expired and the cache population takes longer
|
|
than the `populate_timeout`. {{since('2025.05.06-b29689af', inline=True)}}
|
|
|
|
In the example above calling:
|
|
|
|
```lua
|
|
cached_sqlite_auth_check('daniel', 'tiger')
|
|
```
|
|
|
|
the first time would be a cache miss, because no calls have yet been made with `{'daniel', 'tiger'}` as parameters, so the memoized function would internally call:
|
|
|
|
```lua
|
|
sqlite_auth_check('daniel', 'tiger')
|
|
```
|
|
|
|
The next time that `cached_sqlite_auth_check('daniel', 'tiger')` is called there
|
|
is a cache hit and the previously computed result would be returned, provided that
|
|
5 minutes have not expired since the first call.
|
|
|
|
When the value expires, another call to `sqlite_auth_check('daniel', 'tiger')` will
|
|
be made to determine the value.
|
|
|