Skip to content

Commit d6e527e

Browse files
authored
Merge pull request #379 from sasjs/issue-378
Issue 378
2 parents ea2ec97 + bc2cff1 commit d6e527e

File tree

13 files changed

+178
-66
lines changed

13 files changed

+178
-66
lines changed

.vscode/settings.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
11
{
2-
"cSpell.words": [
3-
"autoexec"
4-
]
2+
"cSpell.words": ["autoexec", "initialising"]
53
}

api/public/swagger.yaml

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,8 @@ components:
113113
properties:
114114
sessionId:
115115
type: string
116-
description: "The SessionId is the name of the temporary folder used to store the outputs.\nFor SAS, this would be the SASWORK folder. Can be used to poll job status.\nThis session ID should be used to poll job status."
117-
example: '{ sessionId: ''20241028074744-54132-1730101664824'' }'
116+
description: "`sessionId` is the ID of the session and the name of the temporary folder\nused to store code outputs.<br><br>\nFor SAS, this would be the location of the SASWORK folder.<br><br>\n`sessionId` can be used to poll session state using the\nGET /SASjsApi/session/{sessionId}/state endpoint."
117+
example: 20241028074744-54132-1730101664824
118118
required:
119119
- sessionId
120120
type: object
@@ -585,6 +585,14 @@ components:
585585
- needsToUpdatePassword
586586
type: object
587587
additionalProperties: false
588+
SessionState:
589+
enum:
590+
- initialising
591+
- pending
592+
- running
593+
- completed
594+
- failed
595+
type: string
588596
ExecutePostRequestPayload:
589597
properties:
590598
_program:
@@ -597,8 +605,8 @@ components:
597605
properties:
598606
sessionId:
599607
type: string
600-
description: "The SessionId is the name of the temporary folder used to store the outputs.\nFor SAS, this would be the SASWORK folder. Can be used to poll program status.\nThis session ID should be used to poll program status."
601-
example: '{ sessionId: ''20241028074744-54132-1730101664824'' }'
608+
description: "`sessionId` is the ID of the session and the name of the temporary folder\nused to store program outputs.<br><br>\nFor SAS, this would be the location of the SASWORK folder.<br><br>\n`sessionId` can be used to poll session state using the\nGET /SASjsApi/session/{sessionId}/state endpoint."
609+
example: 20241028074744-54132-1730101664824
602610
required:
603611
- sessionId
604612
type: object
@@ -1841,6 +1849,30 @@ paths:
18411849
-
18421850
bearerAuth: []
18431851
parameters: []
1852+
'/SASjsApi/session/{sessionId}/state':
1853+
get:
1854+
operationId: SessionState
1855+
responses:
1856+
'200':
1857+
description: Ok
1858+
content:
1859+
application/json:
1860+
schema:
1861+
$ref: '#/components/schemas/SessionState'
1862+
description: "The polling endpoint is currently implemented for single-server deployments only.<br>\nLoad balanced / grid topologies will be supported in a future release.<br>\nIf your site requires this, please reach out to SASjs Support."
1863+
summary: 'Get session state (initialising, pending, running, completed, failed).'
1864+
tags:
1865+
- Session
1866+
security:
1867+
-
1868+
bearerAuth: []
1869+
parameters:
1870+
-
1871+
in: path
1872+
name: sessionId
1873+
required: true
1874+
schema:
1875+
type: string
18441876
/SASjsApi/stp/execute:
18451877
get:
18461878
operationId: ExecuteGetRequest

