TinyStore

Locks / Leases over a connection

Leases over a connection

A holder in the store's own process cannot die apart from the store: its key is let go when its function returns, panics, or the process ends with the store. A holder over a connection can: its process may freeze or crash while the store goes on. So a lease given over a connection lasts its ttl from its last renewal, and the SDK renews it while the function runs.

const summaries = store.lock('summaries', { ttl: '30s' })

await summaries.hold(chatId, async lease => {
	const text = await model.summarize(messages, { signal: lease.signal }) // aborted if the lease is lost
	await saveSummary(chatId, text, lease.fence)
})
summaries = store.lock("summaries", ttl="30s")

async with summaries.hold(chat_id) as lease:  # a lost lease cancels the block at its next await
    text = await model.summarize(messages)
    await save_summary(chat_id, text, lease.fence)
summaries, err := locks.OpenLock(store, "summaries", locks.TTL(30*time.Second))

err = summaries.Hold(ctx, chatID, func(ctx context.Context, lease *locks.Lease) error {
	text, err := model.Summarize(ctx, messages) // ctx ends when the lease is lost
	if err != nil {
		return err
	}
	return saveSummary(ctx, chatID, text, lease.Fence)
})
let summaries = store.lock("summaries").open()?;

summaries.hold(chat_id, |lease| {
    let text = model.summarize(&messages)?;
    if lease.lost() {
        return Err(Error::new(ErrorKind::Lost, "the summary was late")); // revoked at maxHold
    }
    save_summary(chat_id, &text, lease.fence())
})?;

How a lease is kept

StepWhat happens
the key is giventhe server answers with the lease's id, its fence and its maxHold
every third of ttlthe SDK renews the lease, from the holder's own process
a renewal fails oncenot yet a loss: the SDK tries again until the deadline
the deadline passesthe holder counts the lease lost; the store gives the key on
the function endsthe SDK lets the key go, naming the lease
  • The holder's deadline is the time it sent the last renewal that succeeded, plus ttl, less a margin, so the holder gives up before the store does.
  • A lease that ran out is never renewed (contract LOCK-05): holding the key again is a new lease with a new fence.
  • A connection that drops leaves the lease to its deadline, and the SDK renews it by its id on the next connection.
  • A program that froze stops renewing, since renewals come from its own process: its key goes to the next in line within ttl.

ttl is 10 seconds unless given, from 1 second to an hour. Short enough that a crashed holder's key returns soon; long enough that a garbage collection pause or a slow network does not lose a lease that is still working.

When a lease is lost

A lease that ran out, or was revoked at maxHold, ends at that moment:

LanguageThe function hears it
TypeScriptlease.signal aborts: pass it to fetch and the model call
Pythonthe block is cancelled at its next await
Gothe function's ctx ends
Rustlease.lost() turns true

Once the function ends, hold fails lost, even if the function succeeded, so the caller learns that another holder may have run beside it.

The store cannot stop code that ignores the signal: a call the client already made goes on. That is what fences are for: the system it writes to refuses the late write.

Exactly one grant

A request to hold carries an id of its own. If the answer is lost on the way back, the SDK asks again with the same id, on any connection, and the store answers the lease it already gave instead of giving the key twice. The lease's own id, 128 random bits only its holder learns, is what renewing and letting go name: a fence, which a holder hands to others, proves nothing.

A connection admitted to write may hold keys; one that only reads may not.

A store that restarts

A store that opens again holds no lease. A client that held one through its server learns at its next renewal that it lost it, and its next fence, from a new epoch, is larger than every fence before the restart (contract LOCK-02).

See also

Edit this page on GitHub Updated 19 October 2026

esc
↑↓ navigate ↵ open ⇥ filter 0 results