Help us shape the future of Go! Take the 2026 Go Developer Survey

sessioncache

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 29, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

Documentation

Overview

Package sessioncache is the single implementation of the "per-session agent cache" pattern used by the acp path.

The cache needs: a bounded map of sessionID -> agent, singleflight deduplication on create, last-access LRU eviction that skips running entries, and release-on-delete / release-on-evict semantics. Keeping two copies of this logic (one per agent path, before the paths converged) made the concurrency- sensitive invariants drift between paths — any fix to the create-race double-check, the evict-skip rule, or the close-on-race cleanup had to be threaded through both files. This package collapses both.

Leases

Get and GetOrCreate hand back a Lease[T] rather than the bare value. While at least one Lease for an entry is alive, eviction and Delete will not Close() the underlying value: Delete removes the entry from the map immediately but defers the actual Close to the last Release; eviction skips entries with outstanding leases altogether. This closes the close-under-use window between "caller obtained agent" and "caller invoked Run" — without it, a concurrent GetOrCreate that triggers eviction could pick the just-checked-out agent (its IsRunning() is still false because Run() has not entered its body yet) and Close() it before the caller's Run() begins.

Callers MUST treat Release as `defer lease.Release()` immediately after acquisition; double Release is safe (idempotent), forgetting Release pins the entry forever.

The cache is parameterised by the agent type T, which must satisfy Entry (IsRunning + Close). Constructors are injected as closures, so each path can pass whatever extra parameters it needs (workdir, model config, job id, ...) without widening the cache API.

Index

Constants

This section is empty.

Variables

View Source
var ErrCapacityExceeded = errors.New("session cache capacity exceeded")

ErrCapacityExceeded is returned by GetOrCreate when the cache is at capacity and no idle entry (non-running, no outstanding leases) is available for eviction. Matches the error surfaced by the old per-path managers so existing callers (tests, UI error paths) keep working.

View Source
var ErrEntryDetached = errors.New("sessioncache: entry detached during create handoff")

ErrEntryDetached is returned by GetOrCreate when a concurrent Delete removed the freshly-created (or reused) entry between insertion and the per-waiter ref acquisition. The newly-built agent is still constructable, but it has been Closed by the racing Delete; the caller should retry or surface the error. This is a rare race because Delete for a session typically happens at session shutdown, not concurrently with create.

Functions

func EnvInt

func EnvInt(key string, def int) int

EnvInt reads an integer env var with a default fallback. Exposed so the acp manager can share capacity-parsing instead of re-implementing it. Treats empty / non-numeric / non-positive as def.

Types

type Cache

type Cache[T Entry] struct {
	// contains filtered or unexported fields
}

Cache is a bounded singleflight-dedup'd LRU keyed by sessionID. The zero value is NOT usable — use New.

func New

func New[T Entry](cap int) *Cache[T]

New returns a fresh cache with capacity cap (0 or negative disables the bound entirely) and the standard 60s create timeout.

func (*Cache[T]) Delete

func (c *Cache[T]) Delete(sessionID string)

Delete removes the entry for sessionID. If no leases are outstanding, the underlying value is Close()d immediately; otherwise the close is deferred to the last Release. No-op when the id is not cached.

func (*Cache[T]) DeleteWhere

func (c *Cache[T]) DeleteWhere(match func(key string, value T) bool) int

DeleteWhere atomically detaches every entry accepted by match. Outstanding leases keep their value alive until Release; idle entries close immediately.

func (*Cache[T]) Get

func (c *Cache[T]) Get(sessionID string) (*Lease[T], bool)

Get returns a Lease for the cached entry without creating it. Touches lastAccess on hit so a bare Get counts as "recent use" for LRU purposes (matches the old managers). Caller MUST Release the lease when done.

func (*Cache[T]) GetOrCreate

func (c *Cache[T]) GetOrCreate(ctx context.Context, sessionID string, ctor Ctor[T]) (*Lease[T], error)

GetOrCreate returns a Lease for the cached entry, creating it via ctor on miss. Concurrent callers for the same id share a single ctor invocation (singleflight). Create runs under a 60s context detached from the caller's cancellation, but the WAIT for that create respects the caller's ctx, so a Stop while the agent is being built returns immediately rather than blocking up to 60s.

The returned Lease MUST be Released — typically via `defer lease.Release()` — once the caller is done using Value. While at least one Lease is outstanding, the entry will not be Closed by eviction or Delete.

Why singleflight returns *entryBox[T] (not *Lease[T])

singleflight.Do shares ONE return value across all waiters for the same key. If the inner function returned a *Lease[T] (with refs pre-bumped to 1), every concurrent waiter would receive the SAME *Lease pointer. Lease.Release uses a lease-local atomic.Bool to guard releaseOne, so only the first caller's Release would decrement refs — refs would drop to 0 while N-1 callers still held the same pointer and used Value, allowing Delete or eviction to Close the underlying agent under them. Returning the box and letting each waiter independently acquireRef + construct its own Lease in its own goroutine fixes the close-under-use race.

func (*Cache[T]) List

func (c *Cache[T]) List() []T

List snapshots the current values. Callers get a new slice they can iterate without holding the cache lock. List is informational — callers MUST NOT retain the returned values past the call, since no leases are issued and the entries can be evicted at any time.

func (*Cache[T]) WithReuseLog

func (c *Cache[T]) WithReuseLog(fn func(sessionID string, v T)) *Cache[T]

WithReuseLog installs a callback invoked whenever GetOrCreate returns an existing entry. Returns the receiver for chaining.

type Ctor

type Ctor[T Entry] func(ctx context.Context) (T, error)

Ctor is invoked under singleflight on cache miss. A non-nil error aborts the insert and is returned to the caller unwrapped.

type Entry

type Entry interface {
	IsRunning() bool
	Close()
}

Entry is the contract each cached value must satisfy. Both *ACPAgent and *Quartet already implement these methods.

type Lease

type Lease[T Entry] struct {
	Value T
	// contains filtered or unexported fields
}

Lease is a borrowed reference to a cached value. Hold it for the duration of any operation on Value; the entry will not be Closed by eviction or Delete while at least one Lease is outstanding. Release MUST be called exactly once per Lease (typically via defer); calling it more than once is a no-op.

func (*Lease[T]) Release

func (l *Lease[T]) Release()

Release returns the lease to the cache. Idempotent — extra Release calls are silently ignored, so `defer lease.Release()` next to an explicit Release() in an error path is safe.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL