mirror of
https://github.com/sasjs/server.git
synced 2025-12-08 02:42:44 +00:00
refactor(session): implemented session state
This commit is contained in:
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -1,5 +1,3 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"autoexec"
|
||||
]
|
||||
"cSpell.words": ["autoexec", "initialising"]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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<Session> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user