STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
backend.session Namespace Reference

Classes

class  OptimizerState
class  ControllerState
class  UserSession
class  SessionRegistry

Functions

dict _fresh_status ()
T run_compute (UserSession session, Callable[..., T] fn, *args, **kwargs)
str replace_config (UserSession session, config, Optional[str] config_path=None)
 gated_stream (agen)
UserSession get_session (Request request)

Variables

 _PROJECT_ROOT = Path(__file__).resolve().parents[1]
str _DEFAULT_CONFIG_PATH = _PROJECT_ROOT / "configs" / "default.yaml"
 SESSION_TTL_SECONDS = int(os.environ.get("SESSION_TTL_SECONDS", str(2 * 60 * 60)))
 MAX_CONCURRENT_JOBS = int(os.environ.get("MAX_CONCURRENT_JOBS", "3"))
 JOB_SEMAPHORE = asyncio.Semaphore(MAX_CONCURRENT_JOBS)
 T = TypeVar("T")
 registry = SessionRegistry()

Detailed Description

Per-user live working state, keyed on the caller's identity.

The saved-config *library* (``/api/configs``, ``backend/userdata.py``) was already
per-user, but the app's *live* working state -- the config currently loaded, its
runner, an in-flight optimization, the controller instance -- used to be process
globals. Two people connected at once therefore shared one design and clobbered
each other: uploads overwrote each other, an optimization refused to start while
another user's ran, results landed in a single slot the wrong user could read,
and one user's "stop" cancelled another's run.

This module gives each user their own ``UserSession`` holding all of that live
state. Identity is ``X-Auth-Email`` (via ``userdata.current_user``); with no Caddy
in front (local dev) there is no header, so everything collapses onto a single
``local`` session and the app behaves exactly as it did as a single-user global.

Endpoints obtain their session with ``session = Depends(get_session)`` and read
and write through it (``session.app_state``, ``session.optimizer``,
``session.controller``) instead of module globals. Because the heavy optimizer
jobs run inside nested closures, capturing ``session`` there propagates the right
user into the thread pool for free -- no ContextVar plumbing.

Function Documentation

◆ _fresh_status()

dict backend.session._fresh_status ( )
protected
A blank optimizer job-status block (one per layer).

Definition at line 60 of file session.py.

◆ gated_stream()

backend.session.gated_stream ( agen)
Hold JOB_SEMAPHORE for the lifetime of an async generator (an SSE stream).

Wrap the generator handed to ``StreamingResponse`` so a heavy streamed job
(a controller simulation) counts against the global concurrency cap for as
long as it runs, and releases the slot when the stream ends or the client
disconnects. Lets us cap those without reindenting the long loops that
produce them.

Definition at line 213 of file session.py.

◆ get_session()

UserSession backend.session.get_session ( Request request)
FastAPI dependency: the caller's live session.

Same identity key as the saved-config library (``X-Auth-Email`` in prod via
Caddy, ``local`` in dev). Never raises and never denies -- auth is Caddy's
job; this only decides whose in-memory state to use.

Definition at line 227 of file session.py.

◆ replace_config()

str backend.session.replace_config ( UserSession session,
config,
Optional[str] config_path = None )
Put a whole new config in the session: off the event loop, with a new ``load_token``.

Every route that swaps the design wholesale (an upload, a design opened, a YAML handed to a
Layer or the Time-Series tab) goes through here, so none of them forgets either half.
``set_config`` builds the runner -- a cold CEA cache is minutes of CPU, which inline on the
loop froze every request. The token is how a second tab of the same user (one session per
user) sees that this is no longer the design it loaded and stops autosaving into it; a
replacement that kept the old token let that tab write the newcomer over its own design.
Returns the new token, for the response.

Definition at line 197 of file session.py.

◆ run_compute()

T backend.session.run_compute ( UserSession session,
Callable[..., T] fn,
* args,
** kwargs )
Run a solve on the session's runner in the thread pool, one at a time per session.

Forward evaluate and the Time-Series runs are seconds of CPU (a Time-Series run is ~10 s
on a laptop, ~23 s on a CI runner). As ``async def`` endpoints calling the runner inline
they ran on the event loop and froze every other request for that long -- /api/health
included, so a page opened while another page's load-time Time-Series run was going sat on
"Connecting", and the design bar's autosave could not reach the server either (the
Playwright failures of 2026-10-17). Off the loop the server keeps answering; the lock keeps
a session's solves serial, as they were.

Definition at line 178 of file session.py.

Variable Documentation

◆ _DEFAULT_CONFIG_PATH

str backend.session._DEFAULT_CONFIG_PATH = _PROJECT_ROOT / "configs" / "default.yaml"
protected

Definition at line 43 of file session.py.

◆ _PROJECT_ROOT

backend.session._PROJECT_ROOT = Path(__file__).resolve().parents[1]
protected

Definition at line 42 of file session.py.

◆ JOB_SEMAPHORE

backend.session.JOB_SEMAPHORE = asyncio.Semaphore(MAX_CONCURRENT_JOBS)

Definition at line 55 of file session.py.

◆ MAX_CONCURRENT_JOBS

backend.session.MAX_CONCURRENT_JOBS = int(os.environ.get("MAX_CONCURRENT_JOBS", "3"))

Definition at line 54 of file session.py.

◆ registry

backend.session.registry = SessionRegistry()

Definition at line 175 of file session.py.

◆ SESSION_TTL_SECONDS

backend.session.SESSION_TTL_SECONDS = int(os.environ.get("SESSION_TTL_SECONDS", str(2 * 60 * 60)))

Definition at line 48 of file session.py.

◆ T

backend.session.T = TypeVar("T")

Definition at line 57 of file session.py.