From abdcc5942575a9126f63d47a0bb93a93961b65ab Mon Sep 17 00:00:00 2001 From: Guy Harwood <GuyHarwood@users.noreply.github.com> Date: Mon, 12 Jun 2023 16:27:16 +0100 Subject: [PATCH] feature/59309 change default sas timeout for pupil submission tokens (#2533) * change default sas timeout for pupil submission tokens * increase retry count for check submission * add exponential retry config to polly for check submission * remove debug output --------- Co-authored-by: Jon Shipley <jon-shipley@users.noreply.github.com> --- admin/config.js | 2 +- pupil-spa/gen_config.sh | 2 +- .../app/services/http/http.service.spec.ts | 485 ++++++++++-------- .../src/app/services/http/http.service.ts | 11 +- 4 files changed, 270 insertions(+), 230 deletions(-) diff --git a/admin/config.js b/admin/config.js index 2d1639e01e..403603ed72 100644 --- a/admin/config.js +++ b/admin/config.js @@ -125,7 +125,7 @@ module.exports = { Tokens: { // 12 hours default expiry jwtTimeOutHours: process.env.JWT_TIMEOUT_HOURS || 12, - sasTimeOutHours: process.env.SAS_TIMEOUT_HOURS || 25 + sasTimeOutHours: process.env.SAS_TIMEOUT_HOURS || 120 }, Monitoring: { ApplicationInsights: { diff --git a/pupil-spa/gen_config.sh b/pupil-spa/gen_config.sh index dbadcc72fc..16e2102662 100755 --- a/pupil-spa/gen_config.sh +++ b/pupil-spa/gen_config.sh @@ -12,7 +12,7 @@ authUrl=${AUTH_URL:-"http://localhost:3003/auth"} checkStartAPIErrorDelay=${CHECK_START_ERROR_DELAY:-"2000"} checkStartAPIErrorMaxAttempts=${CHECK_START_MAX_ATTEMPTS:-"3"} checkSubmissionAPIErrorDelay=${CHECK_SUBMISSION_ERROR_DELAY:-"30000"} -checkSubmissionAPIErrorMaxAttempts=${CHECK_SUBMISSION_MAX_ATTEMPTS:-"3"} +checkSubmissionAPIErrorMaxAttempts=${CHECK_SUBMISSION_MAX_ATTEMPTS:-"10"} connectivityCheckEnabled=${CONNECTIVITY_CHECK_ENABLED:-"false"} connectivityCheckViewMinDisplay=${CONNECTIVITY_CHECK_MIN_DISPLAY:-"6000"} feedbackAPIErrorDelay=${CHECK_SUBMISSION_ERROR_DELAY:-"3000"} diff --git a/pupil-spa/src/app/services/http/http.service.spec.ts b/pupil-spa/src/app/services/http/http.service.spec.ts index c98015acab..d7b92c4b2f 100644 --- a/pupil-spa/src/app/services/http/http.service.spec.ts +++ b/pupil-spa/src/app/services/http/http.service.spec.ts @@ -1,23 +1,23 @@ -import { TestBed } from '@angular/core/testing'; -import { HttpService } from './http.service'; -import { HttpErrorResponse, HttpHeaders } from '@angular/common/http'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { defer } from 'rxjs/internal/observable/defer'; +import { TestBed } from '@angular/core/testing' +import { HttpService } from './http.service' +import { HttpErrorResponse, HttpHeaders } from '@angular/common/http' +import { HttpClientTestingModule } from '@angular/common/http/testing' +import { defer } from 'rxjs/internal/observable/defer' /** Create async observable that emits-once and completes * after a JS engine turn */ -export function asyncData<T>(data: T) { - return defer(() => Promise.resolve(data)); +export function asyncData<T> (data: T) { + return defer(() => Promise.resolve(data)) } /** Create async observable that emits-once and rejects * after a JS engine turn */ -export function asyncError<T>(data: T) { - return defer(() => Promise.reject(data)); +export function asyncError<T> (data: T) { + return defer(() => Promise.reject(data)) } describe('HttpService', () => { - let httpClientSpy: { get: jasmine.Spy, post: jasmine.Spy }; - let httpService: HttpService; + let httpClientSpy: { get: jasmine.Spy, post: jasmine.Spy, request: jasmine.Spy } + let httpService: HttpService beforeEach(() => { TestBed.configureTestingModule({ @@ -25,222 +25,255 @@ describe('HttpService', () => { providers: [ HttpService ] - }); + }) // Here is how to construct an actual HttpClient instance without DI // service = new HttpService(new HttpClient(new HttpXhrBackend({ build: () => new XMLHttpRequest() }))); - httpClientSpy = jasmine.createSpyObj('HttpClient', ['get', 'post']); - httpService = new HttpService(httpClientSpy as any); - }); + httpClientSpy = jasmine.createSpyObj('HttpClient', ['get', 'post', 'request']) + httpService = new HttpService(httpClientSpy as any) + }) it('should be created', () => { - expect(httpService).toBeTruthy(); - }); - - it('returns the http result from a post on success', async () => { - try { - const expectedResponse = { checkCode: '1111-2222-3333-4444' }; - httpClientSpy.post.and.returnValue(asyncData(expectedResponse)); - const res = await httpService.postJson('http://localhost', { foo: 'bar' }); - expect(res.checkCode).toBeTruthy(); - expect(httpClientSpy.post).toHaveBeenCalledTimes(1); - } catch (error) { - fail(error); - } - }); - - it('returns the httpErrorResponse from a post on 401 unauthorised, single call only', async () => { - try { - httpClientSpy.post.and.returnValue(asyncError(new HttpErrorResponse({ - error: { error: 'Unauthorised' }, - status: 401, - statusText: 'Unauthorized' - }))); - await httpService.postJson('http://localhost', { foo: 'bar' }); - fail('Should have thrown'); - } catch (error) { - expect(error.status).toBe(401); - expect(error.statusText).toBe('Unauthorized'); - expect(error.error).toEqual({ error: 'Unauthorised' }); - expect(httpClientSpy.post).toHaveBeenCalledTimes(1); - } - }); - - it('retries if the error code is 408 - Request Timeout', async () => { - const expectedResponse = { checkCode: '1111-2222-3333-4444' }; - httpClientSpy.post.and - .returnValues( - // 1st response - asyncError(new HttpErrorResponse({ - error: 'Request Timeout', - status: 408, - statusText: 'Request Timeout' - })), - // 2nd response - asyncData(expectedResponse) - ); - const res = await httpService.postJson('http://localhost', { foo: 'bar' }); - expect(res.checkCode).toBeTruthy(); - expect(httpClientSpy.post).toHaveBeenCalledTimes(2); - }); - - it('retries if the error code is 429 - Too many requests', async () => { - const expectedResponse = { checkCode: '1111-2222-3333-4444' }; - httpClientSpy.post.and - .returnValues( - // 1st response - asyncError(new HttpErrorResponse({ - error: 'Too many requests', - status: 429, - statusText: 'Too many requests' - })), - // 2nd response - asyncData(expectedResponse) - ); - const res = await httpService.postJson('http://localhost', { foo: 'bar' }); - expect(res.checkCode).toBeTruthy(); - expect(httpClientSpy.post).toHaveBeenCalledTimes(2); - }); - - it('retries if the error code is 500 - Server Error', async () => { - const expectedResponse = { checkCode: '1111-2222-3333-4444' }; - httpClientSpy.post.and - .returnValues( - // 1st response - asyncError(new HttpErrorResponse({ - error: 'Server error', - status: 500, - statusText: 'Server error' - })), - // 2nd response - asyncData(expectedResponse) - ); - const res = await httpService.postJson('http://localhost', { foo: 'bar' }); - expect(res.checkCode).toBeTruthy(); - expect(httpClientSpy.post).toHaveBeenCalledTimes(2); - }); - - it('retries if the error code is 502 - Bad gateway', async () => { - const expectedResponse = { checkCode: '1111-2222-3333-4444' }; - httpClientSpy.post.and - .returnValues( - // 1st response - asyncError(new HttpErrorResponse({ - error: 'Bad gateway', - status: 502, - statusText: 'Bad gateway' - })), - // 2nd response - asyncData(expectedResponse) - ); - const res = await httpService.postJson('http://localhost', { foo: 'bar' }); - expect(res.checkCode).toBeTruthy(); - expect(httpClientSpy.post).toHaveBeenCalledTimes(2); - }); - - it('retries if the error code is 503 - Service unavailable', async () => { - const expectedResponse = { checkCode: '1111-2222-3333-4444' }; - httpClientSpy.post.and - .returnValues( - // 1st response - asyncError(new HttpErrorResponse({ - error: 'Service unavailable', - status: 503, - statusText: 'Service unavailable' - })), - // 2nd response - asyncData(expectedResponse) - ); - const res = await httpService.postJson('http://localhost', { foo: 'bar' }); - expect(res.checkCode).toBeTruthy(); - expect(httpClientSpy.post).toHaveBeenCalledTimes(2); - }); - - it('retries if the error code is 504 - Gateway timeout', async () => { - const expectedResponse = { checkCode: '1111-2222-3333-4444' }; - httpClientSpy.post.and - .returnValues( - // 1st response - asyncError(new HttpErrorResponse({ - error: 'Gateway timeout', - status: 504, - statusText: 'Gateway timeout' - })), - // 2nd response - asyncData(expectedResponse) - ); - const res = await httpService.postJson('http://localhost', { foo: 'bar' }); - expect(res.checkCode).toBeTruthy(); - expect(httpClientSpy.post).toHaveBeenCalledTimes(2); - }); - - it('retries 5 times', async () => { - const expectedResponse = { checkCode: '1111-2222-3333-4444' }; - httpClientSpy.post.and - .returnValues( - // initial request - asyncError(new HttpErrorResponse({ - error: 'Gateway timeout', - status: 504, - statusText: 'Gateway timeout', - headers: new HttpHeaders({ 'X-jms': (new Date()).toUTCString() }) - })), - // 1st retry - asyncError(new HttpErrorResponse({ - error: 'Server error', - status: 500, - statusText: 'Server error' - })), - // 2nd retry - asyncError(new HttpErrorResponse({ - error: 'Service Unavailable', - status: 503, - statusText: 'Service Unavailable' - })), - // 3rd retry - asyncError(new HttpErrorResponse({ - error: 'Request timeout', - status: 408, - statusText: 'Request timeout' - })), - // 4th retry - asyncError(new HttpErrorResponse({ - error: 'Bad gateway', - status: 502, - statusText: 'Bad gateway' - })), - // 5th retry - asyncData(expectedResponse) - ); - const startTime = Date.now(); - const res = await httpService.postJson('http://localhost', { foo: 'bar' }); - const endTime = Date.now(); - expect(res.checkCode).toBeTruthy(); - expect(httpClientSpy.post).toHaveBeenCalledTimes(6); - // Retry timings are: - // original call: 0 - // call 1: +100ms - // call 2: +200 - // call 3: +400 - // call 4: +800 - // call 5: +1600 - expect(endTime - startTime).toBeGreaterThanOrEqual(1600 + 800 + 400 + 200 + 100); - }); - - it('retries if there is a client side error', async () => { - const expectedResponse = { checkCode: '1111-2222-3333-4444' }; - httpClientSpy.post.and - .returnValues( - // 1st response - asyncError(new HttpErrorResponse({ - error: new ErrorEvent('Connect Failure'), - status: 400, - statusText: 'Connect Failure' - })), - // 2nd response - asyncData(expectedResponse) - ); - const res = await httpService.postJson('http://localhost', { foo: 'bar' }); - expect(res.checkCode).toBeTruthy(); - expect(httpClientSpy.post).toHaveBeenCalledTimes(2); - }); -}); // end describe + expect(httpService).toBeTruthy() + }) + + describe('postJson', () => { + it('returns the http result from a post on success', async () => { + try { + const expectedResponse = { checkCode: '1111-2222-3333-4444' } + httpClientSpy.post.and.returnValue(asyncData(expectedResponse)) + const res = await httpService.postJson('http://localhost', { foo: 'bar' }) + expect(res.checkCode).toBeTruthy() + expect(httpClientSpy.post).toHaveBeenCalledTimes(1) + } catch (error) { + fail(error) + } + }) + + it('returns the httpErrorResponse from a post on 401 unauthorised, single call only', async () => { + try { + httpClientSpy.post.and.returnValue(asyncError(new HttpErrorResponse({ + error: { error: 'Unauthorised' }, + status: 401, + statusText: 'Unauthorized' + }))) + await httpService.postJson('http://localhost', { foo: 'bar' }) + fail('Should have thrown') + } catch (error) { + expect(error.status).toBe(401) + expect(error.statusText).toBe('Unauthorized') + expect(error.error).toEqual({ error: 'Unauthorised' }) + expect(httpClientSpy.post).toHaveBeenCalledTimes(1) + } + }) + + it('retries if the error code is 408 - Request Timeout', async () => { + const expectedResponse = { checkCode: '1111-2222-3333-4444' } + httpClientSpy.post.and + .returnValues( + // 1st response + asyncError(new HttpErrorResponse({ + error: 'Request Timeout', + status: 408, + statusText: 'Request Timeout' + })), + // 2nd response + asyncData(expectedResponse) + ) + const res = await httpService.postJson('http://localhost', { foo: 'bar' }) + expect(res.checkCode).toBeTruthy() + expect(httpClientSpy.post).toHaveBeenCalledTimes(2) + }) + + it('retries if the error code is 429 - Too many requests', async () => { + const expectedResponse = { checkCode: '1111-2222-3333-4444' } + httpClientSpy.post.and + .returnValues( + // 1st response + asyncError(new HttpErrorResponse({ + error: 'Too many requests', + status: 429, + statusText: 'Too many requests' + })), + // 2nd response + asyncData(expectedResponse) + ) + const res = await httpService.postJson('http://localhost', { foo: 'bar' }) + expect(res.checkCode).toBeTruthy() + expect(httpClientSpy.post).toHaveBeenCalledTimes(2) + }) + + it('retries if the error code is 500 - Server Error', async () => { + const expectedResponse = { checkCode: '1111-2222-3333-4444' } + httpClientSpy.post.and + .returnValues( + // 1st response + asyncError(new HttpErrorResponse({ + error: 'Server error', + status: 500, + statusText: 'Server error' + })), + // 2nd response + asyncData(expectedResponse) + ) + const res = await httpService.postJson('http://localhost', { foo: 'bar' }) + expect(res.checkCode).toBeTruthy() + expect(httpClientSpy.post).toHaveBeenCalledTimes(2) + }) + + it('retries if the error code is 502 - Bad gateway', async () => { + const expectedResponse = { checkCode: '1111-2222-3333-4444' } + httpClientSpy.post.and + .returnValues( + // 1st response + asyncError(new HttpErrorResponse({ + error: 'Bad gateway', + status: 502, + statusText: 'Bad gateway' + })), + // 2nd response + asyncData(expectedResponse) + ) + const res = await httpService.postJson('http://localhost', { foo: 'bar' }) + expect(res.checkCode).toBeTruthy() + expect(httpClientSpy.post).toHaveBeenCalledTimes(2) + }) + + it('retries if the error code is 503 - Service unavailable', async () => { + const expectedResponse = { checkCode: '1111-2222-3333-4444' } + httpClientSpy.post.and + .returnValues( + // 1st response + asyncError(new HttpErrorResponse({ + error: 'Service unavailable', + status: 503, + statusText: 'Service unavailable' + })), + // 2nd response + asyncData(expectedResponse) + ) + const res = await httpService.postJson('http://localhost', { foo: 'bar' }) + expect(res.checkCode).toBeTruthy() + expect(httpClientSpy.post).toHaveBeenCalledTimes(2) + }) + + it('retries if the error code is 504 - Gateway timeout', async () => { + const expectedResponse = { checkCode: '1111-2222-3333-4444' } + httpClientSpy.post.and + .returnValues( + // 1st response + asyncError(new HttpErrorResponse({ + error: 'Gateway timeout', + status: 504, + statusText: 'Gateway timeout' + })), + // 2nd response + asyncData(expectedResponse) + ) + const res = await httpService.postJson('http://localhost', { foo: 'bar' }) + expect(res.checkCode).toBeTruthy() + expect(httpClientSpy.post).toHaveBeenCalledTimes(2) + }) + + it('retries 5 times', async () => { + const expectedResponse = { checkCode: '1111-2222-3333-4444' } + httpClientSpy.post.and + .returnValues( + // initial request + asyncError(new HttpErrorResponse({ + error: 'Gateway timeout', + status: 504, + statusText: 'Gateway timeout', + headers: new HttpHeaders({ 'X-jms': (new Date()).toUTCString() }) + })), + // 1st retry + asyncError(new HttpErrorResponse({ + error: 'Server error', + status: 500, + statusText: 'Server error' + })), + // 2nd retry + asyncError(new HttpErrorResponse({ + error: 'Service Unavailable', + status: 503, + statusText: 'Service Unavailable' + })), + // 3rd retry + asyncError(new HttpErrorResponse({ + error: 'Request timeout', + status: 408, + statusText: 'Request timeout' + })), + // 4th retry + asyncError(new HttpErrorResponse({ + error: 'Bad gateway', + status: 502, + statusText: 'Bad gateway' + })), + // 5th retry + asyncData(expectedResponse) + ) + const startTime = Date.now() + const res = await httpService.postJson('http://localhost', { foo: 'bar' }) + const endTime = Date.now() + expect(res.checkCode).toBeTruthy() + expect(httpClientSpy.post).toHaveBeenCalledTimes(6) + // Retry timings are: + // original call: 0 + // call 1: +100ms + // call 2: +200 + // call 3: +400 + // call 4: +800 + // call 5: +1600 + expect(endTime - startTime).toBeGreaterThanOrEqual(1600 + 800 + 400 + 200 + 100) + }) + + it('retries if there is a client side error', async () => { + const expectedResponse = { checkCode: '1111-2222-3333-4444' } + httpClientSpy.post.and + .returnValues( + // 1st response + asyncError(new HttpErrorResponse({ + error: new ErrorEvent('Connect Failure'), + status: 400, + statusText: 'Connect Failure' + })), + // 2nd response + asyncData(expectedResponse) + ) + const res = await httpService.postJson('http://localhost', { foo: 'bar' }) + expect(res.checkCode).toBeTruthy() + expect(httpClientSpy.post).toHaveBeenCalledTimes(2) + }) + + }) + + describe('postXml', () => { + const originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL + beforeAll(() => { + jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000 + }) + + afterAll(() => { + jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout + }) + + it('tries the default number of times with exponential backoff', async () => { + httpClientSpy.request.and + .returnValue( + asyncError(new HttpErrorResponse({ + error: 'Server error', + status: 500, + statusText: 'Server error' + })), + ) + const retryCount = 3; + try { + await httpService.postXml('http://localhost', '<xml></xml>', new HttpHeaders(), retryCount) + fail('Should have thrown') + } catch (error) { + expect(httpClientSpy.request).toHaveBeenCalledTimes(retryCount + 1); + } + }) + + }) // end postXml describe +}) // end main describe diff --git a/pupil-spa/src/app/services/http/http.service.ts b/pupil-spa/src/app/services/http/http.service.ts index 85dfd0967f..7fd9313433 100644 --- a/pupil-spa/src/app/services/http/http.service.ts +++ b/pupil-spa/src/app/services/http/http.service.ts @@ -44,7 +44,14 @@ export class HttpService { public async postXml(url: string, body: any, headers: HttpHeaders, retryCount = this.defaultRetryCount): Promise<any> { const _that = this; - _that.config.retryTimes = retryCount; + const retryConfig = new Array<number>(); + const baseDelay = 3000; + const increaseFactor = 1.5; + retryConfig.push(baseDelay); + for (let i = 1; i < retryCount; i++) { + const instanceDelay = Math.round(increaseFactor * retryConfig[i - 1]); + retryConfig.push(instanceDelay); + } const response = await polly() .handle(error => { // Any requests that don't give a 200 status code will raise an Exception that can be handled here. @@ -86,7 +93,7 @@ export class HttpService { console.warn('http-service: error: unusual error response (will not retry):', error); return false; // shouldn't get here }) - .waitAndRetry(_that.config.retryTimes) + .waitAndRetry(retryConfig) .executeForPromise(function () { return _that.http.request('POST', url, { headers: headers.set('Content-Type', 'text/xml'),