Skip to content
Open
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
16 changes: 15 additions & 1 deletion packages/@webex/plugin-meetings/src/hashTree/hashTreeParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export type HashTreeParserCallbacks = {
locusInfoUpdateCallback: LocusInfoUpdateCallback;
syncLatencyTracker?: SyncLatencyTracker;
generateTrackingId?: GenerateTrackingId;
isLlmExpected?: () => boolean;
};

const SYNC_METRICS_DATA_SETS = [
Expand Down Expand Up @@ -1837,14 +1838,27 @@ class HashTreeParser {
continue;
}

if (
LLM_DATASET_NAMES.includes(dataSet.name) &&
this.callbacks.isLlmExpected &&
!this.callbacks.isLlmExpected()
) {
Comment on lines +1841 to +1845

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Suppress already-armed LLM watchdogs

When LLM becomes not expected after an LLM dataset watchdog has already been scheduled, no further LLM dataset message is guaranteed to arrive and re-enter this guard, so the existing timeout still fires and enqueues /sync before the guard is evaluated again. In that path performSync() also restarts the root-hash sync timer, so the no-LLM case can continue issuing syncs; the expectation check needs to run in the timeout/enqueue path or LLM timers need to be cleared when the meeting stops expecting LLM.

Useful? React with 👍 / 👎.

LoggerProxy.logger.info(
`HashTreeParser#resetHeartbeatWatchdogs --> ${this.debugId} skipping heartbeat watchdog timer for data set "${dataSet.name}" because LLM is disconnected`
);

// eslint-disable-next-line no-continue
continue;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this code here handles the case when we call resetHeartbeatWatchdogs() for main and LLM is not connected, but what about the case when LLM is not connected yet, but it is connecting and later when it becomes connected, we will end up with no heartbeat watchdog. I think we need to check if we need to restart any heartbeat watchdogs when LLM changes from "not connected" to "connected".

}

const backoffTime = this.getWeightedBackoffTime(dataSet.backoff);
const delay = heartbeatIntervalMs + backoffTime;

dataSet.heartbeatWatchdogTimer = setTimeout(() => {
dataSet.heartbeatWatchdogTimer = undefined;

LoggerProxy.logger.warn(
`HashTreeParser#resetHeartbeatWatchdogs --> ${this.debugId} Heartbeat watchdog fired for data set "${dataSet.name}" - no heartbeat received within expected interval, initiating sync`
`HashTreeParser#resetHeartbeatWatchdogs --> ${this.debugId} Heartbeat watchdog fired for data set "${dataSet.name}" - no heartbeat received within expected interval(heartbeatIntervalMs=${heartbeatIntervalMs}, backoffTime=${backoffTime}), initiating sync`
);

Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.HASH_TREE_HEARTBEAT_WATCHDOG_EXPIRED, {
Expand Down
1 change: 1 addition & 0 deletions packages/@webex/plugin-meetings/src/locus-info/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,7 @@ export default class LocusInfo extends EventsScope {
callbacks: {
locusInfoUpdateCallback: this.updateFromHashTree.bind(this, locusUrl),
syncLatencyTracker: this.callbacks.syncLatencyTracker,
isLlmExpected: () => this.parsedLocus.self?.joinedWith?.state === 'JOINED',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use actual LLM channel state

When the LLM websocket drops while the meeting is still joined (for example, connect failed or the socket is reconnecting), joinedWith.state remains JOINED, so this callback reports true and the new watchdog guard still arms the main timer and keeps issuing /sync even though no LLM messages can arrive. Meeting#updateLLMConnection already treats joined state and webex.internal.llm.isConnected() as separate values, so pass the actual connection/ownership state here, or combine it with joined state, instead of using joinedWith alone.

Useful? React with 👍 / 👎.

// Reuse webex-core's tracking-id interceptor sequence (exposed publicly via
// webexTrackingIdSequenceNumbers) so Locus requests share the client's unified
// ${sessionId}_${sequence} tracking id space instead of minting an unrelated id. Fall
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,8 @@ describe('HashTreeParser', () => {
excludedDataSets?: string[],
syncLatencyTracker?: any,
syncLatencyMeetingId = 'meeting-1',
generateTrackingId?: any
generateTrackingId?: any,
isLlmExpected?: () => boolean
) {
return new HashTreeParser({
initialLocus,
Expand All @@ -190,6 +191,7 @@ describe('HashTreeParser', () => {
locusInfoUpdateCallback: callback,
syncLatencyTracker,
generateTrackingId,
isLlmExpected,
},
debugId: 'test',
excludedDataSets,
Expand Down Expand Up @@ -3403,6 +3405,45 @@ describe('HashTreeParser', () => {
expect(parser.dataSets.main.heartbeatWatchdogTimer).to.be.undefined;
});

it('skips watchdog timers for LLM datasets when current meeting LLM is not expected', async () => {
const parser = createHashTreeParser(
undefined,
undefined,
undefined,
undefined,
'meeting-1',
undefined,
() => false
);

const heartbeatMessage = {
dataSets: [
{
...createDataSet('main', 16, 1100),
root: parser.dataSets.main.hashTree.getRootHash(),
},
{
...createDataSet('self', 1, 2100),
url: parser.dataSets.self.url,
root: parser.dataSets.self.hashTree.getRootHash(),
},
{
...createDataSet('atd-unmuted', 16, 3100),
url: parser.dataSets['atd-unmuted'].url,
root: parser.dataSets['atd-unmuted'].hashTree.getRootHash(),
},
],
visibleDataSetsUrl,
locusUrl,
};

parser.handleMessage(heartbeatMessage, 'heartbeat with meeting-scoped llm not expected');

expect(parser.dataSets.main.heartbeatWatchdogTimer).to.be.undefined;
expect(parser.dataSets['atd-unmuted'].heartbeatWatchdogTimer).to.be.undefined;
expect(parser.dataSets.self.heartbeatWatchdogTimer).to.not.be.undefined;
});

it('stops all watchdog timers when meeting ends via sentinel message', async () => {
const parser = createHashTreeParser();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,38 @@ describe('plugin-meetings', () => {
);
});

it('passes an isLlmExpected callback that is true when current device is joined', async () => {
locusInfo.parsedLocus.self = {
state: 'LEFT',
joinedWith: {state: 'JOINED'},
};

await locusInfo.initialSetup({
trigger: 'locus-message',
hashTreeMessage: createHashTreeMessage(['dataset1']),
});

const {isLlmExpected} = HashTreeParserStub.firstCall.args[0].callbacks;

assert.equal(isLlmExpected(), true);
});

it('passes an isLlmExpected callback that is false when self is joined but current device is not joined', async () => {
locusInfo.parsedLocus.self = {
state: 'JOINED',
joinedWith: {state: 'LEFT'},
};

await locusInfo.initialSetup({
trigger: 'locus-message',
hashTreeMessage: createHashTreeMessage(['dataset1']),
});

const {isLlmExpected} = HashTreeParserStub.firstCall.args[0].callbacks;

assert.equal(isLlmExpected(), false);
});

it('should not initialize the hash tree when triggered from a non-hash tree locus message', async () => {
const locus = {url: 'http://locus-url.com', participants: []};

Expand Down