State model
Separate four kinds of state. Script-style “current simulation” globals often pass local tests and fail when one nanoHUB session process serves concurrent MCP sessions and background tasks.
Module globals
Keep only immutable configuration, registries built at import time, and bounded
shared caches in module globals. Protect mutable caches with locks because
async_tool handlers run in background threads. Include cache size limits,
expiry, and invalidation. Never store a global current user, current run,
current working directory, request context, or access token.
Run state
Persist scientific inputs and outputs beneath an application-owned run root.
Return an opaque run_handle; resolve it through the confinement helper in
security.md for every operation. Put status in run-local files or a durable
store so readers do not depend on which worker thread handled the request.
Record ownership separately when runs are user-specific. A handle is a locator, not authorization. Expire old runs, cap total retained bytes, and make cleanup idempotent.
MCP-session state
Use ctx.session_id only for ephemeral negotiated or conversational state such
as host capabilities, a currently mounted app instance, progress channels, or
per-session defaults. Key a synchronized map by session ID and set an expiry.
Handle missing context because direct tests and REST convenience routes may not
provide it.
Do not put authoritative run results only in this map: Tasks may outlive a request, clients reconnect, and old session processes are eventually replaced. Do not use the session ID to decide which authenticated user may access data.
_SESSION_STATE = {}
_SESSION_LOCK = threading.Lock()
def update_session_state(ctx, **changes):
if ctx is None or not ctx.session_id:
return
with _SESSION_LOCK:
state = _SESSION_STATE.setdefault(ctx.session_id, {})
state.update(changes)
state["expires_at"] = time.time() + SESSION_TTL_SECONDS
Prune expired entries and cap the map. Avoid storing ctx itself because it is
request-scoped.
Authenticated-user state
Derive identity and authorization from validated gateway credentials or a
trusted server-side identity supplied by the platform. Bind user-owned runs,
quotas, and permissions to that identity. Never infer identity from
Mcp-Session-Id, a run handle, a model-provided username, or a directory name.
Concurrency rules
Make create operations atomic and return a handle only after inputs are completely written.
Write results to a temporary file and atomically replace the final filename.
Prevent two runs from mutating the same handle simultaneously with a lock, lock file, or durable state transition.
Make read tools tolerate
created,running,completed,failed, andcancelledstates explicitly.Make deletion refuse active work or cancel and join it before removal.
Make retries idempotent in behavior, not only in annotations.
Test two sessions using the same process, two concurrent calls against one handle, reconnection with a new session ID, stale-state expiry, and access to a run owned by a different authenticated identity.