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.
78 lines
3.7 KiB
Markdown
78 lines
3.7 KiB
Markdown
# 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`).
|
|
|
|
```mermaid
|
|
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.
|