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
T
YuryShkoda f84af4ac06 fix(api): return prompt error response instead of hanging when a SAS session fails
The SAS-runtime poll loop in processProgram only checked for
SessionState.completed, so a session that failed (e.g. via %abort;)
left the loop spinning on delay(50) forever - the request never
resolved, even though the SAS log had already been written. Split a single completed-on-either-outcome
flag into separate completed/failed states without updating this loop.

- processProgram now throws when session.state becomes 'failed'
- Execution.ts catches that, reads the complete log (guaranteed
  complete since the session's process has already exited by then),
  and throws a SessionExecutionError carrying it
- stp.ts/code.ts surface { ..., log } in the HTTP error response
- add unit tests for the poll loop and the log-enrichment logic, plus
  a mock SAS executable + end-to-end test exercising the real
  session/process pipeline without needing a SAS install
- add docs/diagrams covering the session lifecycle and the SAS
  execution handshake mechanism, for future context
2026-07-13 13:46:36 +03:00

3.1 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:94-96

    running --> completed: process exits 0<br/>SAS: Session.ts:148-152 (original process)<br/>JS/PY/R: processProgram.ts:116-120 (fresh process)
    running --> failed: process exits non-zero<br/>SAS: Session.ts:153-163<br/>JS/PY/R: processProgram.ts:121-131<br/>failureReason = err.toString()

    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:52-58 completed (success exit) or failed (throws, carrying session.failureReason)
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:115 and its exit drives completed/failed directly in the same function - there is no separate poll loop for these runtimes.