From f94ddc0352973e208b2398bdd400832b8eab50ee Mon Sep 17 00:00:00 2001 From: Yury Date: Wed, 30 Oct 2024 15:33:06 +0300 Subject: [PATCH] refactor(session): implemented session state --- .vscode/settings.json | 4 +- api/src/controllers/internal/Execution.ts | 11 ++- .../internal/FileUploadController.ts | 12 +-- api/src/controllers/internal/Session.ts | 93 ++++++++++--------- .../controllers/internal/processProgram.ts | 19 ++-- api/src/routes/api/spec/stp.spec.ts | 7 +- api/src/types/Session.ts | 14 ++- 7 files changed, 84 insertions(+), 76 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 9a770f0..c15453a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,3 @@ { - "cSpell.words": [ - "autoexec" - ] + "cSpell.words": ["autoexec", "initialising"] } diff --git a/api/src/controllers/internal/Execution.ts b/api/src/controllers/internal/Execution.ts index 3004f56..c1ff197 100644 --- a/api/src/controllers/internal/Execution.ts +++ b/api/src/controllers/internal/Execution.ts @@ -2,7 +2,7 @@ import path from 'path' import fs from 'fs' import { getSessionController, processProgram } from './' import { readFile, fileExists, createFile, readFileBinary } from '@sasjs/utils' -import { PreProgramVars, Session, TreeNode } from '../../types' +import { PreProgramVars, Session, TreeNode, SessionState } from '../../types' import { extractHeaders, getFilesFolder, @@ -75,8 +75,7 @@ export class ExecutionController { const session = sessionByFileUpload ?? (await sessionController.getSession()) - session.inUse = true - session.consumed = true + session.state = SessionState.running const logPath = path.join(session.path, 'log.log') const headersPath = path.join(session.path, 'stpsrv_header.txt') @@ -121,7 +120,7 @@ export class ExecutionController { : '' // it should be deleted by scheduleSessionDestroy - session.inUse = false + session.state = SessionState.completed const resultParts = [] @@ -145,7 +144,9 @@ export class ExecutionController { return { httpHeaders, result: - isDebugOn(vars) || session.crashed ? resultParts.join(`\n`) : webout + isDebugOn(vars) || session.failureReason + ? resultParts.join(`\n`) + : webout } } diff --git a/api/src/controllers/internal/FileUploadController.ts b/api/src/controllers/internal/FileUploadController.ts index a06f512..f234772 100644 --- a/api/src/controllers/internal/FileUploadController.ts +++ b/api/src/controllers/internal/FileUploadController.ts @@ -2,11 +2,8 @@ import { Request, RequestHandler } from 'express' import multer from 'multer' import { uuidv4 } from '@sasjs/utils' import { getSessionController } from '.' -import { - executeProgramRawValidation, - getRunTimeAndFilePath, - RunTimeType -} from '../../utils' +import { executeProgramRawValidation, getRunTimeAndFilePath } from '../../utils' +import { SessionState } from '../../types' export class FileUploadController { private storage = multer.diskStorage({ @@ -56,9 +53,8 @@ export class FileUploadController { } const session = await sessionController.getSession() - // marking consumed true, so that it's not available - // as readySession for any other request - session.consumed = true + // change session state to 'running', so that it's not available for any other request + session.state = SessionState.running req.sasjsSession = session diff --git a/api/src/controllers/internal/Session.ts b/api/src/controllers/internal/Session.ts index 3000c07..c14310e 100644 --- a/api/src/controllers/internal/Session.ts +++ b/api/src/controllers/internal/Session.ts @@ -1,5 +1,5 @@ import path from 'path' -import { Session } from '../../types' +import { Session, SessionState } from '../../types' import { promisify } from 'util' import { execFile } from 'child_process' import { @@ -23,7 +23,9 @@ export class SessionController { protected sessions: Session[] = [] protected getReadySessions = (): Session[] => - this.sessions.filter((sess: Session) => sess.ready && !sess.consumed) + this.sessions.filter( + (session: Session) => session.state === SessionState.pending + ) protected async createSession(): Promise { const sessionId = generateUniqueFileName(generateTimestamp()) @@ -39,19 +41,18 @@ export class SessionController { const session: Session = { id: sessionId, - ready: true, - inUse: true, - consumed: false, - completed: false, + state: SessionState.pending, creationTimeStamp, deathTimeStamp, path: sessionFolder } const headersPath = path.join(session.path, 'stpsrv_header.txt') + await createFile(headersPath, 'content-type: text/html; charset=utf-8') this.sessions.push(session) + return session } @@ -83,10 +84,7 @@ export class SASSessionController extends SessionController { const session: Session = { id: sessionId, - ready: false, - inUse: false, - consumed: false, - completed: false, + state: SessionState.initialising, creationTimeStamp, deathTimeStamp, path: sessionFolder @@ -144,13 +142,20 @@ ${autoExecContent}` process.sasLoc!.endsWith('sas.exe') ? session.path : '' ]) .then(() => { - session.completed = true + session.state = SessionState.completed + process.logger.info('session completed', session) }) .catch((err) => { - session.completed = true - session.crashed = err.toString() - process.logger.error('session crashed', session.id, session.crashed) + session.state = SessionState.failed + + session.failureReason = err.toString() + + process.logger.error( + 'session crashed', + session.id, + session.failureReason + ) }) // we have a triggered session - add to array @@ -167,15 +172,19 @@ ${autoExecContent}` const codeFilePath = path.join(session.path, 'code.sas') // TODO: don't wait forever - while ((await fileExists(codeFilePath)) && !session.crashed) {} + while ( + (await fileExists(codeFilePath)) && + session.state !== SessionState.failed + ) {} - if (session.crashed) + if (session.state === SessionState.failed) { process.logger.error( 'session crashed! while waiting to be ready', - session.crashed + session.failureReason ) - - session.ready = true + } else { + session.state = SessionState.pending + } } private async deleteSession(session: Session) { @@ -189,37 +198,33 @@ ${autoExecContent}` } private scheduleSessionDestroy(session: Session) { - setTimeout( - async () => { - if (session.inUse) { - // adding 10 more minutes + setTimeout(async () => { + if (session.state === SessionState.running) { + // adding 10 more minutes + const newDeathTimeStamp = + parseInt(session.deathTimeStamp) + 10 * 60 * 1000 + session.deathTimeStamp = newDeathTimeStamp.toString() + + this.scheduleSessionDestroy(session) + } else { + const { expiresAfterMins } = session + + // delay session destroy if expiresAfterMins present + if (expiresAfterMins && session.state !== SessionState.completed) { + // calculate session death time using expiresAfterMins const newDeathTimeStamp = - parseInt(session.deathTimeStamp) + 10 * 60 * 1000 + parseInt(session.deathTimeStamp) + expiresAfterMins.mins * 60 * 1000 session.deathTimeStamp = newDeathTimeStamp.toString() + // set expiresAfterMins to true to avoid using it again + session.expiresAfterMins!.used = true + this.scheduleSessionDestroy(session) } else { - const { expiresAfterMins } = session - - // delay session destroy if expiresAfterMins present - if (expiresAfterMins && !expiresAfterMins.used) { - // calculate session death time using expiresAfterMins - const newDeathTimeStamp = - parseInt(session.deathTimeStamp) + - expiresAfterMins.mins * 60 * 1000 - session.deathTimeStamp = newDeathTimeStamp.toString() - - // set expiresAfterMins to true to avoid using it again - session.expiresAfterMins!.used = true - - this.scheduleSessionDestroy(session) - } else { - await this.deleteSession(session) - } + await this.deleteSession(session) } - }, - parseInt(session.deathTimeStamp) - new Date().getTime() - 100 - ) + } + }, parseInt(session.deathTimeStamp) - new Date().getTime() - 100) } } diff --git a/api/src/controllers/internal/processProgram.ts b/api/src/controllers/internal/processProgram.ts index 206f429..791a3a9 100644 --- a/api/src/controllers/internal/processProgram.ts +++ b/api/src/controllers/internal/processProgram.ts @@ -3,7 +3,7 @@ import { WriteStream, createWriteStream } from 'fs' import { execFile } from 'child_process' import { once } from 'stream' import { createFile, moveFile } from '@sasjs/utils' -import { PreProgramVars, Session } from '../../types' +import { PreProgramVars, Session, SessionState } from '../../types' import { RunTimeType } from '../../utils' import { ExecutionVars, @@ -49,7 +49,7 @@ export const processProgram = async ( await moveFile(codePath + '.bkp', codePath) // we now need to poll the session status - while (!session.completed) { + while (session.state === SessionState.completed) { await delay(50) } } else { @@ -114,13 +114,20 @@ export const processProgram = async ( await execFilePromise(executablePath, [codePath], writeStream) .then(() => { - session.completed = true + session.state = SessionState.completed + process.logger.info('session completed', session) }) .catch((err) => { - session.completed = true - session.crashed = err.toString() - process.logger.error('session crashed', session.id, session.crashed) + session.state = SessionState.failed + + session.failureReason = err.toString() + + process.logger.error( + 'session crashed', + session.id, + session.failureReason + ) }) // copy the code file to log and end write stream diff --git a/api/src/routes/api/spec/stp.spec.ts b/api/src/routes/api/spec/stp.spec.ts index 1512378..1bfd79b 100644 --- a/api/src/routes/api/spec/stp.spec.ts +++ b/api/src/routes/api/spec/stp.spec.ts @@ -25,7 +25,7 @@ import { SASSessionController } from '../../../controllers/internal' import * as ProcessProgramModule from '../../../controllers/internal/processProgram' -import { Session } from '../../../types' +import { Session, SessionState } from '../../../types' const clientId = 'someclientID' @@ -493,10 +493,7 @@ const mockedGetSession = async () => { const session: Session = { id: sessionId, - ready: true, - inUse: true, - consumed: false, - completed: false, + state: SessionState.pending, creationTimeStamp, deathTimeStamp, path: sessionFolder diff --git a/api/src/types/Session.ts b/api/src/types/Session.ts index 4fa1cfb..1015e1e 100644 --- a/api/src/types/Session.ts +++ b/api/src/types/Session.ts @@ -1,12 +1,16 @@ +export enum SessionState { + initialising = 'initialising', // session is initialising and nor ready to be used yet + pending = 'pending', // session is ready to be used + running = 'running', // session is in use + completed = 'completed', // session is completed and can be destroyed + failed = 'failed' // session failed +} export interface Session { id: string - ready: boolean + state: SessionState creationTimeStamp: string deathTimeStamp: string path: string - inUse: boolean - consumed: boolean - completed: boolean - crashed?: string expiresAfterMins?: { mins: number; used: boolean } + failureReason?: string }