sessionmanager

package
v0.41.0 Latest Latest
Warning

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

Go to latest
Published: Jul 28, 2026 License: Apache-2.0 Imports: 24 Imported by: 0

Documentation

Overview

Package sessionmanager provides session lifecycle management.

This package implements the two-phase session creation pattern that bridges the MCP SDK's session management with the vMCP server's backend lifecycle:

  • Phase 1 (Generate): Creates a placeholder session with no context
  • Phase 2 (CreateSession): Replaces placeholder with fully-initialized MultiSession

The Manager type implements the server.SessionManager interface and is used by the server package.

Index

Constants

View Source
const (
	// MetadataKeyTerminated is the session metadata key that marks a placeholder
	// session as explicitly terminated by the client.
	MetadataKeyTerminated = "terminated"

	// MetadataValTrue is the string value stored under MetadataKeyTerminated
	// when a session has been terminated.
	MetadataValTrue = "true"
)

Variables

This section is empty.

Functions

This section is empty.

Types

type FactoryConfig added in v0.12.5

type FactoryConfig struct {
	// Base is the underlying session factory. Required.
	Base vmcpsession.MultiSessionFactory

	// OptimizerConfig is optional optimizer configuration.
	// When non-nil and OptimizerFactory is nil, New() creates the optimizer
	// factory from this config and returns a cleanup function.
	OptimizerConfig *optimizer.Config

	// OptimizerFactory is an optional pre-built optimizer factory.
	// If set, takes precedence over OptimizerConfig.
	// If nil and OptimizerConfig is also nil, the optimizer is disabled.
	OptimizerFactory func(context.Context, []mcpserver.ServerTool) (optimizer.Optimizer, error)

	// TelemetryProvider is the optional telemetry provider.
	// If non-nil, the optimizer factory (whether derived from OptimizerConfig or
	// supplied via OptimizerFactory) and workflow executors are wrapped with telemetry.
	TelemetryProvider *telemetry.Provider

	// CacheCapacity is the maximum number of live MultiSession entries held in
	// the node-local ValidatingCache. When the cache is full the least-recently-used
	// session is evicted (its backend connections are closed via onEvict). A value of
	// 0 uses defaultCacheCapacity (1000). Negative values are rejected by
	// sessionmanager.New.
	CacheCapacity int

	// AdvertiseFromCore signals that the advertised capability set is sourced from
	// the core (the Serve path), not from this factory's per-session aggregation.
	//
	// It is required whenever an optimizer is configured:
	//   - true:  New exposes the resolved factory via Manager.OptimizerFactory so
	//            the Serve layer builds a per-session optimizer over the core's
	//            tools — the single writer of the shared FTS5 store (the AC6
	//            no-double-index guarantee).
	//   - false: fine without an optimizer; with one, New rejects the config at
	//            construction, because the Serve layer discards the decorator's
	//            per-session tools (the optimizer would index the store yet serve
	//            nobody) and the Modern capability gate would fail open.
	// New resolves the optimizer factory and owns its store/cleanup. server.New
	// sets this unconditionally (server.go), so every in-tree composition
	// advertises from the core; the flag exists for direct-Serve embedders. The
	// decorator branch the false case used to select is now unreachable — its
	// deletion is tracked in #6103.
	AdvertiseFromCore bool
}

FactoryConfig holds the session factory construction parameters that the session manager needs to build its decorating factory. It is separate from server.Config to avoid a circular import between the server and sessionmanager packages.

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager bridges the domain session lifecycle (MultiSession / MultiSessionFactory) to the mcpcompat SDK's SessionIdManager interface.

It implements a two-phase session-creation pattern:

  • Generate(): called by SDK during initialize without context; stores an empty placeholder via storage.
  • CreateSession(): called from OnRegisterSession hook once context is available; calls factory.MakeSessionWithID(), then persists the session metadata to storage.

Storage split

MultiSession holds live in-process state (backend HTTP connections, routing table) that cannot be serialized or recovered across processes. A separate in-process multiSessions map holds the authoritative MultiSession reference for this pod. The pluggable SessionDataStorage (LocalSessionDataStorage or RedisSessionDataStorage) carries only the lightweight, serialisable session metadata required for TTL management, Validate(), and cross-pod visibility.

