TessariDBDocs
  • 0.34.2-betalatest
  • 0.34.1-beta
  • 0.34.0-beta
  • 0.33.4-beta
  • 0.33.3-beta
  • 0.33.2-beta
  • 0.33.1-beta
  • 0.33.0-beta
  • 0.32.0-beta

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 ABSENT writes only when there is no key,
  • IF PRESENT only when there is one,
  • IF = value only 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 };
Expiry, counters and locks — TessariDB