Skip to documentation
Documentation navigation

Documentation navigation

Documentation / guides

Leases

Protect single-writer ownership of a live Session journal.

developer

The session journal is single-writer. A lease grants one live owner and an epoch that fences stale owners at the ledger CAS boundary. The composition root acquires and releases the lease; the journal and object GC receive only the narrow ownership view they need.

Lease contract

The exact public contract is:

type Lease interface {
	SessionID() uuid.UUID
	Epoch() uint64
	Valid() bool
	Lost() <-chan struct{}
	Release(context.Context) error
}

Epoch increases across handovers. Valid is a fast non-blocking state check; Lost closes when the lease is released or overtaken. Release is owned by the holder and is idempotent. The journal itself never releases its lease.

Fencing

Store.OpenJournal reads the current tip, appends a FenceRecord carrying the lease epoch at that tip, and marks the journal ready only after the fence commits. Every later append uses a CAS against the tracked tip. A stale writer can pass its local Valid check and still lose the backend CAS; the hard fence then returns *journal.AppendError.

%%{init: {"theme":"dark"}}%%
sequenceDiagram
    participant A as old owner
    participant B as new owner
    participant L as lease backend
    participant D as ledger

    A->>L: epoch 4
    B->>L: acquire epoch 5
    B->>D: append Fence{epoch:5}
    A->>D: CAS at stale tip
    D-->>A: conflict / AppendError
    B->>D: append under tracked tip

The fence is an internal record. Ordinary event replay does not expose it; full record replay does, so maintenance and index hydration can see ownership boundaries.

Acquisition and release

The store adapter exposes:

func (s *Store) AcquireLease(context.Context, uuid.UUID) (journal.Lease, error)
func (s *Store) OpenJournal(context.Context, uuid.UUID, journal.Lease) (journal.SessionJournal, error)

An already-held session returns *journal.LeaseHeldError with the session ID and holder epoch. A nil lease passed to OpenJournal returns *sessionstore.NilLeaseError. A successful rig construction installs lease.Release on the session. Shutdown releases workspace-root ownership first, then the session lease, after the last durable append.

Lease loss

Once Valid is false or Lost is closed, the journal refuses new appends with *journal.JournalLeaseLostError, which unwraps to *journal.LeaseLostError. The journal does not re-read the tip or try to recover in place. Stop using the stale writer and let a new lifecycle acquire a new lease and fence the stream.

var lost *journal.JournalLeaseLostError
if errors.As(err, &lost) {
	log.Printf("writer lost epoch for session %s", lost.SessionID)
	return err
}

Source and proof

← back to documentation