TinyStore

Locks / Fences

Fences

A lock makes two holders at once rare; a fence makes a late one harmless. Every lease carries a fence, a number larger than every earlier lease's. A system that keeps the largest fence it has seen for a key refuses a write from a holder whose lease ran out while it was paused.

await summaries.hold(chatId, async lease => save(chatId, text, lease.fence))
  1. Worker A holds the chat's key with fence 7, then pauses: a long garbage collection.
  2. A's lease runs out. Worker B holds the key with fence 8 and saves its summary.
  3. A wakes and saves too. The database keeps the largest fence, 8, so it refuses A's write.

Checking a fence in SQL

A fence is a 128-bit number, too large for an SQLite integer. Written as 40 zero-padded digits, it compares as text in the same order:

migrations/0004_summary_fence.sql
alter table chats add column summary_fence text not null default '';
await summaries.hold(chatId, async lease => {
	const summary = await model.summarize(await history(chatId))
	const fence = lease.fence.toString().padStart(40, '0')
	const done = await db.exec`update chats set summary = ${summary}, summary_fence = ${fence}
		where id = ${chatId} and summary_fence < ${fence}`
	if (done.changes === 0) return // a later holder already wrote
})
async with summaries.hold(chat_id) as lease:
    summary = await model.summarize(await history(chat_id))
    fence = f"{lease.fence:040d}"
    await db.exec(
        "update chats set summary = ?, summary_fence = ? where id = ? and summary_fence < ?",
        summary, fence, chat_id, fence,
    )
err = summaries.Hold(ctx, chatID, func(ctx context.Context, lease *locks.Lease) error {
	summary, err := model.Summarize(ctx, history(ctx, chatID))
	if err != nil {
		return err
	}
	fence := fmt.Sprintf("%040s", lease.Fence)
	_, err = db.Exec(ctx, `update chats set summary = ?, summary_fence = ? where id = ? and summary_fence < ?`,
		summary, fence, chatID, fence)
	return err
})
summaries.hold(chat_id, |lease| -> Result<(), AppError> {
    let summary = model.summarize(&history(chat_id)?)?;
    let fence = format!("{:040}", lease.fence().get());
    db.exec(sql!(
        "update chats set summary = ?, summary_fence = ? where id = ? and summary_fence < ?",
        summary,
        &fence,
        chat_id,
        &fence
    ))?;
    Ok(())
})?;

The check and the write are one statement, so nothing can come between them.

Fences in each language

LanguageA fence is
TypeScripta bigint, every digit kept
Pythonan int
Goa locks.Fence of two uint64s, compared by Less, printed by String
Rustan ordered Fence, its value through get()
the wirea decimal string

What a fence promises

A fence is two numbers compared in order: the store's epoch in the high 64 bits, a count in the low. The count grows with every lease the store gives while it is open; the epoch is written to kv.db each time the store opens, as the larger of the time in milliseconds and the epoch before it plus one.

BetweenA later lease's fence is larger
two leases of one store while it is openalways
a store and itself after a crash or a restartalways (contract LOCK-02)
a store and one restored from a backuponly while the restoring machine's clock is past the epochs given since the backup

A semaphore's leases have no fence: three valid holders at once would refuse each other.

Fences outside the store

A fence protects only where something compares it. Send it to any system that can keep the largest one per key, a header your own service checks, a conditional write in object storage. A payment provider that compares nothing needs an idempotency key all the same: a lock makes two attempts at once rarer, and only the provider's own deduplication makes a charge happen once.

See also

Edit this page on GitHub Updated 19 October 2026

esc
↑↓ navigate ↵ open ⇥ filter 0 results