api/src/controllers/code.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,12 @@ interface TriggerCodePayload {
4242

4343
interface TriggerCodeResponse {
4444
/**
45-
* The SessionId is the name of the temporary folder used to store the outputs.
46-
* For SAS, this would be the SASWORK folder. Can be used to poll job status.
47-
* This session ID should be used to poll job status.
48-
* @example "{ sessionId: '20241028074744-54132-1730101664824' }"
45+
* `sessionId` is the ID of the session and the name of the temporary folder
46+
* used to store code outputs.<br><br>
47+
* For SAS, this would be the location of the SASWORK folder.<br><br>
48+
* `sessionId` can be used to poll session state using the
49+
* GET /SASjsApi/session/{sessionId}/state endpoint.
50+
* @example "20241028074744-54132-1730101664824"
4951
*/
5052
sessionId: string
5153
}

api/src/controllers/internal/Execution.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import path from 'path'
22
import fs from 'fs'
33
import { getSessionController, processProgram } from './'
44
import { readFile, fileExists, createFile, readFileBinary } from '@sasjs/utils'
5-
import { PreProgramVars, Session, TreeNode } from '../../types'
5+
import { PreProgramVars, Session, TreeNode, SessionState } from '../../types'
66
import {
77
extractHeaders,
88
getFilesFolder,
@@ -75,8 +75,7 @@ export class ExecutionController {
7575

7676
const session =
7777
sessionByFileUpload ?? (await sessionController.getSession())
78-
session.inUse = true
79-
session.consumed = true
78+
session.state = SessionState.running
8079

8180
const logPath = path.join(session.path, 'log.log')
8281
const headersPath = path.join(session.path, 'stpsrv_header.txt')
@@ -121,7 +120,7 @@ export class ExecutionController {
121120
: ''
122121

123122
// it should be deleted by scheduleSessionDestroy
124-
session.inUse = false
123+
session.state = SessionState.completed
125124

126125
const resultParts = []
127126

@@ -145,7 +144,9 @@ export class ExecutionController {
145144
return {
146145
httpHeaders,
147146
result:
148-
isDebugOn(vars) || session.crashed ? resultParts.join(`\n`) : webout
147+
isDebugOn(vars) || session.failureReason
148+
? resultParts.join(`\n`)
149+
: webout
149150
}
150151
}
151152

api/src/controllers/internal/FileUploadController.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,8 @@ import { Request, RequestHandler } from 'express'
22
import multer from 'multer'
33
import { uuidv4 } from '@sasjs/utils'
44
import { getSessionController } from '.'
5-
import {
6-
executeProgramRawValidation,
7-
getRunTimeAndFilePath,
8-
RunTimeType
9-
} from '../../utils'
5+
import { executeProgramRawValidation, getRunTimeAndFilePath } from '../../utils'
6+
import { SessionState } from '../../types'
107

118
export class FileUploadController {
129
private storage = multer.diskStorage({
@@ -56,9 +53,8 @@ export class FileUploadController {
5653
}
5754

5855
const session = await sessionController.getSession()
59-
// marking consumed true, so that it's not available
60-
// as readySession for any other request
61-
session.consumed = true
56+
// change session state to 'running', so that it's not available for any other request
57+
session.state = SessionState.running
6258

6359
req.sasjsSession = session
6460

api/src/controllers/internal/Session.ts

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import path from 'path'
2-
import { Session } from '../../types'
2+
import { Session, SessionState } from '../../types'
33
import { promisify } from 'util'
44
import { execFile } from 'child_process'
55
import {
@@ -23,7 +23,9 @@ export class SessionController {
2323
protected sessions: Session[] = []
2424

2525
protected getReadySessions = (): Session[] =>
26-
this.sessions.filter((sess: Session) => sess.ready && !sess.consumed)
26+
this.sessions.filter(
27+
(session: Session) => session.state === SessionState.pending
28+
)
2729

2830
protected async createSession(): Promise<Session> {
2931
const sessionId = generateUniqueFileName(generateTimestamp())
@@ -39,19 +41,18 @@ export class SessionController {
3941

4042
const session: Session = {
4143
id: sessionId,
42-
ready: true,
43-
inUse: true,
44-
consumed: false,
45-
completed: false,
44+
state: SessionState.pending,
4645
creationTimeStamp,
4746
deathTimeStamp,
4847
path: sessionFolder
4948
}
5049

5150
const headersPath = path.join(session.path, 'stpsrv_header.txt')
51+
5252
await createFile(headersPath, 'content-type: text/html; charset=utf-8')
5353

5454
this.sessions.push(session)
55+
5556
return session
5657
}
5758

@@ -66,6 +67,10 @@ export class SessionController {
6667

6768
return session
6869
}
70+
71+
public getSessionById(id: string) {
72+
return this.sessions.find((session) => session.id === id)
73+
}
6974
}
7075

7176
export class SASSessionController extends SessionController {
@@ -83,10 +88,7 @@ export class SASSessionController extends SessionController {
8388

8489
const session: Session = {
8590
id: sessionId,
86-
ready: false,
87-
inUse: false,
88-
consumed: false,
89-
completed: false,
91+
state: SessionState.initialising,
9092
creationTimeStamp,
9193
deathTimeStamp,
9294
path: sessionFolder
@@ -144,13 +146,20 @@ ${autoExecContent}`
144146
process.sasLoc!.endsWith('sas.exe') ? session.path : ''
145147
])
146148
.then(() => {
147-
session.completed = true
149+
session.state = SessionState.completed
150+
148151
process.logger.info('session completed', session)
149152
})
150153
.catch((err) => {
151-
session.completed = true
152-
session.crashed = err.toString()
153-
process.logger.error('session crashed', session.id, session.crashed)
154+
session.state = SessionState.failed
155+
156+
session.failureReason = err.toString()
157+
158+
process.logger.error(
159+
'session crashed',
160+
session.id,
161+
session.failureReason
162+
)
154163
})
155164

156165
// we have a triggered session - add to array
@@ -167,15 +176,19 @@ ${autoExecContent}`
167176
const codeFilePath = path.join(session.path, 'code.sas')
168177

169178
// TODO: don't wait forever
170-
while ((await fileExists(codeFilePath)) && !session.crashed) {}
179+
while (
180+
(await fileExists(codeFilePath)) &&
181+
session.state !== SessionState.failed
182+
) {}
171183

172-
if (session.crashed)
184+
if (session.state === SessionState.failed) {
173185
process.logger.error(
174186
'session crashed! while waiting to be ready',
175-
session.crashed
187+
session.failureReason
176188
)
177-
178-
session.ready = true
189+
} else {
190+
session.state = SessionState.pending
191+
}
179192
}
180193

181194
private async deleteSession(session: Session) {
@@ -191,7 +204,7 @@ ${autoExecContent}`
191204
private scheduleSessionDestroy(session: Session) {
192205
setTimeout(
193206
async () => {
194-
if (session.inUse) {
207+
if (session.state === SessionState.running) {
195208
// adding 10 more minutes
196209
const newDeathTimeStamp =
197210
parseInt(session.deathTimeStamp) + 10 * 60 * 1000
@@ -202,7 +215,7 @@ ${autoExecContent}`
202215
const { expiresAfterMins } = session
203216

204217
// delay session destroy if expiresAfterMins present
205-
if (expiresAfterMins && !expiresAfterMins.used) {
218+
if (expiresAfterMins && session.state !== SessionState.completed) {
206219
// calculate session death time using expiresAfterMins
207220
const newDeathTimeStamp =
208221
parseInt(session.deathTimeStamp) +

api/src/controllers/internal/processProgram.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { WriteStream, createWriteStream } from 'fs'
33
import { execFile } from 'child_process'
44
import { once } from 'stream'
55
import { createFile, moveFile } from '@sasjs/utils'
6-
import { PreProgramVars, Session } from '../../types'
6+
import { PreProgramVars, Session, SessionState } from '../../types'
77
import { RunTimeType } from '../../utils'
88
import {
99
ExecutionVars,
@@ -49,7 +49,7 @@ export const processProgram = async (
4949
await moveFile(codePath + '.bkp', codePath)
5050

5151
// we now need to poll the session status
52-
while (!session.completed) {
52+
while (session.state !== SessionState.completed) {
5353
await delay(50)
5454
}
5555
} else {
@@ -114,13 +114,20 @@ export const processProgram = async (
114114

115115
await execFilePromise(executablePath, [codePath], writeStream)
116116
.then(() => {
117-
session.completed = true
117+
session.state = SessionState.completed
118+
118119
process.logger.info('session completed', session)
119120
})
120121
.catch((err) => {
121-
session.completed = true
122-
session.crashed = err.toString()
123-
process.logger.error('session crashed', session.id, session.crashed)
122+
session.state = SessionState.failed
123+
124+
session.failureReason = err.toString()
125+
126+
process.logger.error(
127+
'session crashed',
128+
session.id,
129+
session.failureReason
130+
)
124131
})
125132

126133
// copy the code file to log and end write stream

api/src/controllers/session.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import express from 'express'
22
import { Request, Security, Route, Tags, Example, Get } from 'tsoa'
33
import { UserResponse } from './user'
4+
import { getSessionController } from './internal'
5+
import { SessionState } from '../types'
46

57
interface SessionResponse extends UserResponse {
68
needsToUpdatePassword: boolean
@@ -26,6 +28,18 @@ export class SessionController {
2628
): Promise<SessionResponse> {
2729
return session(request)
2830
}
31+
32+
/**
33+
* The polling endpoint is currently implemented for single-server deployments only.<br>
34+
* Load balanced / grid topologies will be supported in a future release.<br>
35+
* If your site requires this, please reach out to SASjs Support.
36+
* @summary Get session state (initialising, pending, running, completed, failed).
37+
* @example completed
38+
*/
39+
@Get('/:sessionId/state')
40+
public async sessionState(sessionId: string): Promise<SessionState> {
41+
return sessionState(sessionId)
42+
}
2943
}
3044

3145
const session = (req: express.Request) => ({
@@ -35,3 +49,23 @@ const session = (req: express.Request) => ({
3549
isAdmin: req.user!.isAdmin,
3650
needsToUpdatePassword: req.user!.needsToUpdatePassword
3751
})
52+
53+
const sessionState = (sessionId: string): SessionState => {
54+
for (let runTime of process.runTimes) {
55+
// get session controller for each available runTime
56+
const sessionController = getSessionController(runTime)
57+
58+
// get session by sessionId
59+
const session = sessionController.getSessionById(sessionId)
60+
61+
// return session state if session was found
62+
if (session) {
63+
return session.state
64+
}
65+
}
66+
67+
throw {
68+
code: 404,
69+
message: `Session with ID '${sessionId}' was not found.`
70+
}
71+
}

0 commit comments

Comments
 (0)