1
0
mirror of https://github.com/sasjs/server.git synced 2026-07-23 21:25:29 +00:00
Files
server/api/docs/diagrams/session-lifecycle.md
YuryShkoda 850695024f fix(api): return 200 with embedded log on SAS session failure instead of 400
A failed session (e.g. SAS %abort;, or a non-zero JS/PY/R exit) is a
normal outcome of running arbitrary user code, not a request-shape or
server problem. The #388 fix stopped processProgram() from hanging
forever on a failed SAS session, but did so by throwing a
SessionExecutionError, which surfaced as an HTTP 400 with a bespoke
JSON shape - breaking Studio's log tab, which only renders on 2xx.

Align the SAS branch with the pre-existing JS/PY/R behaviour: resolve
instead of throwing, and let Execution.ts fold session.failureReason
into the log the same way it already does for a debug-mode run. This
removes the now-dead SessionExecutionError class and its try/catch
wrapper entirely.

Update the diagrams in api/docs/diagrams/ to match.
2026-07-15 13:59:01 +03:00

3.7 KiB

Session lifecycle (SessionState)

Enum defined in api/src/types/Session.ts. A Session is { id, state, path, creationTimeStamp, deathTimeStamp, expiresAfterMins?, failureReason? }. Sessions live in an in-memory array on a per-runtime singleton controller (process.sasSessionController for SAS, process.sessionController shared by JS/PY/R — see getSessionController() in Session.ts:239-253).

stateDiagram-v2
    [*] --> initialising: SAS runtime<br/>SASSessionController.createSession()<br/>Session.ts:77-95
    [*] --> pending: JS/PY/R runtime, no process yet<br/>SessionController.createSession()<br/>Session.ts:30-57

    initialising --> pending: dummy SYSIN file deleted<br/>by SAS's autoexec<br/>waitForSession(), Session.ts:175-192
    initialising --> failed: spawned SAS process exits<br/>before handshake completes<br/>Session.ts:153-163, 184-188

    pending --> running: session picked for a request<br/>executeProgram(), Execution.ts:76-78

    running --> completed: process exits 0<br/>SAS: Session.ts:148-152 (original process)<br/>JS/PY/R: processProgram.ts:124-129 (fresh process)
    running --> failed: process exits non-zero<br/>SAS: Session.ts:153-163<br/>JS/PY/R: processProgram.ts:130-135<br/>failureReason = err.toString()<br/>neither branch throws - see request-execution-flow.md

    completed --> [*]: deleteSession()<br/>scheduleSessionDestroy(), Session.ts:194-236
    failed --> [*]: deleteSession()<br/>scheduleSessionDestroy()

    note right of initialising
        SAS only. The real sas
        executable is already
        running, spin-waiting
        inside its AUTOEXEC for
        real code to arrive.
        See sas-execution-handshake.md
    end note

    note right of pending
        Session sits in the pool,
        reusable. Pre-warmed up to
        3 pending sessions per
        runtime (getSession(),
        Session.ts:59-69)
    end note

    note right of running
        SAS: no new process spawned
        here, request just feeds
        code to the already-running
        process.
        JS/PY/R: a brand new
        interpreter process is
        spawned right now.
    end note

Consumers of state

Reader Location Watches for
waitForSession Session.ts:175-192 failed (breaks early) or the dummy SYSIN file disappearing (implies session survived init)
processProgram (SAS branch poll loop) processProgram.ts:58-63 completed or failed - either just stops the loop and returns normally; failed does not throw. Execution.ts reads session.failureReason afterward to fold the log into a normal (200) result, same as it already does for JS/PY/R.
scheduleSessionDestroy Session.ts:204-236 running (extends death timer instead of destroying)

Asymmetry between runtimes

  • SAS: one OS process per session, spawned once at createSession() time and reused for exactly one job (see sas-execution-handshake.md). running/completed/failed are all driven by that same process's eventual exit.
  • JS/PY/R: a session is cheap (folder + id, no process). The interpreter process is spawned fresh, per request, inside processProgram.ts:124 and its exit drives completed/failed directly in the same function - there is no separate poll loop for these runtimes (SAS needs one because it's polling a state change happening in a process spawned earlier, at session creation; JS/PY/R just await the process they spawn right there).
  • Both runtimes resolve processProgram() normally on failed, they never throw for it - a failed session (any runtime) is a normal outcome of running arbitrary user code, not a request-shape/server problem. See request-execution-flow.md for how Execution.ts turns that into a response.