Expiry, counters and locks
A key that expires, a counter that never loses an increment, a write that happens only if, and a lock that frees itself.
The things a cache is usually run for — sessions that time out, counters, rate limits, locks — are four statements on a space. Each one is a write in the same transaction as everything else in the database, and each behaves the same on the memory backend and the disk one.
DEFINE NAMESPACE app;
USE NAMESPACE app;
DEFINE DATABASE web;
USE DATABASE web;
DEFINE SPACE sessions;
DEFINE SPACE counters;
DEFINE SPACE locks;
A key that expires
SET sessions:'abc' = { user: 'ada' } EXPIRE 30m;
RETURN TTL sessions:'abc';
A duration a little under thirty minutes. EXPIRE takes a duration from now
or a datetime:
SET sessions:'until' = 'x' EXPIRE datetime '2099-01-01T00:00:00Z';
Once the instant passes, the key is answered by no read of its space — GET,
KEYS, a SELECT and a read an index serves alike. Nothing has to run first:
the instant is written into the key's version, and every reader compares it with
its own clock. A follower hides an expired key exactly as the node that wrote it
does, and a restart changes nothing.
TTL tells three states apart, which is the point of having three:
SET sessions:'forever' = 'x';
RETURN TTL sessions:'forever';
NULL — the key is there and never expires.
RETURN TTL sessions:'nobody';
NONE — there is no such key.
Moving and clearing an expiry
EXPIRE sessions:'forever' 10m;
PERSIST sessions:'abc';
RETURN TTL sessions:'abc';
EXPIRE key when sets or moves the expiry of a key that exists and answers
true, or false when there is no key. PERSIST makes a key stop expiring and
answers whether it had an expiry to clear. The last line answers NULL.
A SET replaces the whole version, so a SET without EXPIRE clears the
expiry the key had. That is easy to do by accident in code that refreshes a
value and forgets to repeat the clause.
An expiry that is not in the future is refused and writes nothing:
SET sessions:'late' = 'x' EXPIRE datetime '2000-01-01T00:00:00Z';
InvalidExpiry. An EXPIRE statement whose moment has already passed is
different: it removes the key, which is what it asked for.
Where the bytes go
Removing an expired key's bytes is the node's housekeeping, and it happens through the log: each expiring version has an entry in an expiry index written in the same batch as the key, and what has expired is deleted like any other delete — replicated to followers, fenced by leadership, and visible on the change feed. An answer never waits for it.
A clock stepped forward expires keys early and one stepped back keeps them late. Neither is corrected here: keep the nodes' clocks synchronised.
A counter that never loses an increment
INCR counters:'hits';
INCR counters:'hits' BY 10;
11. INCR adds to a number and answers the result. A missing key counts from
zero, an expiry the key had is kept — so INCR on a key set with EXPIRE 1m
is a rate-limit window — and a key holding anything but a number is refused:
SET counters:'name' = 'ada';
INCR counters:'name';
NotArithmetic.
The read and the write are one transaction, so two writers cannot both read 10
and both write 11. Run as a statement of its own, an increment that loses the
race is run again on a fresh snapshot for up to a second before the ordinary
retriable conflict is answered, so four writers hammering one counter land every
increment and none of them is refused. Inside BEGIN … COMMIT a conflict is the
transaction's, as always.
A write that happens only if
SET locks:'report' = 'worker-7' IF ABSENT EXPIRE 30s;
SET locks:'report' = 'worker-9' IF ABSENT EXPIRE 30s;
true, then false. A conditional SET answers whether it wrote:
IF ABSENTwrites only when there is no key,IF PRESENTonly when there is one,IF = valueonly when the key holds that value — a compare-and-set.
The first line above is a lock that frees itself: whoever wrote it holds it
for thirty seconds, and a holder that crashes cannot keep it longer. IF and
EXPIRE may come in either order.
SET locks:'report' = 'free' IF = 'worker-7' EXPIRE 1ms;
true — the holder handing the lock back, which it can do only while the key
still says it holds it. A worker whose lock already expired and was taken by
someone else writes nothing, and is told so with false.
The EXPIRE is what makes it a hand-back. A SET replaces the whole version, so
one without EXPIRE would leave a key that never expires: TTL answers NULL,
and every later IF ABSENT answers false — the lock could never be taken
again. With it, the key is gone a moment later and the next taker's IF ABSENT
answers true. DEL takes no condition, so it is not a safe hand-back: after
the lease lapsed it would delete the next holder's lock.
The words stay yours
expire, persist and ttl are not reserved. A field or a table may still be
called that:
DEFINE COLLECTION ttl;
CREATE ttl = { expire: 1, persist: true };