1
0
mirror of https://github.com/sasjs/server.git synced 2025-12-11 03:34:35 +00:00

refactor(session): implemented session state

This commit is contained in:
Yury
2024-10-30 15:33:06 +03:00
parent 03670cf0d6
commit f94ddc0352
7 changed files with 84 additions and 76 deletions

View File

@@ -1,5 +1,3 @@
{ {
"cSpell.words": [ "cSpell.words": ["autoexec", "initialising"]
"autoexec"
]
} }

View File

@@ -2,7 +2,7 @@ import path from 'path'
import fs from 'fs' import fs from 'fs'
import { getSessionController, processProgram } from './' import { getSessionController, processProgram } from './'
import { readFile, fileExists, createFile, readFileBinary } from '@sasjs/utils' import { readFile, fileExists, createFile, readFileBinary } from '@sasjs/utils'
import { PreProgramVars, Session, TreeNode } from '../../types' import { PreProgramVars, Session, TreeNode, SessionState } from '../../types'
import { import {
extractHeaders, extractHeaders,
getFilesFolder, getFilesFolder,
@@ -75,8 +75,7 @@ export class ExecutionController {
const session = const session =
sessionByFileUpload ?? (await sessionController.getSession()) sessionByFileUpload ?? (await sessionController.getSession())
session.inUse = true session.state = SessionState.running
session.consumed = true
const logPath = path.join(session.path, 'log.log') const logPath = path.join(session.path, 'log.log')
const headersPath = path.join(session.path, 'stpsrv_header.txt') const headersPath = path.join(session.path, 'stpsrv_header.txt')
@@ -121,7 +120,7 @@ export class ExecutionController {
: '' : ''
// it should be deleted by scheduleSessionDestroy // it should be deleted by scheduleSessionDestroy
session.inUse = false session.state = SessionState.completed
const resultParts = [] const resultParts = []
@@ -145,7 +144,9 @@ export class ExecutionController {
return { return {
httpHeaders, httpHeaders,
result: result:
isDebugOn(vars) || session.crashed ? resultParts.join(`\n`) : webout isDebugOn(vars) || session.failureReason
? resultParts.join(`\n`)
: webout
} }
} }

View File

@@ -2,11 +2,8 @@ import { Request, RequestHandler } from 'express'
import multer from 'multer' import multer from 'multer'
import { uuidv4 } from '@sasjs/utils' import { uuidv4 } from '@sasjs/utils'
import { getSessionController } from '.' import { getSessionController } from '.'
import { import { executeProgramRawValidation, getRunTimeAndFilePath } from '../../utils'
executeProgramRawValidation, import { SessionState } from '../../types'
getRunTimeAndFilePath,
RunTimeType
} from '../../utils'
export class FileUploadController { export class FileUploadController {
private storage = multer.diskStorage({ private storage = multer.diskStorage({
@@ -56,9 +53,8 @@ export class FileUploadController {
} }
const session = await sessionController.getSession() const session = await sessionController.getSession()
// marking consumed true, so that it's not available // change session state to 'running', so that it's not available for any other request
// as readySession for any other request session.state = SessionState.running
session.consumed = true
req.sasjsSession = session req.sasjsSession = session

View File

@@ -1,5 +1,5 @@
import path from 'path' import path from 'path'
import { Session } from '../../types' import { Session, SessionState } from '../../types'
import { promisify } from 'util' import { promisify } from 'util'
import { execFile } from 'child_process' import { execFile } from 'child_process'
import { import {
@@ -23,7 +23,9 @@ export class SessionController {
protected sessions: Session[] = [] protected sessions: Session[] = []
protected getReadySessions = (): 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<Session> { protected async createSession(): Promise<Session> {
const sessionId = generateUniqueFileName(generateTimestamp()) const sessionId = generateUniqueFileName(generateTimestamp())
@@ -39,19 +41,18 @@ export class SessionController {
const session: Session = { const session: Session = {
id: sessionId, id: sessionId,
ready: true, state: SessionState.pending,
inUse: true,
consumed: false,
completed: false,
creationTimeStamp, creationTimeStamp,
deathTimeStamp, deathTimeStamp,
path: sessionFolder path: sessionFolder
} }
const headersPath = path.join(session.path, 'stpsrv_header.txt') const headersPath = path.join(session.path, 'stpsrv_header.txt')
await createFile(headersPath, 'content-type: text/html; charset=utf-8') await createFile(headersPath, 'content-type: text/html; charset=utf-8')
this.sessions.push(session) this.sessions.push(session)
return session return session
} }
@@ -83,10 +84,7 @@ export class SASSessionController extends SessionController {
const session: Session = { const session: Session = {
id: sessionId, id: sessionId,
ready: false, state: SessionState.initialising,
inUse: false,
consumed: false,
completed: false,
creationTimeStamp, creationTimeStamp,
deathTimeStamp, deathTimeStamp,
path: sessionFolder path: sessionFolder
@@ -144,13 +142,20 @@ ${autoExecContent}`
process.sasLoc!.endsWith('sas.exe') ? session.path : '' process.sasLoc!.endsWith('sas.exe') ? session.path : ''
]) ])
.then(() => { .then(() => {
session.completed = true session.state = SessionState.completed
process.logger.info('session completed', session) process.logger.info('session completed', session)
}) })
.catch((err) => { .catch((err) => {
session.completed = true session.state = SessionState.failed
session.crashed = err.toString()
process.logger.error('session crashed', session.id, session.crashed) session.failureReason = err.toString()
process.logger.error(
'session crashed',
session.id,
session.failureReason
)
}) })
// we have a triggered session - add to array // we have a triggered session - add to array
@@ -167,15 +172,19 @@ ${autoExecContent}`
const codeFilePath = path.join(session.path, 'code.sas') const codeFilePath = path.join(session.path, 'code.sas')
// TODO: don't wait forever // 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( process.logger.error(
'session crashed! while waiting to be ready', 'session crashed! while waiting to be ready',
session.crashed session.failureReason
) )
} else {
session.ready = true session.state = SessionState.pending
}
} }
private async deleteSession(session: Session) { private async deleteSession(session: Session) {
@@ -189,37 +198,33 @@ ${autoExecContent}`
} }
private scheduleSessionDestroy(session: Session) { private scheduleSessionDestroy(session: Session) {
setTimeout( setTimeout(async () => {
async () => { if (session.state === SessionState.running) {
if (session.inUse) { // adding 10 more minutes
// 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 = const newDeathTimeStamp =
parseInt(session.deathTimeStamp) + 10 * 60 * 1000 parseInt(session.deathTimeStamp) + expiresAfterMins.mins * 60 * 1000
session.deathTimeStamp = newDeathTimeStamp.toString() session.deathTimeStamp = newDeathTimeStamp.toString()
// set expiresAfterMins to true to avoid using it again
session.expiresAfterMins!.used = true
this.scheduleSessionDestroy(session) this.scheduleSessionDestroy(session)
} else { } else {
const { expiresAfterMins } = session await this.deleteSession(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)
}
} }
}, }
parseInt(session.deathTimeStamp) - new Date().getTime() - 100 }, parseInt(session.deathTimeStamp) - new Date().getTime() - 100)
)
} }
} }

View File

@@ -3,7 +3,7 @@ import { WriteStream, createWriteStream } from 'fs'
import { execFile } from 'child_process' import { execFile } from 'child_process'
import { once } from 'stream' import { once } from 'stream'
import { createFile, moveFile } from '@sasjs/utils' import { createFile, moveFile } from '@sasjs/utils'
import { PreProgramVars, Session } from '../../types' import { PreProgramVars, Session, SessionState } from '../../types'
import { RunTimeType } from '../../utils' import { RunTimeType } from '../../utils'
import { import {
ExecutionVars, ExecutionVars,
@@ -49,7 +49,7 @@ export const processProgram = async (
await moveFile(codePath + '.bkp', codePath) await moveFile(codePath + '.bkp', codePath)
// we now need to poll the session status // we now need to poll the session status
while (!session.completed) { while (session.state === SessionState.completed) {
await delay(50) await delay(50)
} }
} else { } else {
@@ -114,13 +114,20 @@ export const processProgram = async (
await execFilePromise(executablePath, [codePath], writeStream) await execFilePromise(executablePath, [codePath], writeStream)
.then(() => { .then(() => {
session.completed = true session.state = SessionState.completed
process.logger.info('session completed', session) process.logger.info('session completed', session)
}) })
.catch((err) => { .catch((err) => {
session.completed = true session.state = SessionState.failed
session.crashed = err.toString()
process.logger.error('session crashed', session.id, session.crashed) session.failureReason = err.toString()
process.logger.error(
'session crashed',
session.id,
session.failureReason
)
}) })
// copy the code file to log and end write stream // copy the code file to log and end write stream

View File

@@ -25,7 +25,7 @@ import {
SASSessionController SASSessionController
} from '../../../controllers/internal' } from '../../../controllers/internal'
import * as ProcessProgramModule from '../../../controllers/internal/processProgram' import * as ProcessProgramModule from '../../../controllers/internal/processProgram'
import { Session } from '../../../types' import { Session, SessionState } from '../../../types'
const clientId = 'someclientID' const clientId = 'someclientID'
@@ -493,10 +493,7 @@ const mockedGetSession = async () => {
const session: Session = { const session: Session = {
id: sessionId, id: sessionId,
ready: true, state: SessionState.pending,
inUse: true,
consumed: false,
completed: false,
creationTimeStamp, creationTimeStamp,
deathTimeStamp, deathTimeStamp,
path: sessionFolder path: sessionFolder

View File

@@ -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 { export interface Session {
id: string id: string
ready: boolean state: SessionState
creationTimeStamp: string creationTimeStamp: string
deathTimeStamp: string deathTimeStamp: string
path: string path: string
inUse: boolean
consumed: boolean
completed: boolean
crashed?: string
expiresAfterMins?: { mins: number; used: boolean } expiresAfterMins?: { mins: number; used: boolean }
failureReason?: string
} }