diff --git a/src/SessionManager.ts b/src/SessionManager.ts index eb195a0..a0c68d7 100644 --- a/src/SessionManager.ts +++ b/src/SessionManager.ts @@ -5,10 +5,10 @@ import { prefixMessage } from '@sasjs/utils/error' import { RequestClient } from './request/RequestClient' const MAX_SESSION_COUNT = 1 -const RETRY_LIMIT: number = 3 -let RETRY_COUNT: number = 0 export class SessionManager { + private loggedErrors: NoSessionStateError[] = [] + constructor( private serverUrl: string, private contextName: string, @@ -154,69 +154,75 @@ export class SessionManager { session: Session, etag: string | null, accessToken?: string - ) { + ): Promise { const logger = process.logger || console let sessionState = session.state const stateLink = session.links.find((l: any) => l.rel === 'state') - return new Promise(async (resolve, reject) => { - if ( - sessionState === 'pending' || - sessionState === 'running' || - sessionState === '' - ) { - if (stateLink) { - if (this.debug && !this.printedSessionState.printed) { - logger.info('Polling session status...') + if ( + sessionState === 'pending' || + sessionState === 'running' || + sessionState === '' + ) { + if (stateLink) { + if (this.debug && !this.printedSessionState.printed) { + logger.info('Polling session status...') - this.printedSessionState.printed = true - } - - const { result: state, responseStatus: responseStatus } = - await this.getSessionState( - `${this.serverUrl}${stateLink.href}?wait=30`, - etag!, - accessToken - ).catch((err) => { - throw prefixMessage(err, 'Error while getting session state.') - }) - - sessionState = state.trim() - - if (this.debug && this.printedSessionState.state !== sessionState) { - logger.info(`Current session state is '${sessionState}'`) - - this.printedSessionState.state = sessionState - this.printedSessionState.printed = false - } - - // There is an internal error present in SAS Viya 3.5 - // Retry to wait for a session status in such case of SAS internal error - if (!sessionState) { - if (RETRY_COUNT < RETRY_LIMIT) { - RETRY_COUNT++ - - resolve(this.waitForSession(session, etag, accessToken)) - } else { - reject( - new NoSessionStateError( - responseStatus, - this.serverUrl + stateLink.href, - session.links.find((l: any) => l.rel === 'log') - ?.href as string - ) - ) - } - } - - resolve(sessionState) + this.printedSessionState.printed = true } + + const { result: state, responseStatus: responseStatus } = + await this.getSessionState( + `${this.serverUrl}${stateLink.href}?wait=30`, + etag!, + accessToken + ).catch((err) => { + throw prefixMessage(err, 'Error while getting session state.') + }) + + sessionState = state.trim() + + if (this.debug && this.printedSessionState.state !== sessionState) { + logger.info(`Current session state is '${sessionState}'`) + + this.printedSessionState.state = sessionState + this.printedSessionState.printed = false + } + + if (!sessionState) { + const stateError = new NoSessionStateError( + responseStatus, + this.serverUrl + stateLink.href, + session.links.find((l: any) => l.rel === 'log')?.href as string + ) + + if ( + !this.loggedErrors.find( + (err: NoSessionStateError) => + err.serverResponseStatus === stateError.serverResponseStatus + ) + ) { + this.loggedErrors.push(stateError) + + logger.info(stateError.message) + } + + return await this.waitForSession(session, etag, accessToken) + } + + this.loggedErrors = [] + + return sessionState } else { - resolve(sessionState) + throw 'Error while getting session state link.' } - }) + } else { + this.loggedErrors = [] + + return sessionState + } } private async getSessionState( diff --git a/src/test/SessionManager.spec.ts b/src/test/SessionManager.spec.ts index c818d4f..ace8526 100644 --- a/src/test/SessionManager.spec.ts +++ b/src/test/SessionManager.spec.ts @@ -3,6 +3,8 @@ import { RequestClient } from '../request/RequestClient' import { NoSessionStateError } from '../types/errors' import * as dotenv from 'dotenv' import axios from 'axios' +import { Logger, LogLevel } from '@sasjs/utils' +import { Session } from '../types' jest.mock('axios') const mockedAxios = axios as jest.Mocked @@ -47,36 +49,91 @@ describe('SessionManager', () => { }) describe('waitForSession', () => { + const session: Session = { + id: 'id', + state: '', + links: [{ rel: 'state', href: '', uri: '', type: '', method: 'GET' }], + attributes: { + sessionInactiveTimeout: 0 + }, + creationTimeStamp: '' + } + + beforeEach(() => { + ;(process as any).logger = new Logger(LogLevel.Off) + }) + it('should reject with NoSessionStateError if SAS server did not provide session state', async () => { - const responseStatus = 304 + let requestAttempt = 0 + const requestAttemptLimit = 10 + const sessionState = 'idle' + + mockedAxios.get.mockImplementation(() => { + requestAttempt += 1 + + if (requestAttempt >= requestAttemptLimit) { + return Promise.resolve({ data: sessionState, status: 200 }) + } + + return Promise.resolve({ data: '', status: 304 }) + }) + + jest.spyOn((process as any).logger, 'info') + + sessionManager.debug = true + + await expect( + sessionManager['waitForSession'](session, null, 'access_token') + ).resolves.toEqual(sessionState) + + expect(mockedAxios.get).toHaveBeenCalledTimes(requestAttemptLimit) + expect((process as any).logger.info).toHaveBeenCalledTimes(3) + expect((process as any).logger.info).toHaveBeenNthCalledWith( + 1, + 'Polling session status...' + ) + expect((process as any).logger.info).toHaveBeenNthCalledWith( + 2, + `Could not get session state. Server responded with 304 whilst checking state: ${process.env.SERVER_URL}` + ) + expect((process as any).logger.info).toHaveBeenNthCalledWith( + 3, + `Current session state is '${sessionState}'` + ) + }) + + it('should throw an error if there is no session link', async () => { + const customSession = JSON.parse(JSON.stringify(session)) + customSession.links = [] mockedAxios.get.mockImplementation(() => - Promise.resolve({ data: '', status: responseStatus }) + Promise.resolve({ data: customSession.state, status: 200 }) ) await expect( - sessionManager['waitForSession']( - { - id: 'id', - state: '', - links: [ - { rel: 'state', href: '', uri: '', type: '', method: 'GET' } - ], - attributes: { - sessionInactiveTimeout: 0 - }, - creationTimeStamp: '' - }, - null, - 'access_token' - ) - ).rejects.toEqual( - new NoSessionStateError( - responseStatus, - process.env.SERVER_URL as string, - 'logUrl' - ) + sessionManager['waitForSession'](customSession, null, 'access_token') + ).rejects.toContain('Error while getting session state link.') + }) + + it('should throw an error if could not get session state', async () => { + mockedAxios.get.mockImplementation(() => Promise.reject('Mocked error')) + + await expect( + sessionManager['waitForSession'](session, null, 'access_token') + ).rejects.toContain('Error while getting session state.') + }) + + it('should return session state', async () => { + const customSession = JSON.parse(JSON.stringify(session)) + customSession.state = 'completed' + + mockedAxios.get.mockImplementation(() => + Promise.resolve({ data: customSession.state, status: 200 }) ) + + await expect( + sessionManager['waitForSession'](customSession, null, 'access_token') + ).resolves.toEqual(customSession.state) }) }) })