Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 25 additions & 29 deletions packages/calling/src/CallingClient/calling/call.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -963,10 +963,11 @@ describe('State Machine handler tests', () => {
await flushPromises(3);

expect(setInterval).toHaveBeenCalledTimes(1);
expect(setInterval).toHaveBeenCalledWith(expect.any(Function), DEFAULT_SESSION_TIMER);
expect(funcSpy).toBeCalledTimes(1);
expect(logSpy).toBeCalledWith('Session refresh successful', {
file: 'call',
method: 'handleCallEstablished',
method: 'scheduleCallKeepaliveInterval',
});
expect(logSpy).toHaveBeenCalledWith(
`${METHOD_START_MESSAGE} with: ${call.getCorrelationId()}`,
Expand Down Expand Up @@ -1023,23 +1024,16 @@ describe('State Machine handler tests', () => {

const funcSpy = jest.spyOn(call, 'postStatus').mockRejectedValue(statusPayload);

if (call['sessionTimer'] === undefined) {
/* In cases where this test is run independently/alone, there is no sessionTimer initiated
Thus we will check and initialize the timer when not present by calling handleCallEstablish() */
call['handleCallEstablished']({} as CallEvent);
}
call['handleCallEstablished']({} as CallEvent);

jest.advanceTimersByTime(DEFAULT_SESSION_TIMER);

/* This is to flush all the promises from the Promise queue so that
* Jest.fakeTimers can advance time and also clear the promise Queue
*/
await flushPromises(2);

await Promise.resolve();
await Promise.resolve();

expect(clearInterval).toHaveBeenCalledTimes(2); // check this
expect(clearInterval).toHaveBeenCalledTimes(1);
expect(funcSpy).toBeCalledTimes(1);
});

Expand All @@ -1053,7 +1047,7 @@ describe('State Machine handler tests', () => {

const okPayload = <WebexRequestPayload>(<unknown>{statusCode: 200, body: {}});

const sendEvtSpy = jest.spyOn(call as any, 'sendCallStateMachineEvt');
const scheduleKeepaliveSpy = jest.spyOn(call as any, 'scheduleCallKeepaliveInterval');
const postStatusSpy = jest
.spyOn(call as any, 'postStatus')
.mockRejectedValueOnce(errorPayload)
Expand All @@ -1064,19 +1058,19 @@ describe('State Machine handler tests', () => {
}

jest.advanceTimersByTime(DEFAULT_SESSION_TIMER);
await Promise.resolve();
await Promise.resolve();
await flushPromises(2);

expect(postStatusSpy).toHaveBeenCalledTimes(1);
expect(sendEvtSpy).toHaveBeenCalledWith({type: 'E_CALL_ESTABLISHED'});

// Now advance by 1 second for the retry-after interval
jest.advanceTimersByTime(1000);
await flushPromises(2);

expect(postStatusSpy).toHaveBeenCalledTimes(2);
expect(scheduleKeepaliveSpy).toHaveBeenCalledTimes(2);
});

it('keepalive ends after reaching max retry count', async () => {
const resolvePromise = async () => {
await Promise.resolve();
await Promise.resolve();
};

const errorPayload = <WebexRequestPayload>(<unknown>{
statusCode: 500,
headers: {
Expand All @@ -1097,30 +1091,32 @@ describe('State Machine handler tests', () => {

// Advance timer to trigger the first failure (uses DEFAULT_SESSION_TIMER)
jest.advanceTimersByTime(DEFAULT_SESSION_TIMER);
await resolvePromise();
await flushPromises(2);

// Now advance by 1 second for each of the 3 more retry attempts (retry-after: 1 second each)
// Now advance by 1 second for each of the 4 retry attempts (retry-after: 1 second each)
// Need to do this separately to allow state machine to process and create new intervals
jest.advanceTimersByTime(1000);
await resolvePromise();
await flushPromises(2);

jest.advanceTimersByTime(1000);
await resolvePromise();
await flushPromises(2);

jest.advanceTimersByTime(1000);
await flushPromises(2);

jest.advanceTimersByTime(1000);
await resolvePromise();
await flushPromises(2);

// The error handler should detect we're at max retry count and stop
expect(warnSpy).toHaveBeenCalledWith(
`Max call keepalive retry attempts reached for call: ${call.getCorrelationId()}`,
`Max keepalive retry attempts reached. Aborting call keepalive for callId: ${call.getCallId()}`,
{
file: 'call',
method: 'handleCallEstablished',
method: 'keepaliveRetryCallback',
}
);
expect(postStatusSpy).toHaveBeenCalledTimes(4);
expect(call['callKeepaliveRetryCount']).toBe(0);
expect(call['sessionTimer']).toBeUndefined();
expect(postStatusSpy).toHaveBeenCalledTimes(5);
expect(call['callKeepaliveRetryCount']).toBe(4);
});

it('state changes during successful incoming call', async () => {
Expand Down
161 changes: 83 additions & 78 deletions packages/calling/src/CallingClient/calling/call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,6 @@ export class Call extends Eventing<CallEventTypes> implements ICall {

private callKeepaliveRetryCount = 0;

private callKeepaliveInterval?: number;

/**
* Getter to check if the call is muted or not.
*
Expand Down Expand Up @@ -1499,103 +1497,110 @@ export class Call extends Eventing<CallEventTypes> implements ICall {
this.sendCallStateMachineEvt({type: 'E_CALL_CLEARED'});
}

/**
* Handle Call Established - Roap related negotiations.
*
* @param event - Call Events.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
private handleCallEstablished(event: CallEvent) {
const loggerContext = {
file: CALL_FILE,
method: METHODS.HANDLE_CALL_ESTABLISHED,
};
private callKeepaliveRetryCallback = (interval: number) => {
if (this.callKeepaliveRetryCount === MAX_CALL_KEEPALIVE_RETRY_COUNT) {
log.warn(
`Max keepalive retry attempts reached. Aborting call keepalive for callId: ${this.callId}`,
{
file: CALL_FILE,
method: 'keepaliveRetryCallback',
}
);

log.info(`${METHOD_START_MESSAGE} with: ${this.getCorrelationId()}`, loggerContext);
return;
}

this.emit(CALL_EVENT_KEYS.ESTABLISHED, this.correlationId);
this.callKeepaliveRetryCount += 1;

/* Reset Early dialog parameters */
this.earlyMedia = false;
setTimeout(async () => {
try {
await this.postStatus();
this.scheduleCallKeepaliveInterval();
} catch (err: unknown) {
await this.handleCallKeepaliveError(err);
}
}, interval * 1000);
};

this.connected = true;
private handleCallKeepaliveError = async (err: unknown) => {
const error = <WebexRequestPayload>err;

/* Session timers need to be reset at all offer/answer exchanges */
/* We are clearing the timer here as all are error scenarios. Only scenario where
* timer reset won't be required is 503 with retry after. But that case will
* be handled automatically as Mobius will also reset timer when we post status
* in retry-after scenario.
*/
/* istanbul ignore next */
if (this.sessionTimer) {
log.log('Resetting session timer', loggerContext);

clearInterval(this.sessionTimer);
}

const abort = await handleCallErrors(
(callError: CallError) => {
this.emit(CALL_EVENT_KEYS.CALL_ERROR, callError);
this.submitCallErrorMetric(callError);
},
ERROR_LAYER.CALL_CONTROL,
this.callKeepaliveRetryCallback,
this.getCorrelationId(),
error,
'handleCallEstablished',
CALL_FILE
);

if (abort) {
this.sendCallStateMachineEvt({type: 'E_SEND_CALL_DISCONNECT'});
this.emit(CALL_EVENT_KEYS.DISCONNECT, this.getCorrelationId());
this.callKeepaliveRetryCount = 0;
}

await uploadLogs({
correlationId: this.correlationId,
callId: this.callId,
broadworksCorrelationInfo: this.broadworksCorrelationInfo,
});
};

private scheduleCallKeepaliveInterval = () => {
const loggerContext = {
file: CALL_FILE,
method: 'scheduleCallKeepaliveInterval',
};

this.sessionTimer = setInterval(async () => {
try {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const res = await this.postStatus();
this.callKeepaliveRetryCount = 0;
this.callKeepaliveInterval = undefined;

log.info(`Session refresh successful`, loggerContext);
} catch (err: unknown) {
const error = <WebexRequestPayload>err;
await this.handleCallKeepaliveError(err);
}
}, DEFAULT_SESSION_TIMER);
};

/* We are clearing the timer here as all are error scenarios. Only scenario where
* timer reset won't be required is 503 with retry after. But that case will
* be handled automatically as Mobius will also reset timer when we post status
* in retry-after scenario.
*/
/* istanbul ignore next */
if (this.sessionTimer) {
clearInterval(this.sessionTimer);
}
/**
* Handle Call Established - Roap related negotiations.
*
* @param event - Call Events.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
private handleCallEstablished(event: CallEvent) {
const loggerContext = {
file: CALL_FILE,
method: METHODS.HANDLE_CALL_ESTABLISHED,
};

const abort = await handleCallErrors(
(callError: CallError) => {
this.emit(CALL_EVENT_KEYS.CALL_ERROR, callError);
this.submitCallErrorMetric(callError);
},
ERROR_LAYER.CALL_CONTROL,
(interval: number) => {
this.callKeepaliveRetryCount += 1;
this.callKeepaliveInterval = interval * 1000;

// If we have reached the max retry count, do not attempt to refresh the session
if (this.callKeepaliveRetryCount === MAX_CALL_KEEPALIVE_RETRY_COUNT) {
this.callKeepaliveRetryCount = 0;
clearInterval(this.sessionTimer);
this.sessionTimer = undefined;
this.callKeepaliveInterval = undefined;

log.warn(
`Max call keepalive retry attempts reached for call: ${this.getCorrelationId()}`,
loggerContext
);

return;
}
log.info(`${METHOD_START_MESSAGE} with: ${this.getCorrelationId()}`, loggerContext);

// Scheduling next keepalive attempt - calling handleCallEstablished
this.sendCallStateMachineEvt({type: 'E_CALL_ESTABLISHED'});
},
this.getCorrelationId(),
error,
'handleCallEstablished',
CALL_FILE
);
this.emit(CALL_EVENT_KEYS.ESTABLISHED, this.correlationId);

if (abort) {
this.sendCallStateMachineEvt({type: 'E_SEND_CALL_DISCONNECT'});
this.emit(CALL_EVENT_KEYS.DISCONNECT, this.getCorrelationId());
this.callKeepaliveRetryCount = 0;
this.callKeepaliveInterval = undefined;
}
/* Reset Early dialog parameters */
this.earlyMedia = false;

await uploadLogs({
correlationId: this.correlationId,
callId: this.callId,
broadworksCorrelationInfo: this.broadworksCorrelationInfo,
});
}
}, this.callKeepaliveInterval || DEFAULT_SESSION_TIMER);
this.connected = true;

this.scheduleCallKeepaliveInterval();
}

/**
Expand Down
Loading