Documentation / guides
Garbage collection
Collect expired Session data under explicit retention rules.
Sessionstore GC reclaims only orphaned offload blobs. It is not a journal compaction operation and it never deletes ledger history. Because an upload is durable before its ledger pointer, a crash can leave an unreferenced blob; GC scans the ledger for live pointers and sweeps the remainder.
Object GC contract
The exact public types are:
type GCResult struct {
Scanned int
Referenced int
Deleted int
DeletedKeys []string
}
func (s *Store) OpenObjectGC(
id uuid.UUID, lease journal.Lease,
) (*ObjectGC, error)
func (g *ObjectGC) GC(context.Context) (GCResult, error)
OpenObjectGC rejects a nil lease with *sessionstore.NilLeaseError. The GC
does not acquire or release ownership; the composition root supplies the lease
and remains responsible for its lifetime.
Live-set sweep
One pass performs these steps:
- Check
lease.Valid()andlease.Lost(); otherwise return*GCLeaseNotHeldErrorand delete nothing. - Read every ledger envelope under
sessions/<uuid>and collect referenced blob keys fromblobptrframes. - List
sessions/<uuid>/blobs/. - Delete listed keys absent from the complete live set, in sorted order.
- Recheck lease ownership before each delete. A loss stops the sweep.
%%{init: {"theme":"dark"}}%%
flowchart TD
A[GC(ctx)] --> B{lease still held?}
B -- no --> X[GCLeaseNotHeldError, no deletes]
B -- yes --> C[scan every ledger frame]
C -->|scan/decode error| Y[GCScanError, no deletes]
C --> D[list blob prefix]
D -->|list error| Z[GCListError, no deletes]
D --> E[set difference: listed minus referenced]
E --> F{lease before each delete?}
F -- no --> Q[stop with GCLeaseNotHeldError]
F -- yes --> G[delete orphan]
G --> H[GCResult]
GCScanError and GCListError fail closed because an incomplete live set is
unsafe. A delete failure returns *GCDeleteError with the blob key; previous
deletes are not rolled back, so operators can run another pass.
Lease and shutdown
Storage has no per-blob modification time, so this GC has no grace period. The lease guard alone is not enough if an active append can upload between the scan and sweep. The caller must serialize GC with every journal append, normally by using the runtime’s offload-GC admission wrapper while the session is idle or owned by the same writer.
The session runtime stops and joins its offload-GC runner before appending
SessionStopped and releasing the session lease. Do not run a standalone GC
against a live session while the runtime can still offload frames.
Workspace reference scan
Sessionstore also exposes operator-facing workspace discovery:
func (s *Store) WorkspaceLiveRefs(
context.Context, []uuid.UUID,
) (map[workspacestore.Ref]struct{}, error)
func (s *Store) WorkspaceCheckpointBySeq(
context.Context, uuid.UUID, uint64,
) (CheckpointSummary, bool, error)
func (s *Store) WorkspaceCheckpointByTurn(
context.Context, uuid.UUID, uuid.UUID,
) (CheckpointSummary, bool, error)
WorkspaceLiveRefs scans every retained session’s public event history and
keeps refs from both WorkspaceCheckpointed and WorkspaceRestored. It only
discovers the live set; actual workspace collection remains an explicit
operator action serialized against snapshot writers. Incomplete scans return
*WorkspaceJournalScanError and no set is returned.
GC example
lease, err := store.AcquireLease(ctx, id)
if err != nil {
return err
}
defer lease.Release(context.Background())
gc, err := store.OpenObjectGC(id, lease)
if err != nil {
return err
}
result, err := gc.GC(ctx)
if err != nil {
var notHeld *sessionstore.GCLeaseNotHeldError
if errors.As(err, ¬Held) {
return fmt.Errorf("ownership changed; retry after reopening: %w", err)
}
return err
}
log.Printf("scanned=%d kept=%d deleted=%d", result.Scanned, result.Referenced, result.Deleted)
The runtime normally owns the lease release, so a caller using
Rig.NewSession or RestoreSession should not acquire a second lease just to
run the configured GC runner.