Because MultiSession objects are node-local, horizontal scaling requires sticky routing when session-affinity is desired. When Redis is used as the session-storage backend the metadata is durable across pod restarts, and the live MultiSession can be re-created via factory.RestoreSession() on a cache miss.

TODO: Long-term, the cache and storage should be layered behind a single interface so the session manager does not need to coordinate between them. Reads would go through the cache (handling misses, singleflight, and liveness transparently); writes go to storage; caching is an implementation detail hidden from the caller.

func New

func New(
	storage transportsession.DataStorage,
	cfg *FactoryConfig,
	backendRegistry vmcp.BackendRegistry,
) (*Manager, func(context.Context) error, error)

New creates a Manager backed by the given SessionDataStorage and backend registry. It builds the decorating session factory from cfg, wiring the optimizer and composite tool layers internally.

An optimizer (FactoryConfig.OptimizerFactory or OptimizerConfig) requires FactoryConfig.AdvertiseFromCore; New rejects the combination otherwise. The guard makes a non-nil OptimizerFactory() a faithful "optimizer enabled" signal, which the Modern capability gate in pkg/vmcp/server/modern_gate.go depends on.

The returned cleanup function releases any resources allocated during construction (e.g. the optimizer's SQLite store). Callers must invoke it on shutdown. If no cleanup is needed, a no-op function is returned.

func (*Manager) CreateSession

func (sm *Manager) CreateSession(
	ctx context.Context,
	sessionID string,
	sink vmcpsession.ListChangedSink,
) (vmcpsession.MultiSession, error)

CreateSession is Phase 2 of the two-phase creation pattern.

It is called from the OnRegisterSession hook once the request context is available. It:

  1. Resolves the caller identity from the context.
  2. Lists available backends from the registry.
  3. Calls MultiSessionFactory.MakeSessionWithID() to build a fully-formed MultiSession (which opens real HTTP connections to each backend).
  4. Persists session metadata to storage and caches the live MultiSession in the node-local map.

sink, when non-nil, is threaded to MultiSessionFactory.MakeSessionWithID so every backend connector opened for this session can report an asynchronous backend notification (#5748); pass nil to disable that consumption.

The returned MultiSession can be retrieved later via GetMultiSession().

func (*Manager) DecorateSession added in v0.12.3

func (sm *Manager) DecorateSession(sessionID string, fn func(sessiontypes.MultiSession) sessiontypes.MultiSession) error

DecorateSession retrieves the MultiSession for sessionID, applies fn to it, and stores the result back. Returns an error if the session is not found or has not yet been upgraded from placeholder to MultiSession.

storage.Update is the concurrency guard. If it returns (false, nil), the session was deleted; the cache entry will be evicted on the next Get when checkSession detects ErrSessionNotFound.

func (*Manager) Generate

func (sm *Manager) Generate() string

Generate implements the SDK's SessionIdManager.Generate().

Phase 1 of the two-phase creation pattern: creates a unique session ID, stores an empty placeholder via storage, and returns the ID to the SDK. No context is available at this point.

The placeholder is replaced by CreateSession() in Phase 2 once context is available via the OnRegisterSession hook.

func (*Manager) GetMultiSession

func (sm *Manager) GetMultiSession(ctx context.Context, sessionID string) (vmcpsession.MultiSession, bool)

GetMultiSession retrieves the fully-formed MultiSession for a given SDK session ID. Returns (nil, false) if the session does not exist or has not yet been upgraded from placeholder to MultiSession.

On a cache hit, liveness is confirmed via storage.Load (which also refreshes the Redis TTL). On a cache miss, the session is restored from storage via factory.RestoreSession, enabling cross-pod session recovery when Redis is used as the storage backend.

The context is propagated to storage and restore operations using context.WithoutCancel so caller identity (e.g. *auth.Identity in ctx) reaches the backend Initialize handshake during cross-pod session restore.

func (*Manager) NotifyBackendExpired added in v0.16.0

func (sm *Manager) NotifyBackendExpired(sessionID, workloadID string, metadata map[string]string)

NotifyBackendExpired updates session metadata in storage to reflect that the backend identified by workloadID is no longer connected. It removes the per-backend session ID key and rebuilds MetadataKeyBackendIDs so that a cross-pod RestoreSession call does not attempt to reconnect to the expired backend session.

The caller supplies the session metadata it already holds (e.g. from MultiSession.GetMetadata). Passing nil metadata is treated as "no metadata available" and is a silent no-op, avoiding a redundant storage round-trip.

After a successful storage update, the cached entry is not immediately evicted. On the next GetMultiSession call, checkSession detects that the stored MetadataKeyBackendIDs differs from the cached session's value, evicts the stale entry via onEvict, and triggers RestoreSession with the updated metadata. On storage error, no eviction occurs and the caller retries on the next access.

This is a best-effort operation. If the session key is absent from storage (terminated or expired), updateMetadata's SET XX is a no-op. Storage errors are logged but not returned.

func (*Manager) OptimizerFactory added in v0.30.1

func (m *Manager) OptimizerFactory() func(context.Context, []mcpserver.ServerTool) (optimizer.Optimizer, error)

OptimizerFactory returns the resolved (telemetry-wrapped) optimizer factory, or nil exactly when the optimizer is disabled: New rejects an optimizer without FactoryConfig.AdvertiseFromCore, so nil-ness here is a faithful "optimizer enabled" signal (the Modern capability gate in pkg/vmcp/server/modern_gate.go depends on it).

It is consumed by the Serve path, which builds a per-session optimizer over the core's advertised tool set. The optimizer's shared store and its cleanup remain owned by this Manager (the cleanup function returned from New). server.New sets AdvertiseFromCore unconditionally, so this getter is live on every composition path.

func (*Manager) Terminate

func (sm *Manager) Terminate(sessionID string) (isNotAllowed bool, err error)

Terminate implements the SDK's SessionIdManager.Terminate().

The two session types are handled asymmetrically to prevent a race condition where client termination during the Phase 1→Phase 2 window could resurrect sessions with open backend connections:

  • MultiSession (Phase 2): the storage key is deleted. The node-local cache self-heals on the next Get: checkSession detects ErrSessionNotFound, evicts the entry, and onEvict closes backend connections. After deletion Validate() reports the absent key as (isTerminated=true, nil), so the next request — on this or any other replica — is rejected with a definitive 404 and the client re-initializes instead of retrying the dead session.

  • Placeholder (Phase 1): the session is marked terminated=true and left for TTL cleanup. This prevents CreateSession() from opening backend connections for an already-terminated session (see fast-fail check in CreateSession). The terminated flag also lets Validate() return (isTerminated=true, nil) during the window between termination and TTL expiry — the same terminated response the deleted case now gives.

Returns (isNotAllowed=false, nil) on success; client termination is always permitted.

func (*Manager) Validate

func (sm *Manager) Validate(sessionID string) (isTerminated bool, err error)

Validate implements the SDK's SessionIdManager.Validate().

Returns (isTerminated=true, nil) for a session that is definitively gone: explicitly terminated (placeholder marked terminated=true) OR absent from storage (a full MultiSession is deleted on Terminate, TTL-expired, or never existed). All of these must reject the request as a hard termination so the client re-initializes rather than retrying a dead session.

Returns (false, error) only for a genuine, transient storage error (e.g. the backing store is unreachable) — the caller should treat this as retryable and NOT drop session state.

Returns (false, nil) for valid, active sessions.

This distinction is load-bearing: the streamable transport maps (isTerminated=true) to HTTP 404 (-> client ErrSessionTerminated -> re-init) and a non-terminated error to HTTP 503 (retryable). Reporting a genuinely-gone session as an error would surface as 503 and make a client whose session was terminated on another replica retry the dead session forever. Terminate still DELETES the key (unchanged), so the resurrection-race guarantee is preserved; only the way an absent key is reported here changes.

Jump to

Keyboard shortcuts

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