diff --git a/packages/calling/src/CallingClient/constants.ts b/packages/calling/src/CallingClient/constants.ts index 3e325e2c454..c3b8cc87fc4 100644 --- a/packages/calling/src/CallingClient/constants.ts +++ b/packages/calling/src/CallingClient/constants.ts @@ -107,6 +107,7 @@ export const SEC_TO_MSEC_MFACTOR = 1000; export const MINUTES_TO_SEC_MFACTOR = 60; export const REG_RANDOM_T_FACTOR_UPPER_LIMIT = 10000; export const REG_TRY_BACKUP_TIMER_VAL_IN_SEC = 1200; +export const REG_TRY_BACKUP_TIMER_VAL_FOR_CC_IN_SEC = 114; export const REG_FAILBACK_429_MAX_RETRIES = 5; export const REGISTER_UTIL = 'registerDevice'; export const GET_MOBIUS_SERVERS_UTIL = 'getMobiusServers'; diff --git a/packages/calling/src/CallingClient/registration/register.test.ts b/packages/calling/src/CallingClient/registration/register.test.ts index 0f0da477562..8283b073b86 100644 --- a/packages/calling/src/CallingClient/registration/register.test.ts +++ b/packages/calling/src/CallingClient/registration/register.test.ts @@ -22,6 +22,7 @@ import { KEEPALIVE_UTIL, MINUTES_TO_SEC_MFACTOR, REGISTRATION_FILE, + REG_TRY_BACKUP_TIMER_VAL_FOR_CC_IN_SEC, REG_TRY_BACKUP_TIMER_VAL_IN_SEC, SEC_TO_MSEC_MFACTOR, } from '../constants'; @@ -64,6 +65,17 @@ describe('Registration Tests', () => { }, }; + const ccMockResponse = { + ...mockResponse, + body: { + ...mockResponse.body, + serviceData: { + domain: '', + indicator: 'contactcenter', + }, + }, + }; + const failurePayload = ({ statusCode: 500, body: mockPostResponse, @@ -85,15 +97,19 @@ describe('Registration Tests', () => { let restoreSpy; let postRegistrationSpy; - beforeEach(() => { + const setupRegistration = (mockServiceData) => { const mutex = new Mutex(); - reg = createRegistration(webex, MockServiceData, mutex, lineEmitter, LOGGER.INFO); + reg = createRegistration(webex, mockServiceData, mutex, lineEmitter, LOGGER.INFO); reg.setMobiusServers(mobiusUris.primary, mobiusUris.backup); jest.clearAllMocks(); restartSpy = jest.spyOn(reg, 'restartRegistration'); failbackRetry429Spy = jest.spyOn(reg, FAILBACK_429_RETRY_UTIL); restoreSpy = jest.spyOn(reg, 'restorePreviousRegistration'); postRegistrationSpy = jest.spyOn(reg, 'postRegistration'); + }; + + beforeEach(() => { + setupRegistration(MockServiceData); }); afterEach(() => { @@ -218,6 +234,36 @@ describe('Registration Tests', () => { expect(reg.getActiveMobiusUrl()).toEqual(mobiusUris.backup[0]); }); + it('cc: verify unreachable primary with reachable backup server', async () => { + setupRegistration({...MockServiceData, indicator: ServiceIndicator.CONTACT_CENTER}); + + jest.useFakeTimers(); + webex.request + .mockRejectedValueOnce(failurePayload) + .mockRejectedValueOnce(failurePayload) + .mockResolvedValueOnce(successPayload); + + expect(reg.getStatus()).toEqual(RegistrationStatus.IDLE); + await reg.triggerRegistration(); + jest.advanceTimersByTime(REG_TRY_BACKUP_TIMER_VAL_FOR_CC_IN_SEC * SEC_TO_MSEC_MFACTOR); + await flushPromises(); + + expect(webex.request).toBeCalledTimes(3); + expect(webex.request).toBeCalledWith({ + ...ccMockResponse, + method: 'POST', + uri: `${mobiusUris.primary[0]}device`, + }); + expect(webex.request).toBeCalledWith({ + ...ccMockResponse, + method: 'POST', + uri: `${mobiusUris.backup[0]}device`, + }); + expect(reg.getStatus()).toEqual(RegistrationStatus.ACTIVE); + /* Active Url must match with the backup url as per the test */ + expect(reg.getActiveMobiusUrl()).toEqual(mobiusUris.backup[0]); + }); + it('verify unreachable primary and backup servers', async () => { jest.useFakeTimers(); // try the primary twice and register successfully with backup servers @@ -444,15 +490,14 @@ describe('Registration Tests', () => { file: REGISTRATION_FILE, method: 'startKeepaliveTimer', }; - const mockKeepAliveBody = {device: mockPostResponse.device}; - beforeEach(async () => { + const beforeEachSetupForKeepalive = async () => { postRegistrationSpy.mockResolvedValueOnce(successPayload); jest.useFakeTimers(); await reg.triggerRegistration(); expect(reg.getStatus()).toBe(RegistrationStatus.ACTIVE); - }); + }; afterEach(() => { jest.clearAllTimers(); @@ -471,6 +516,7 @@ describe('Registration Tests', () => { }); it('verify successful keep-alive cases', async () => { + await beforeEachSetupForKeepalive(); const keepAlivePayload = ({ statusCode: 200, body: mockKeepAliveBody, @@ -487,6 +533,7 @@ describe('Registration Tests', () => { }); it('verify failure keep-alive cases: Retry Success', async () => { + await beforeEachSetupForKeepalive(); const failurePayload = ({ statusCode: 503, body: mockKeepAliveBody, @@ -517,6 +564,7 @@ describe('Registration Tests', () => { }); it('verify failure keep-alive cases: Restore failure', async () => { + await beforeEachSetupForKeepalive(); const restoreSpy = jest.spyOn(reg, 'restorePreviousRegistration'); const restartRegSpy = jest.spyOn(reg, 'restartRegistration'); const reconnectSpy = jest.spyOn(reg, 'reconnectOnFailure'); @@ -565,6 +613,7 @@ describe('Registration Tests', () => { }); it('verify failure keep-alive cases: Restore Success', async () => { + await beforeEachSetupForKeepalive(); const restoreSpy = jest.spyOn(reg, 'restorePreviousRegistration'); const restartRegSpy = jest.spyOn(reg, 'restartRegistration'); const reconnectSpy = jest.spyOn(reg, 'reconnectOnFailure'); @@ -616,6 +665,7 @@ describe('Registration Tests', () => { }); it('verify failure followed by recovery of keepalive', async () => { + await beforeEachSetupForKeepalive(); const failurePayload = ({ statusCode: 503, body: mockKeepAliveBody, @@ -647,7 +697,53 @@ describe('Registration Tests', () => { expect(reg.keepaliveTimer).toBe(timer); }); + it('cc: verify failover to backup server after 4 keep alive failure with primary server', async () => { + // Register with contact center service + setupRegistration({...MockServiceData, indicator: ServiceIndicator.CONTACT_CENTER}); + await beforeEachSetupForKeepalive(); + + const failurePayload = ({ + statusCode: 503, + body: mockKeepAliveBody, + }); + const successPayload = ({ + statusCode: 200, + body: mockKeepAliveBody, + }); + + const clearIntervalSpy = jest.spyOn(global, 'clearInterval'); + + jest + .spyOn(reg, 'postKeepAlive') + .mockRejectedValueOnce(failurePayload) + .mockRejectedValueOnce(failurePayload) + .mockRejectedValueOnce(failurePayload) + .mockRejectedValueOnce(failurePayload) + .mockResolvedValue(successPayload); + + expect(reg.getStatus()).toBe(RegistrationStatus.ACTIVE); + + const timer = reg.keepaliveTimer; + + jest.advanceTimersByTime(5 * mockPostResponse.keepaliveInterval * SEC_TO_MSEC_MFACTOR); + await flushPromises(); + + expect(clearIntervalSpy).toBeCalledOnceWith(timer); + expect(reg.getStatus()).toBe(RegistrationStatus.INACTIVE); + expect(reg.keepaliveTimer).not.toBe(timer); + + webex.request.mockRejectedValueOnce(failurePayload).mockResolvedValue(successPayload); + + jest.advanceTimersByTime(REG_TRY_BACKUP_TIMER_VAL_FOR_CC_IN_SEC * SEC_TO_MSEC_MFACTOR); + await flushPromises(); + + /* Active Url must match with the backup url as per the test */ + expect(reg.getActiveMobiusUrl()).toEqual(mobiusUris.backup[0]); + expect(reg.getStatus()).toBe(RegistrationStatus.ACTIVE); + }); + it('verify final error for keep-alive', async () => { + await beforeEachSetupForKeepalive(); const restoreSpy = jest.spyOn(reg, 'restorePreviousRegistration'); const restartRegSpy = jest.spyOn(reg, 'restartRegistration'); const reconnectSpy = jest.spyOn(reg, 'reconnectOnFailure'); @@ -686,6 +782,7 @@ describe('Registration Tests', () => { }); it('verify failure keep-alive case with active call present: Restore Success after call ends', async () => { + await beforeEachSetupForKeepalive(); const restoreSpy = jest.spyOn(reg, 'restorePreviousRegistration'); const restartRegSpy = jest.spyOn(reg, 'restartRegistration'); const reconnectSpy = jest.spyOn(reg, 'reconnectOnFailure'); diff --git a/packages/calling/src/CallingClient/registration/register.ts b/packages/calling/src/CallingClient/registration/register.ts index 862f827300e..297c8fef62e 100644 --- a/packages/calling/src/CallingClient/registration/register.ts +++ b/packages/calling/src/CallingClient/registration/register.ts @@ -17,6 +17,7 @@ import { IDeviceInfo, RegistrationStatus, ServiceData, + ServiceIndicator, WebexRequestPayload, } from '../../common/types'; import {ISDKConnector, WebexSDK} from '../../SDKConnector/types'; @@ -39,6 +40,7 @@ import { DEFAULT_REHOMING_INTERVAL_MIN, DEFAULT_REHOMING_INTERVAL_MAX, DEFAULT_KEEPALIVE_INTERVAL, + REG_TRY_BACKUP_TIMER_VAL_FOR_CC_IN_SEC, } from '../constants'; import {LINE_EVENTS, LineEmitterCallback} from '../line/types'; import {LineError} from '../../Errors/catalog/LineError'; @@ -73,6 +75,7 @@ export class Registration implements IRegistration { private backupMobiusUris: string[]; private registerRetry = false; private reconnectPending = false; + private isCCFlow = false; /** */ @@ -85,6 +88,8 @@ export class Registration implements IRegistration { ) { this.sdkConnector = SDKConnector; this.serviceData = serviceData; + this.isCCFlow = serviceData.indicator === ServiceIndicator.CONTACT_CENTER; + if (!this.sdkConnector.getWebex()) { SDKConnector.setWebex(webex); } @@ -257,8 +262,12 @@ export class Registration implements IRegistration { let interval = this.getRegRetryInterval(attempt); - if (timeElapsed + interval > REG_TRY_BACKUP_TIMER_VAL_IN_SEC) { - const excessVal = timeElapsed + interval - REG_TRY_BACKUP_TIMER_VAL_IN_SEC; + const TIMER_THRESHOLD = this.isCCFlow + ? REG_TRY_BACKUP_TIMER_VAL_FOR_CC_IN_SEC + : REG_TRY_BACKUP_TIMER_VAL_IN_SEC; + + if (timeElapsed + interval > TIMER_THRESHOLD) { + const excessVal = timeElapsed + interval - TIMER_THRESHOLD; interval -= excessVal; } @@ -681,13 +690,15 @@ export class Registration implements IRegistration { private startKeepaliveTimer(url: string, interval: number) { let keepAliveRetryCount = 0; this.clearKeepaliveTimer(); + const RETRY_COUNT_THRESHOLD = this.isCCFlow ? 4 : 5; + this.keepaliveTimer = setInterval(async () => { const logContext = { file: REGISTRATION_FILE, method: this.startKeepaliveTimer.name, }; await this.mutex.runExclusive(async () => { - if (this.isDeviceRegistered() && keepAliveRetryCount < 5) { + if (this.isDeviceRegistered() && keepAliveRetryCount < RETRY_COUNT_THRESHOLD) { try { const res = await this.postKeepAlive(url); log.info(`Sent Keepalive, status: ${res.statusCode}`, logContext); @@ -720,7 +731,7 @@ export class Registration implements IRegistration { {method: this.startKeepaliveTimer.name, file: REGISTRATION_FILE} ); - if (abort || keepAliveRetryCount >= 5) { + if (abort || keepAliveRetryCount >= RETRY_COUNT_THRESHOLD) { this.setStatus(RegistrationStatus.INACTIVE); this.clearKeepaliveTimer(); this.clearFailbackTimer();