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
49 changes: 49 additions & 0 deletions packages/@webex/plugin-meetings/src/meeting/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@ import {LocusDTO} from '../locus-info/types';
// default callback so we don't call an undefined function, but in practice it should never be used
const DEFAULT_ICE_PHASE_CALLBACK = () => 'JOIN_MEETING_FINAL';

const LLM_HEALTHCHECK_TIMER_MS = 3 * 60 * 1000;

const logRequest = (request: any, {logText = ''}) => {
LoggerProxy.logger.info(`${logText} - sending request`);

Expand Down Expand Up @@ -756,6 +758,7 @@ export default class Meeting extends StatelessWebexPlugin {
private uploadLogsTimer?: ReturnType<typeof setTimeout>;
private logUploadIntervalIndex: number;
private mediaServerIp: string;
private llmHealthCheckTimer?: ReturnType<typeof setTimeout>;

/**
* @param {Object} attrs
Expand Down Expand Up @@ -6111,6 +6114,46 @@ export default class Meeting extends StatelessWebexPlugin {
});
}

/** starts a timer that after a few minutes checks if
* the LLM connection is connected, if not it sends a metric
* @private
* @returns {void}
*/
private startLLMHealthCheckTimer() {
// first cancel any existing timer
this.clearLLMHealthCheckTimer();

this.llmHealthCheckTimer = setTimeout(() => {
// @ts-ignore
const isConnected = this.webex.internal.llm.isConnected();

if (!isConnected) {
// @ts-ignore
const {hasEverConnected} = this.webex.internal.llm;

// only send metric if not connected - to avoid too many metrics
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.LLM_HEALTHCHECK_FAILURE, {
correlation_id: this.correlationId,
hasEverConnected,
});
}

this.llmHealthCheckTimer = undefined;
}, LLM_HEALTHCHECK_TIMER_MS);
}

/**
* Clears the LLM health check timer
* @private
* @returns {void}
*/
private clearLLMHealthCheckTimer() {
if (this.llmHealthCheckTimer) {
clearTimeout(this.llmHealthCheckTimer);
this.llmHealthCheckTimer = undefined;
}
}

/**
* Connects to low latency mercury and reconnects if the address has changed
* It will also disconnect if called when the meeting has ended
Expand Down Expand Up @@ -6153,6 +6196,8 @@ export default class Meeting extends StatelessWebexPlugin {
this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
// @ts-ignore - Fix type
this.webex.internal.llm.off(LOCUS_LLM_EVENT, this.processLocusLLMEvent);

this.clearLLMHealthCheckTimer();
}

if (!isJoined) {
Expand All @@ -6175,6 +6220,8 @@ export default class Meeting extends StatelessWebexPlugin {
'Meeting:index#updateLLMConnection --> enabled to receive relay events!'
);

this.startLLMHealthCheckTimer();

return Promise.resolve(registerAndConnectResult);
});
}
Expand Down Expand Up @@ -9424,6 +9471,8 @@ export default class Meeting extends StatelessWebexPlugin {
this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
// @ts-ignore - Fix type
this.webex.internal.llm.off(LOCUS_LLM_EVENT, this.processLocusLLMEvent);

this.clearLLMHealthCheckTimer();
};

/**
Expand Down
1 change: 1 addition & 0 deletions packages/@webex/plugin-meetings/src/metrics/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const BEHAVIORAL_METRICS = {
GET_DISPLAY_MEDIA_FAILURE: 'js_sdk_get_display_media_failures',
JOIN_WITH_MEDIA_FAILURE: 'js_sdk_join_with_media_failures',
LLM_CONNECTION_AFTER_JOIN_FAILURE: 'js_sdk_llm_connection_after_join_failure',
LLM_HEALTHCHECK_FAILURE: 'js_sdk_llm_healthcheck_failure',
RECEIVE_TRANSCRIPTION_AFTER_JOIN_FAILURE: 'js_sdk_receive_transcription_after_join_failure',

DISCONNECT_DUE_TO_INACTIVITY: 'js_sdk_disconnect_due_to_inactivity',
Expand Down
73 changes: 73 additions & 0 deletions packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,15 @@ describe('plugin-meetings', () => {
debug: () => {},
};

let fakeClock;

beforeEach(() => {
sinon.stub(Metrics, 'sendBehavioralMetric');
fakeClock = sinon.useFakeTimers();
});
afterEach(() => {
sinon.restore();
fakeClock.restore();
});

before(() => {
Expand Down Expand Up @@ -2160,6 +2164,75 @@ describe('plugin-meetings', () => {

assert.calledOnceWithExactly(locusInfoParseStub, meeting, eventData);
});

it('UpdateLLMConnection sends a metric if not connected after timeout', async () => {
sinon.stub(meeting, 'isJoined').returns(true);
sinon.stub(meeting.webex.internal.llm, 'isConnected').returns(false);
sinon.stub(meeting.webex.internal.llm, 'hasEverConnected').value(true);
sinon.stub(meeting.webex.internal.llm, 'registerAndConnect').resolves({});

// Restore the real updateLLMConnection
meeting.updateLLMConnection.restore();

// Call updateLLMConnection to start the timer
await meeting.updateLLMConnection();

// Fast forward time by 3 minutes
fakeClock.tick(3 * 60 * 1000);

assert.calledWith(
Metrics.sendBehavioralMetric,
BEHAVIORAL_METRICS.LLM_HEALTHCHECK_FAILURE,
{
correlation_id: meeting.correlationId,
hasEverConnected: true,
}
);
});

it('clears the LLM health check timer when disconnecting LLM', async () => {
const isJoinedStub = sinon.stub(meeting, 'isJoined');
sinon.stub(meeting.webex.internal.llm, 'isConnected');
sinon.stub(meeting.webex.internal.llm, 'disconnectLLM').resolves();
sinon.stub(meeting.webex.internal.llm, 'registerAndConnect').resolves({});
sinon
.stub(meeting.webex.internal.llm, 'getLocusUrl')
.returns('https://locus1.example.com');
sinon
.stub(meeting.webex.internal.llm, 'getDatachannelUrl')
.returns('https://datachannel1.example.com');

// Restore the real updateLLMConnection
meeting.updateLLMConnection.restore();

// First, connect LLM and start the timer
isJoinedStub.returns(true);
meeting.webex.internal.llm.isConnected.returns(false);
await meeting.updateLLMConnection();

// Verify timer was started
assert.exists(meeting.llmHealthCheckTimer);

// Now simulate that we're no longer joined
isJoinedStub.returns(false);
meeting.webex.internal.llm.isConnected.returns(true);

await meeting.updateLLMConnection();

assert.calledOnce(meeting.webex.internal.llm.disconnectLLM);

// Verify the timer was cleared (should be undefined)
assert.isUndefined(meeting.llmHealthCheckTimer);

// Fast forward time to ensure no metric is sent
Metrics.sendBehavioralMetric.resetHistory();
fakeClock.tick(3 * 60 * 1000);

assert.neverCalledWith(
Metrics.sendBehavioralMetric,
BEHAVIORAL_METRICS.LLM_HEALTHCHECK_FAILURE
);
});
});

describe('refreshPermissionToken', () => {
Expand Down
Loading