Documentation / guides
Leases
Protect single-writer ownership of a live Session journal.
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
}