mirror of
https://github.com/sasjs/server.git
synced 2026-07-23 21:25:29 +00:00
850695024f
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.
3.7 KiB
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 (seesas-execution-handshake.md).running/completed/failedare 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:124and its exit drivescompleted/faileddirectly 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 justawaitthe process they spawn right there). - Both runtimes resolve
processProgram()normally onfailed, 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. Seerequest-execution-flow.mdfor howExecution.tsturns that into a response.