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))- Worker A holds the chat's key with fence 7, then pauses: a long garbage collection.
- A's lease runs out. Worker B holds the key with fence 8 and saves its summary.
- 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:
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
| Language | A fence is |
|---|---|
| TypeScript | a bigint, every digit kept |
| Python | an int |
| Go | a locks.Fence of two uint64s, compared by Less, printed by String |
| Rust | an ordered Fence, its value through get() |
| the wire | a 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.
| Between | A later lease's fence is larger |
|---|---|
| two leases of one store while it is open | always |
| a store and itself after a crash or a restart | always (contract LOCK-02) |
| a store and one restored from a backup | only 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
- Locks: the engine's overview.
- Leases over a connection: when a lease runs out.
- One summary per chat at a time in the Handbook.