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 ¶
- Variables
- func EnvInt(key string, def int) int
- type Cache
- func (c *Cache[T]) Delete(sessionID string)
- func (c *Cache[T]) DeleteWhere(match func(key string, value T) bool) int
- func (c *Cache[T]) Get(sessionID string) (*Lease[T], bool)
- func (c *Cache[T]) GetOrCreate(ctx context.Context, sessionID string, ctor Ctor[T]) (*Lease[T], error)
- func (c *Cache[T]) List() []T
- func (c *Cache[T]) WithReuseLog(fn func(sessionID string, v T)) *Cache[T]
- type Ctor
- type Entry
- type Lease
Constants ¶
This section is empty.
Variables ¶
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.
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 ¶
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 ¶
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 ¶
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 ¶
DeleteWhere atomically detaches every entry accepted by match. Outstanding leases keep their value alive until Release; idle entries close immediately.
func (*Cache[T]) Get ¶
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 ¶
WithReuseLog installs a callback invoked whenever GetOrCreate returns an existing entry. Returns the receiver for chaining.
type Ctor ¶
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.