@webex/plugin-meetings 3.12.0-next.7 → 3.12.0-next.70
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +9 -0
- package/dist/aiEnableRequest/index.js +15 -2
- package/dist/aiEnableRequest/index.js.map +1 -1
- package/dist/breakouts/breakout.js +8 -3
- package/dist/breakouts/breakout.js.map +1 -1
- package/dist/breakouts/index.js +26 -2
- package/dist/breakouts/index.js.map +1 -1
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/constants.js +30 -7
- package/dist/constants.js.map +1 -1
- package/dist/controls-options-manager/constants.js +11 -1
- package/dist/controls-options-manager/constants.js.map +1 -1
- package/dist/controls-options-manager/index.js +38 -24
- package/dist/controls-options-manager/index.js.map +1 -1
- package/dist/controls-options-manager/util.js +91 -0
- package/dist/controls-options-manager/util.js.map +1 -1
- package/dist/hashTree/constants.js +13 -1
- package/dist/hashTree/constants.js.map +1 -1
- package/dist/hashTree/hashTreeParser.js +880 -382
- package/dist/hashTree/hashTreeParser.js.map +1 -1
- package/dist/hashTree/utils.js +42 -0
- package/dist/hashTree/utils.js.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/interceptors/dataChannelAuthToken.js +75 -15
- package/dist/interceptors/dataChannelAuthToken.js.map +1 -1
- package/dist/interceptors/locusRetry.js +23 -8
- package/dist/interceptors/locusRetry.js.map +1 -1
- package/dist/interpretation/index.js +10 -1
- package/dist/interpretation/index.js.map +1 -1
- package/dist/interpretation/interpretation.types.js +7 -0
- package/dist/interpretation/interpretation.types.js.map +1 -0
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/locus-info/controlsUtils.js +4 -1
- package/dist/locus-info/controlsUtils.js.map +1 -1
- package/dist/locus-info/index.js +298 -87
- package/dist/locus-info/index.js.map +1 -1
- package/dist/locus-info/types.js +19 -0
- package/dist/locus-info/types.js.map +1 -1
- package/dist/media/index.js +3 -1
- package/dist/media/index.js.map +1 -1
- package/dist/media/properties.js +1 -0
- package/dist/media/properties.js.map +1 -1
- package/dist/meeting/in-meeting-actions.js +3 -1
- package/dist/meeting/in-meeting-actions.js.map +1 -1
- package/dist/meeting/index.js +1046 -689
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/muteState.js +10 -1
- package/dist/meeting/muteState.js.map +1 -1
- package/dist/meeting/request.js +5 -2
- package/dist/meeting/request.js.map +1 -1
- package/dist/meeting/util.js +20 -2
- package/dist/meeting/util.js.map +1 -1
- package/dist/meeting-info/meeting-info-v2.js +2 -2
- package/dist/meeting-info/meeting-info-v2.js.map +1 -1
- package/dist/meetings/index.js +231 -78
- package/dist/meetings/index.js.map +1 -1
- package/dist/meetings/meetings.types.js +6 -1
- package/dist/meetings/meetings.types.js.map +1 -1
- package/dist/meetings/request.js +39 -0
- package/dist/meetings/request.js.map +1 -1
- package/dist/meetings/util.js +79 -5
- package/dist/meetings/util.js.map +1 -1
- package/dist/member/index.js +10 -0
- package/dist/member/index.js.map +1 -1
- package/dist/member/types.js.map +1 -1
- package/dist/member/util.js +3 -0
- package/dist/member/util.js.map +1 -1
- package/dist/metrics/constants.js +4 -1
- package/dist/metrics/constants.js.map +1 -1
- package/dist/multistream/codec/constants.js +63 -0
- package/dist/multistream/codec/constants.js.map +1 -0
- package/dist/multistream/mediaRequestManager.js +62 -15
- package/dist/multistream/mediaRequestManager.js.map +1 -1
- package/dist/multistream/receiveSlot.js +9 -0
- package/dist/multistream/receiveSlot.js.map +1 -1
- package/dist/reactions/reactions.type.js.map +1 -1
- package/dist/recording-controller/index.js +1 -3
- package/dist/recording-controller/index.js.map +1 -1
- package/dist/types/config.d.ts +2 -0
- package/dist/types/constants.d.ts +9 -1
- package/dist/types/controls-options-manager/constants.d.ts +6 -1
- package/dist/types/controls-options-manager/index.d.ts +10 -0
- package/dist/types/hashTree/constants.d.ts +2 -0
- package/dist/types/hashTree/hashTreeParser.d.ts +146 -17
- package/dist/types/hashTree/utils.d.ts +18 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/interceptors/locusRetry.d.ts +4 -4
- package/dist/types/interpretation/interpretation.types.d.ts +10 -0
- package/dist/types/locus-info/index.d.ts +50 -6
- package/dist/types/locus-info/types.d.ts +21 -1
- package/dist/types/media/properties.d.ts +1 -0
- package/dist/types/meeting/in-meeting-actions.d.ts +2 -0
- package/dist/types/meeting/index.d.ts +78 -5
- package/dist/types/meeting/request.d.ts +1 -0
- package/dist/types/meeting/util.d.ts +8 -0
- package/dist/types/meetings/index.d.ts +30 -2
- package/dist/types/meetings/meetings.types.d.ts +15 -0
- package/dist/types/meetings/request.d.ts +14 -0
- package/dist/types/member/index.d.ts +1 -0
- package/dist/types/member/types.d.ts +1 -0
- package/dist/types/member/util.d.ts +1 -0
- package/dist/types/metrics/constants.d.ts +3 -0
- package/dist/types/multistream/codec/constants.d.ts +7 -0
- package/dist/types/multistream/mediaRequestManager.d.ts +22 -5
- package/dist/types/reactions/reactions.type.d.ts +3 -0
- package/dist/webinar/index.js +305 -159
- package/dist/webinar/index.js.map +1 -1
- package/package.json +22 -22
- package/src/aiEnableRequest/index.ts +16 -0
- package/src/breakouts/breakout.ts +3 -1
- package/src/breakouts/index.ts +31 -0
- package/src/config.ts +2 -0
- package/src/constants.ts +13 -2
- package/src/controls-options-manager/constants.ts +14 -1
- package/src/controls-options-manager/index.ts +47 -24
- package/src/controls-options-manager/util.ts +81 -1
- package/src/hashTree/constants.ts +16 -0
- package/src/hashTree/hashTreeParser.ts +580 -196
- package/src/hashTree/utils.ts +36 -0
- package/src/index.ts +6 -0
- package/src/interceptors/dataChannelAuthToken.ts +88 -12
- package/src/interceptors/locusRetry.ts +25 -4
- package/src/interpretation/index.ts +27 -9
- package/src/interpretation/interpretation.types.ts +11 -0
- package/src/locus-info/controlsUtils.ts +3 -1
- package/src/locus-info/index.ts +293 -97
- package/src/locus-info/types.ts +25 -1
- package/src/media/index.ts +3 -0
- package/src/media/properties.ts +1 -0
- package/src/meeting/in-meeting-actions.ts +4 -0
- package/src/meeting/index.ts +386 -48
- package/src/meeting/muteState.ts +10 -1
- package/src/meeting/request.ts +11 -0
- package/src/meeting/util.ts +21 -2
- package/src/meeting-info/meeting-info-v2.ts +4 -2
- package/src/meetings/index.ts +134 -44
- package/src/meetings/meetings.types.ts +19 -0
- package/src/meetings/request.ts +43 -0
- package/src/meetings/util.ts +97 -1
- package/src/member/index.ts +10 -0
- package/src/member/types.ts +1 -0
- package/src/member/util.ts +3 -0
- package/src/metrics/constants.ts +3 -0
- package/src/multistream/codec/constants.ts +58 -0
- package/src/multistream/mediaRequestManager.ts +119 -28
- package/src/multistream/receiveSlot.ts +18 -0
- package/src/reactions/reactions.type.ts +3 -0
- package/src/recording-controller/index.ts +1 -2
- package/src/webinar/index.ts +214 -36
- package/test/unit/spec/aiEnableRequest/index.ts +86 -0
- package/test/unit/spec/breakouts/breakout.ts +9 -3
- package/test/unit/spec/breakouts/index.ts +49 -0
- package/test/unit/spec/controls-options-manager/index.js +140 -29
- package/test/unit/spec/controls-options-manager/util.js +165 -0
- package/test/unit/spec/hashTree/hashTreeParser.ts +1838 -180
- package/test/unit/spec/hashTree/utils.ts +125 -1
- package/test/unit/spec/interceptors/dataChannelAuthToken.ts +196 -0
- package/test/unit/spec/interceptors/locusRetry.ts +205 -4
- package/test/unit/spec/interpretation/index.ts +26 -4
- package/test/unit/spec/locus-info/controlsUtils.js +172 -57
- package/test/unit/spec/locus-info/index.js +487 -81
- package/test/unit/spec/media/index.ts +31 -0
- package/test/unit/spec/meeting/in-meeting-actions.ts +2 -0
- package/test/unit/spec/meeting/index.js +1240 -37
- package/test/unit/spec/meeting/muteState.js +81 -0
- package/test/unit/spec/meeting/request.js +12 -0
- package/test/unit/spec/meeting/utils.js +33 -0
- package/test/unit/spec/meeting-info/meetinginfov2.js +19 -10
- package/test/unit/spec/meetings/index.js +360 -10
- package/test/unit/spec/meetings/request.js +141 -0
- package/test/unit/spec/meetings/utils.js +189 -0
- package/test/unit/spec/member/index.js +7 -0
- package/test/unit/spec/member/util.js +24 -0
- package/test/unit/spec/multistream/mediaRequestManager.ts +501 -37
- package/test/unit/spec/recording-controller/index.js +9 -8
- package/test/unit/spec/webinar/index.ts +329 -28
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import {cloneDeep, isEmpty, zip} from 'lodash';
|
|
2
2
|
import HashTree, {LeafDataItem} from './hashTree';
|
|
3
3
|
import LoggerProxy from '../common/logs/logger-proxy';
|
|
4
|
+
import Metrics from '../metrics';
|
|
5
|
+
import BEHAVIORAL_METRICS from '../metrics/constants';
|
|
4
6
|
import {Enum, HTTP_VERBS} from '../constants';
|
|
5
|
-
import {DataSetNames, EMPTY_HASH} from './constants';
|
|
7
|
+
import {DataSetNames, DATA_SET_INIT_PRIORITY, EMPTY_HASH, LLM_DATASET_NAMES} from './constants';
|
|
6
8
|
import {ObjectType, HtMeta, HashTreeObject} from './types';
|
|
7
|
-
import {LocusDTO} from '../locus-info/types';
|
|
8
|
-
import {deleteNestedObjectsWithHtMeta, isMetadata} from './utils';
|
|
9
|
+
import {LocusDTO, LocusErrorCodes} from '../locus-info/types';
|
|
10
|
+
import {deleteNestedObjectsWithHtMeta, isMetadata, sleep, sortByInitPriority} from './utils';
|
|
11
|
+
|
|
12
|
+
export enum SyncAllBackoffType {
|
|
13
|
+
NONE = 'none',
|
|
14
|
+
ONLY_LLM = 'onlyLLM',
|
|
15
|
+
ALL = 'all',
|
|
16
|
+
}
|
|
9
17
|
|
|
10
18
|
export interface DataSet {
|
|
11
19
|
url: string;
|
|
@@ -18,10 +26,12 @@ export interface DataSet {
|
|
|
18
26
|
maxMs: number;
|
|
19
27
|
exponent: number;
|
|
20
28
|
};
|
|
29
|
+
heartbeatIntervalMs?: number;
|
|
21
30
|
}
|
|
22
31
|
|
|
23
32
|
export interface RootHashMessage {
|
|
24
33
|
dataSets: Array<DataSet>;
|
|
34
|
+
heartbeatIntervalMs?: number;
|
|
25
35
|
}
|
|
26
36
|
export interface HashTreeMessage {
|
|
27
37
|
dataSets: Array<DataSet>;
|
|
@@ -47,6 +57,7 @@ interface InternalDataSet extends DataSet {
|
|
|
47
57
|
hashTree?: HashTree; // set only for visible data sets
|
|
48
58
|
timer?: ReturnType<typeof setTimeout>;
|
|
49
59
|
heartbeatWatchdogTimer?: ReturnType<typeof setTimeout>;
|
|
60
|
+
syncAbortController?: AbortController;
|
|
50
61
|
}
|
|
51
62
|
|
|
52
63
|
type WebexRequestMethod = (options: Record<string, any>) => Promise<any>;
|
|
@@ -54,13 +65,24 @@ type WebexRequestMethod = (options: Record<string, any>) => Promise<any>;
|
|
|
54
65
|
export const LocusInfoUpdateType = {
|
|
55
66
|
OBJECTS_UPDATED: 'OBJECTS_UPDATED',
|
|
56
67
|
MEETING_ENDED: 'MEETING_ENDED',
|
|
68
|
+
LOCUS_NOT_FOUND: 'LOCUS_NOT_FOUND',
|
|
57
69
|
} as const;
|
|
58
70
|
|
|
59
71
|
export type LocusInfoUpdateType = Enum<typeof LocusInfoUpdateType>;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
72
|
+
|
|
73
|
+
interface LocusUpdatePayloads {
|
|
74
|
+
[LocusInfoUpdateType.OBJECTS_UPDATED]: {updatedObjects: HashTreeObject[]};
|
|
75
|
+
[LocusInfoUpdateType.MEETING_ENDED]: unknown; // No extra data
|
|
76
|
+
[LocusInfoUpdateType.LOCUS_NOT_FOUND]: unknown; // No extra data
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type LocusInfoUpdate = {
|
|
80
|
+
[K in keyof LocusUpdatePayloads]: {
|
|
81
|
+
updateType: K;
|
|
82
|
+
} & LocusUpdatePayloads[K];
|
|
83
|
+
}[keyof LocusUpdatePayloads];
|
|
84
|
+
|
|
85
|
+
export type LocusInfoUpdateCallback = (update: LocusInfoUpdate) => void;
|
|
64
86
|
|
|
65
87
|
interface LeafInfo {
|
|
66
88
|
type: ObjectType;
|
|
@@ -75,6 +97,13 @@ interface LeafInfo {
|
|
|
75
97
|
*/
|
|
76
98
|
export class MeetingEndedError extends Error {}
|
|
77
99
|
|
|
100
|
+
/**
|
|
101
|
+
* This error is thrown when a 404 is received from Locus hash tree endpoints, indicating that the locus URL
|
|
102
|
+
* is no longer valid (e.g. participant moved to a breakout room, or meeting ended).
|
|
103
|
+
* It's handled internally by HashTreeParser and results in LOCUS_NOT_FOUND being sent up.
|
|
104
|
+
*/
|
|
105
|
+
export class LocusNotFoundError extends Error {}
|
|
106
|
+
|
|
78
107
|
/* Currently Locus always sends Metadata objects only in the "self" dataset.
|
|
79
108
|
* If this ever changes, update all the code that relies on this constant.
|
|
80
109
|
*/
|
|
@@ -96,9 +125,17 @@ class HashTreeParser {
|
|
|
96
125
|
locusInfoUpdateCallback: LocusInfoUpdateCallback;
|
|
97
126
|
visibleDataSets: VisibleDataSetInfo[];
|
|
98
127
|
debugId: string;
|
|
99
|
-
heartbeatIntervalMs?: number;
|
|
100
128
|
private excludedDataSets: string[];
|
|
101
129
|
state: 'active' | 'stopped';
|
|
130
|
+
private syncQueue: Array<{dataSetName: string; reason: string; isInitialization?: boolean}> = [];
|
|
131
|
+
private isSyncInProgress = false;
|
|
132
|
+
// tracks whether syncAllDatasets is currently in its backoff delay phase and with what scope
|
|
133
|
+
private syncAllBackoffType: SyncAllBackoffType = SyncAllBackoffType.NONE;
|
|
134
|
+
// datasets that received messages during the syncAllDatasets backoff sleep and should be skipped
|
|
135
|
+
private dataSetsSyncedDuringBackoff: Set<string> = new Set();
|
|
136
|
+
private syncQueueProcessingPromise: Promise<void> = Promise.resolve();
|
|
137
|
+
// top-level heartbeat interval from the most recent message, used as fallback when dataset-level value is missing
|
|
138
|
+
private topLevelHeartbeatIntervalMs?: number;
|
|
102
139
|
|
|
103
140
|
/**
|
|
104
141
|
* Constructor for HashTreeParser
|
|
@@ -224,16 +261,16 @@ class HashTreeParser {
|
|
|
224
261
|
* @param {DataSet} dataSetInfo The new data set to be added
|
|
225
262
|
* @returns {Promise}
|
|
226
263
|
*/
|
|
227
|
-
private initializeNewVisibleDataSet(
|
|
264
|
+
private async initializeNewVisibleDataSet(
|
|
228
265
|
visibleDataSetInfo: VisibleDataSetInfo,
|
|
229
266
|
dataSetInfo: DataSet
|
|
230
|
-
): Promise<
|
|
267
|
+
): Promise<void> {
|
|
231
268
|
if (this.isVisibleDataSet(dataSetInfo.name)) {
|
|
232
269
|
LoggerProxy.logger.info(
|
|
233
270
|
`HashTreeParser#initializeNewVisibleDataSet --> ${this.debugId} Data set "${dataSetInfo.name}" already exists, skipping init`
|
|
234
271
|
);
|
|
235
272
|
|
|
236
|
-
return
|
|
273
|
+
return;
|
|
237
274
|
}
|
|
238
275
|
|
|
239
276
|
LoggerProxy.logger.info(
|
|
@@ -241,7 +278,7 @@ class HashTreeParser {
|
|
|
241
278
|
);
|
|
242
279
|
|
|
243
280
|
if (!this.addToVisibleDataSetsList(visibleDataSetInfo)) {
|
|
244
|
-
return
|
|
281
|
+
return;
|
|
245
282
|
}
|
|
246
283
|
|
|
247
284
|
const hashTree = new HashTree([], dataSetInfo.leafCount);
|
|
@@ -251,51 +288,8 @@ class HashTreeParser {
|
|
|
251
288
|
hashTree,
|
|
252
289
|
};
|
|
253
290
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Sends a special sync request to Locus with all leaves empty - this is a way to get all the data for a given dataset.
|
|
259
|
-
*
|
|
260
|
-
* @param {string} datasetName - name of the dataset for which to send the request
|
|
261
|
-
* @param {string} debugText - text to include in logs
|
|
262
|
-
* @returns {Promise}
|
|
263
|
-
*/
|
|
264
|
-
private sendInitializationSyncRequestToLocus(
|
|
265
|
-
datasetName: string,
|
|
266
|
-
debugText: string
|
|
267
|
-
): Promise<{updateType: LocusInfoUpdateType; updatedObjects?: HashTreeObject[]}> {
|
|
268
|
-
const dataset = this.dataSets[datasetName];
|
|
269
|
-
|
|
270
|
-
if (!dataset) {
|
|
271
|
-
LoggerProxy.logger.warn(
|
|
272
|
-
`HashTreeParser#sendInitializationSyncRequestToLocus --> ${this.debugId} No data set found for ${datasetName}, cannot send the request for leaf data`
|
|
273
|
-
);
|
|
274
|
-
|
|
275
|
-
return Promise.resolve(null);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
const emptyLeavesData = new Array(dataset.leafCount).fill([]);
|
|
279
|
-
|
|
280
|
-
LoggerProxy.logger.info(
|
|
281
|
-
`HashTreeParser#sendInitializationSyncRequestToLocus --> ${this.debugId} Sending initial sync request to Locus for data set "${datasetName}" with empty leaf data`
|
|
282
|
-
);
|
|
283
|
-
|
|
284
|
-
return this.sendSyncRequestToLocus(this.dataSets[datasetName], emptyLeavesData).then(
|
|
285
|
-
(syncResponse) => {
|
|
286
|
-
if (syncResponse) {
|
|
287
|
-
return {
|
|
288
|
-
updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
289
|
-
updatedObjects: this.parseMessage(
|
|
290
|
-
syncResponse,
|
|
291
|
-
`via empty leaves /sync API call for ${debugText}`
|
|
292
|
-
),
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
return {updateType: LocusInfoUpdateType.OBJECTS_UPDATED, updatedObjects: []};
|
|
297
|
-
}
|
|
298
|
-
);
|
|
291
|
+
this.enqueueSyncForDataset(dataSetInfo.name, 'new visible data set initialization', true);
|
|
292
|
+
await this.syncQueueProcessingPromise;
|
|
299
293
|
}
|
|
300
294
|
|
|
301
295
|
/**
|
|
@@ -382,9 +376,8 @@ class HashTreeParser {
|
|
|
382
376
|
if (this.state === 'stopped') {
|
|
383
377
|
return;
|
|
384
378
|
}
|
|
385
|
-
const updatedObjects: HashTreeObject[] = [];
|
|
386
379
|
|
|
387
|
-
for (const dataSet of visibleDataSets) {
|
|
380
|
+
for (const dataSet of sortByInitPriority(visibleDataSets, DATA_SET_INIT_PRIORITY)) {
|
|
388
381
|
const {name, leafCount, url} = dataSet;
|
|
389
382
|
|
|
390
383
|
if (!this.dataSets[name]) {
|
|
@@ -420,19 +413,12 @@ class HashTreeParser {
|
|
|
420
413
|
);
|
|
421
414
|
this.dataSets[name].hashTree = new HashTree([], leafCount);
|
|
422
415
|
|
|
423
|
-
|
|
424
|
-
const data = await this.sendInitializationSyncRequestToLocus(name, debugText);
|
|
425
|
-
|
|
426
|
-
if (data.updateType === LocusInfoUpdateType.OBJECTS_UPDATED) {
|
|
427
|
-
updatedObjects.push(...(data.updatedObjects || []));
|
|
428
|
-
}
|
|
416
|
+
this.enqueueSyncForDataset(name, `initialization sync for ${debugText}`, true);
|
|
429
417
|
}
|
|
430
418
|
}
|
|
431
419
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
updatedObjects,
|
|
435
|
-
});
|
|
420
|
+
// wait for all enqueued initialization syncs to complete
|
|
421
|
+
await this.syncQueueProcessingPromise;
|
|
436
422
|
}
|
|
437
423
|
|
|
438
424
|
/**
|
|
@@ -586,12 +572,40 @@ class HashTreeParser {
|
|
|
586
572
|
)}`
|
|
587
573
|
);
|
|
588
574
|
|
|
575
|
+
this.cancelPendingSyncsForDataSets(dataSets.map((ds) => ds.name));
|
|
576
|
+
|
|
589
577
|
dataSets.forEach((dataSet) => {
|
|
590
578
|
this.updateDataSetInfo(dataSet);
|
|
591
579
|
this.runSyncAlgorithm(dataSet);
|
|
592
580
|
});
|
|
593
581
|
}
|
|
594
582
|
|
|
583
|
+
/**
|
|
584
|
+
* Handles known errors that can happen during syncs
|
|
585
|
+
*
|
|
586
|
+
* @param {any} error - The error to handle
|
|
587
|
+
* @returns {boolean} true if the error was recognized and handled, false otherwise
|
|
588
|
+
*/
|
|
589
|
+
private handleSyncErrors(error: any) {
|
|
590
|
+
if (error instanceof MeetingEndedError) {
|
|
591
|
+
this.callLocusInfoUpdateCallback({
|
|
592
|
+
updateType: LocusInfoUpdateType.MEETING_ENDED,
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
return true;
|
|
596
|
+
}
|
|
597
|
+
if (error instanceof LocusNotFoundError) {
|
|
598
|
+
this.callLocusInfoUpdateCallback({
|
|
599
|
+
updateType: LocusInfoUpdateType.LOCUS_NOT_FOUND,
|
|
600
|
+
});
|
|
601
|
+
this.stop();
|
|
602
|
+
|
|
603
|
+
return true;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
return false;
|
|
607
|
+
}
|
|
608
|
+
|
|
595
609
|
/**
|
|
596
610
|
* Asynchronously initializes new visible data sets
|
|
597
611
|
*
|
|
@@ -608,11 +622,7 @@ class HashTreeParser {
|
|
|
608
622
|
);
|
|
609
623
|
queueMicrotask(() => {
|
|
610
624
|
this.initializeNewVisibleDataSets(dataSetsRequiringInitialization).catch((error) => {
|
|
611
|
-
if (error
|
|
612
|
-
this.callLocusInfoUpdateCallback({
|
|
613
|
-
updateType: LocusInfoUpdateType.MEETING_ENDED,
|
|
614
|
-
});
|
|
615
|
-
} else {
|
|
625
|
+
if (!this.handleSyncErrors(error)) {
|
|
616
626
|
LoggerProxy.logger.warn(
|
|
617
627
|
`HashTreeParser#queueInitForNewVisibleDataSets --> ${
|
|
618
628
|
this.debugId
|
|
@@ -690,7 +700,14 @@ class HashTreeParser {
|
|
|
690
700
|
|
|
691
701
|
const {dataSets, locus, metadata} = update;
|
|
692
702
|
|
|
703
|
+
LoggerProxy.logger.info(
|
|
704
|
+
`HashTreeParser#handleLocusUpdate --> ${this.debugId} received update with dataSets=${dataSets
|
|
705
|
+
?.map((ds) => ds.name)
|
|
706
|
+
.join(',')} metadata=${metadata ? 'yes' : 'no'}`
|
|
707
|
+
);
|
|
708
|
+
|
|
693
709
|
if (!dataSets) {
|
|
710
|
+
// this happens for example when we handle GET /loci response
|
|
694
711
|
LoggerProxy.logger.info(
|
|
695
712
|
`HashTreeParser#handleLocusUpdate --> ${this.debugId} received hash tree update without dataSets`
|
|
696
713
|
);
|
|
@@ -788,12 +805,25 @@ class HashTreeParser {
|
|
|
788
805
|
maxMs: receivedDataSet.backoff.maxMs,
|
|
789
806
|
exponent: receivedDataSet.backoff.exponent,
|
|
790
807
|
};
|
|
808
|
+
this.dataSets[receivedDataSet.name].heartbeatIntervalMs = receivedDataSet.heartbeatIntervalMs;
|
|
791
809
|
LoggerProxy.logger.info(
|
|
792
810
|
`HashTreeParser#updateDataSetInfo --> ${this.debugId} updated "${receivedDataSet.name}" dataset to version=${receivedDataSet.version}, root=${receivedDataSet.root}`
|
|
793
811
|
);
|
|
794
812
|
}
|
|
795
813
|
}
|
|
796
814
|
|
|
815
|
+
/**
|
|
816
|
+
* Updates the leaf count for a data set, resizing its hash tree accordingly.
|
|
817
|
+
*
|
|
818
|
+
* @param {InternalDataSet} dataSet - The data set to update
|
|
819
|
+
* @param {number} newLeafCount - The new leaf count
|
|
820
|
+
* @returns {void}
|
|
821
|
+
*/
|
|
822
|
+
private updateDataSetLeafCount(dataSet: InternalDataSet, newLeafCount: number): void {
|
|
823
|
+
dataSet.hashTree?.resize(newLeafCount);
|
|
824
|
+
dataSet.leafCount = newLeafCount;
|
|
825
|
+
}
|
|
826
|
+
|
|
797
827
|
/**
|
|
798
828
|
* Checks for changes in the visible data sets based on the updated objects.
|
|
799
829
|
* @param {HashTreeObject[]} updatedObjects - The list of updated hash tree objects.
|
|
@@ -844,6 +874,8 @@ class HashTreeParser {
|
|
|
844
874
|
*/
|
|
845
875
|
private deleteHashTree(dataSetName: string) {
|
|
846
876
|
this.dataSets[dataSetName].hashTree = undefined;
|
|
877
|
+
this.dataSets[dataSetName].syncAbortController?.abort();
|
|
878
|
+
this.dataSets[dataSetName].syncAbortController = undefined;
|
|
847
879
|
|
|
848
880
|
// we also need to stop the timers as there is no hash tree anymore to sync
|
|
849
881
|
if (this.dataSets[dataSetName].timer) {
|
|
@@ -960,7 +992,7 @@ class HashTreeParser {
|
|
|
960
992
|
}
|
|
961
993
|
const allDataSets = await this.getAllVisibleDataSetsFromLocus();
|
|
962
994
|
|
|
963
|
-
for (const ds of addedDataSets) {
|
|
995
|
+
for (const ds of sortByInitPriority(addedDataSets, DATA_SET_INIT_PRIORITY)) {
|
|
964
996
|
const dataSetInfo = allDataSets.find((d) => d.name === ds.name);
|
|
965
997
|
|
|
966
998
|
LoggerProxy.logger.info(
|
|
@@ -972,12 +1004,8 @@ class HashTreeParser {
|
|
|
972
1004
|
`HashTreeParser#initializeNewVisibleDataSets --> ${this.debugId} missing info about data set "${ds.name}" in Locus response from visibleDataSetsUrl`
|
|
973
1005
|
);
|
|
974
1006
|
} else {
|
|
975
|
-
// we're awaiting in a loop, because in practice there will be only one new data set at a time,
|
|
976
|
-
// so no point in trying to parallelize this
|
|
977
1007
|
// eslint-disable-next-line no-await-in-loop
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
this.callLocusInfoUpdateCallback(updates);
|
|
1008
|
+
await this.initializeNewVisibleDataSet(ds, dataSetInfo);
|
|
981
1009
|
}
|
|
982
1010
|
}
|
|
983
1011
|
}
|
|
@@ -997,25 +1025,39 @@ class HashTreeParser {
|
|
|
997
1025
|
const {dataSets, visibleDataSetsUrl} = message;
|
|
998
1026
|
|
|
999
1027
|
LoggerProxy.logger.info(
|
|
1000
|
-
`HashTreeParser#parseMessage --> ${this.debugId}
|
|
1001
|
-
|
|
1028
|
+
`HashTreeParser#parseMessage --> ${this.debugId} ${
|
|
1029
|
+
debugText || ''
|
|
1030
|
+
} dataSets: ${message.dataSets
|
|
1031
|
+
?.map(({name, version}) => `${name}:${version}`)
|
|
1032
|
+
.join(',')}, elements: ${message.locusStateElements
|
|
1033
|
+
?.map(
|
|
1034
|
+
(el) =>
|
|
1035
|
+
`${el.htMeta.elementId.type}:${el.htMeta.elementId.id}:${el.htMeta.elementId.version}${
|
|
1036
|
+
el.data ? '+' : '-'
|
|
1037
|
+
}`
|
|
1038
|
+
)
|
|
1039
|
+
.join(',')}`
|
|
1002
1040
|
);
|
|
1041
|
+
|
|
1003
1042
|
if (message.locusStateElements?.length === 0) {
|
|
1004
1043
|
LoggerProxy.logger.warn(
|
|
1005
1044
|
`HashTreeParser#parseMessage --> ${this.debugId} got empty locusStateElements!!!`
|
|
1006
1045
|
);
|
|
1007
|
-
|
|
1046
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.HASH_TREE_EMPTY_LOCUS_STATE_ELEMENTS, {
|
|
1047
|
+
debugId: this.debugId,
|
|
1048
|
+
});
|
|
1008
1049
|
}
|
|
1009
1050
|
|
|
1010
1051
|
// first, update our metadata about the datasets with info from the message
|
|
1011
1052
|
this.visibleDataSetsUrl = visibleDataSetsUrl;
|
|
1012
1053
|
dataSets.forEach((dataSet) => this.updateDataSetInfo(dataSet));
|
|
1054
|
+
this.cancelPendingSyncsForDataSets(dataSets.map((ds) => ds.name));
|
|
1013
1055
|
|
|
1014
1056
|
const updatedObjects: HashTreeObject[] = [];
|
|
1015
1057
|
|
|
1016
1058
|
// when we detect new visible datasets, it may be that the metadata about them is not
|
|
1017
1059
|
// available in the message, they will require separate async initialization
|
|
1018
|
-
let dataSetsRequiringInitialization = [];
|
|
1060
|
+
let dataSetsRequiringInitialization: VisibleDataSetInfo[] = [];
|
|
1019
1061
|
|
|
1020
1062
|
// first find out if there are any visible data set changes - they're signalled in Metadata object updates
|
|
1021
1063
|
const metadataUpdates = (message.locusStateElements || []).filter((object) =>
|
|
@@ -1023,7 +1065,7 @@ class HashTreeParser {
|
|
|
1023
1065
|
);
|
|
1024
1066
|
|
|
1025
1067
|
if (metadataUpdates.length > 0) {
|
|
1026
|
-
const updatedMetadataObjects = [];
|
|
1068
|
+
const updatedMetadataObjects: HashTreeObject[] = [];
|
|
1027
1069
|
|
|
1028
1070
|
metadataUpdates.forEach((object) => {
|
|
1029
1071
|
// todo: once Locus supports it, we will use the "view" field here instead of dataSetNames
|
|
@@ -1052,7 +1094,7 @@ class HashTreeParser {
|
|
|
1052
1094
|
}
|
|
1053
1095
|
}
|
|
1054
1096
|
|
|
1055
|
-
if (message.locusStateElements
|
|
1097
|
+
if (message.locusStateElements && message.locusStateElements.length > 0) {
|
|
1056
1098
|
// by this point we now have this.dataSets setup for data sets from this message
|
|
1057
1099
|
// and hash trees created for the new visible data sets,
|
|
1058
1100
|
// so we can now process all the updates from the message
|
|
@@ -1118,9 +1160,10 @@ class HashTreeParser {
|
|
|
1118
1160
|
return;
|
|
1119
1161
|
}
|
|
1120
1162
|
|
|
1121
|
-
if (message.heartbeatIntervalMs) {
|
|
1122
|
-
this.
|
|
1163
|
+
if (message.heartbeatIntervalMs !== undefined) {
|
|
1164
|
+
this.topLevelHeartbeatIntervalMs = message.heartbeatIntervalMs;
|
|
1123
1165
|
}
|
|
1166
|
+
|
|
1124
1167
|
if (this.isEndMessage(message)) {
|
|
1125
1168
|
LoggerProxy.logger.info(
|
|
1126
1169
|
`HashTreeParser#handleMessage --> ${this.debugId} received sentinel END MEETING message`
|
|
@@ -1148,20 +1191,17 @@ class HashTreeParser {
|
|
|
1148
1191
|
* @param {Object} updates parsed from a Locus message
|
|
1149
1192
|
* @returns {void}
|
|
1150
1193
|
*/
|
|
1151
|
-
private callLocusInfoUpdateCallback(updates: {
|
|
1152
|
-
updateType: LocusInfoUpdateType;
|
|
1153
|
-
updatedObjects?: HashTreeObject[];
|
|
1154
|
-
}) {
|
|
1194
|
+
private callLocusInfoUpdateCallback(updates: LocusInfoUpdate) {
|
|
1155
1195
|
if (this.state === 'stopped') {
|
|
1156
1196
|
return;
|
|
1157
1197
|
}
|
|
1158
1198
|
|
|
1159
|
-
const {updateType
|
|
1199
|
+
const {updateType} = updates;
|
|
1160
1200
|
|
|
1161
|
-
if (updateType === LocusInfoUpdateType.OBJECTS_UPDATED && updatedObjects?.length > 0) {
|
|
1201
|
+
if (updateType === LocusInfoUpdateType.OBJECTS_UPDATED && updates.updatedObjects?.length > 0) {
|
|
1162
1202
|
// Filter out updates for objects that already have a higher version in their datasets,
|
|
1163
1203
|
// or removals for objects that still exist in any of their datasets
|
|
1164
|
-
const filteredUpdates = updatedObjects.filter((object) => {
|
|
1204
|
+
const filteredUpdates = updates.updatedObjects.filter((object) => {
|
|
1165
1205
|
const {elementId} = object.htMeta;
|
|
1166
1206
|
const {type, id, version} = elementId;
|
|
1167
1207
|
|
|
@@ -1198,10 +1238,10 @@ class HashTreeParser {
|
|
|
1198
1238
|
});
|
|
1199
1239
|
|
|
1200
1240
|
if (filteredUpdates.length > 0) {
|
|
1201
|
-
this.locusInfoUpdateCallback(updateType,
|
|
1241
|
+
this.locusInfoUpdateCallback({updateType, updatedObjects: filteredUpdates});
|
|
1202
1242
|
}
|
|
1203
1243
|
} else if (updateType !== LocusInfoUpdateType.OBJECTS_UPDATED) {
|
|
1204
|
-
this.locusInfoUpdateCallback(updateType
|
|
1244
|
+
this.locusInfoUpdateCallback({updateType});
|
|
1205
1245
|
}
|
|
1206
1246
|
}
|
|
1207
1247
|
|
|
@@ -1223,82 +1263,363 @@ class HashTreeParser {
|
|
|
1223
1263
|
* Performs a sync for the given data set.
|
|
1224
1264
|
*
|
|
1225
1265
|
* @param {InternalDataSet} dataSet - The data set to sync
|
|
1226
|
-
* @param {string} rootHash - Our current root hash for this data set
|
|
1227
1266
|
* @param {string} reason - The reason for the sync (used for logging)
|
|
1267
|
+
* @param {boolean} [isInitialization] - Whether this is an initialization sync (sends empty leaves data instead of comparing hashes)
|
|
1228
1268
|
* @returns {Promise<void>}
|
|
1229
1269
|
*/
|
|
1230
1270
|
private async performSync(
|
|
1231
1271
|
dataSet: InternalDataSet,
|
|
1232
|
-
|
|
1233
|
-
|
|
1272
|
+
reason: string,
|
|
1273
|
+
isInitialization?: boolean
|
|
1234
1274
|
): Promise<void> {
|
|
1235
1275
|
if (!dataSet.hashTree) {
|
|
1236
1276
|
return;
|
|
1237
1277
|
}
|
|
1238
1278
|
|
|
1279
|
+
const abortController = dataSet.syncAbortController ?? new AbortController();
|
|
1280
|
+
dataSet.syncAbortController = abortController;
|
|
1281
|
+
|
|
1282
|
+
const {hashTree} = dataSet;
|
|
1283
|
+
const rootHash = hashTree.getRootHash();
|
|
1284
|
+
|
|
1239
1285
|
try {
|
|
1240
1286
|
LoggerProxy.logger.info(
|
|
1241
1287
|
`HashTreeParser#performSync --> ${this.debugId} ${reason}, syncing data set "${dataSet.name}"`
|
|
1242
1288
|
);
|
|
1243
1289
|
|
|
1244
|
-
|
|
1290
|
+
let leavesData: Record<number, LeafDataItem[]> = {};
|
|
1245
1291
|
|
|
1246
|
-
if (
|
|
1247
|
-
|
|
1292
|
+
if (!isInitialization) {
|
|
1293
|
+
if (dataSet.leafCount !== 1) {
|
|
1294
|
+
let receivedHashes;
|
|
1248
1295
|
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
dataSet.name,
|
|
1253
|
-
rootHash
|
|
1254
|
-
);
|
|
1296
|
+
try {
|
|
1297
|
+
// request hashes from sender
|
|
1298
|
+
const hashesResult = await this.getHashesFromLocus(dataSet.name, rootHash);
|
|
1255
1299
|
|
|
1256
|
-
|
|
1300
|
+
if (!hashesResult) {
|
|
1301
|
+
// hashes match, no sync needed
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1257
1304
|
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1305
|
+
receivedHashes = hashesResult.hashes;
|
|
1306
|
+
|
|
1307
|
+
this.updateDataSetLeafCount(dataSet, hashesResult.dataSet.leafCount);
|
|
1308
|
+
} catch (error: any) {
|
|
1309
|
+
if (error?.statusCode === 409) {
|
|
1310
|
+
// this is a leaf count mismatch, we should do nothing, just wait for another heartbeat message from Locus
|
|
1311
|
+
LoggerProxy.logger.info(
|
|
1312
|
+
`HashTreeParser#getHashesFromLocus --> ${this.debugId} Got 409 when fetching hashes for data set "${dataSet.name}": ${error.message}`
|
|
1313
|
+
);
|
|
1265
1314
|
|
|
1266
|
-
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
throw error;
|
|
1267
1318
|
}
|
|
1268
|
-
|
|
1319
|
+
|
|
1320
|
+
// identify mismatched leaves
|
|
1321
|
+
const mismatchedLeaveIndexes = hashTree.diffHashes(receivedHashes);
|
|
1322
|
+
|
|
1323
|
+
mismatchedLeaveIndexes.forEach((index) => {
|
|
1324
|
+
leavesData[index] = hashTree.getLeafData(index);
|
|
1325
|
+
});
|
|
1326
|
+
} else {
|
|
1327
|
+
leavesData = {0: hashTree.getLeafData(0)};
|
|
1269
1328
|
}
|
|
1329
|
+
}
|
|
1270
1330
|
|
|
1271
|
-
|
|
1272
|
-
|
|
1331
|
+
if (abortController.signal.aborted) {
|
|
1332
|
+
LoggerProxy.logger.info(
|
|
1333
|
+
`HashTreeParser#performSync --> ${this.debugId} abandoning sync for "${dataSet.name}" before /sync - message received during sync`
|
|
1334
|
+
);
|
|
1273
1335
|
|
|
1274
|
-
|
|
1275
|
-
mismatchedLeavesData[index] = dataSet.hashTree.getLeafData(index);
|
|
1276
|
-
});
|
|
1277
|
-
} else {
|
|
1278
|
-
mismatchedLeavesData[0] = dataSet.hashTree.getLeafData(0);
|
|
1336
|
+
return;
|
|
1279
1337
|
}
|
|
1280
1338
|
// request sync for mismatched leaves
|
|
1281
|
-
|
|
1282
|
-
const syncResponse = await this.sendSyncRequestToLocus(dataSet, mismatchedLeavesData);
|
|
1339
|
+
let syncResponse: HashTreeMessage | null = null;
|
|
1283
1340
|
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1341
|
+
if (isInitialization) {
|
|
1342
|
+
syncResponse = await this.sendSyncRequestToLocus(dataSet, {isInitialization: true});
|
|
1343
|
+
} else if (Object.keys(leavesData).length > 0) {
|
|
1344
|
+
syncResponse = await this.sendSyncRequestToLocus(dataSet, {
|
|
1345
|
+
mismatchedLeavesData: leavesData,
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// sync API may return nothing (in that case data will arrive via messages)
|
|
1350
|
+
// or it may return a response in the same format as messages
|
|
1351
|
+
// We still need to restart the sync timer as a safety net in case the messages don't arrive.
|
|
1352
|
+
this.runSyncAlgorithm(dataSet);
|
|
1353
|
+
|
|
1354
|
+
if (syncResponse) {
|
|
1355
|
+
// clear the abort controller before processing the response so that
|
|
1356
|
+
// parseMessage() -> cancelPendingSyncsForDataSets() doesn't log a
|
|
1357
|
+
// misleading "aborting sync" message for this already-completed sync
|
|
1358
|
+
dataSet.syncAbortController = undefined;
|
|
1359
|
+
|
|
1360
|
+
// the format of sync response is the same as messages, so we can reuse the same handler
|
|
1361
|
+
this.handleMessage(
|
|
1362
|
+
syncResponse,
|
|
1363
|
+
`via sync API (${
|
|
1364
|
+
isInitialization ? 'init' : `${Object.keys(leavesData).length} mismatched leaves`
|
|
1365
|
+
})`
|
|
1366
|
+
);
|
|
1289
1367
|
}
|
|
1290
1368
|
} catch (error) {
|
|
1291
|
-
if (error
|
|
1292
|
-
this.callLocusInfoUpdateCallback({
|
|
1293
|
-
updateType: LocusInfoUpdateType.MEETING_ENDED,
|
|
1294
|
-
});
|
|
1295
|
-
} else {
|
|
1369
|
+
if (!this.handleSyncErrors(error)) {
|
|
1296
1370
|
LoggerProxy.logger.warn(
|
|
1297
1371
|
`HashTreeParser#performSync --> ${this.debugId} error during sync for data set "${dataSet.name}":`,
|
|
1298
1372
|
error
|
|
1299
1373
|
);
|
|
1300
1374
|
}
|
|
1375
|
+
} finally {
|
|
1376
|
+
dataSet.syncAbortController = undefined;
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
/**
|
|
1381
|
+
* Cancels any pending or in-flight syncs for the specified data sets.
|
|
1382
|
+
* This removes matching entries from the sync queue and aborts any in-flight sync HTTP requests.
|
|
1383
|
+
*
|
|
1384
|
+
* @param {string[]} dataSetNames - The names of the data sets to cancel syncs for
|
|
1385
|
+
* @returns {void}
|
|
1386
|
+
*/
|
|
1387
|
+
private cancelPendingSyncsForDataSets(dataSetNames: string[]): void {
|
|
1388
|
+
const previousLength = this.syncQueue.length;
|
|
1389
|
+
|
|
1390
|
+
this.syncQueue = this.syncQueue.filter((entry) => !dataSetNames.includes(entry.dataSetName));
|
|
1391
|
+
|
|
1392
|
+
if (previousLength !== this.syncQueue.length) {
|
|
1393
|
+
LoggerProxy.logger.info(
|
|
1394
|
+
`HashTreeParser#cancelPendingSyncsForDataSets --> ${this.debugId} removed ${
|
|
1395
|
+
previousLength - this.syncQueue.length
|
|
1396
|
+
} entries from sync queue for data sets: ${dataSetNames.join(', ')}`
|
|
1397
|
+
);
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
this.markDataSetsForSyncAllBackoffSkip(dataSetNames);
|
|
1401
|
+
this.abortInFlightSyncs(dataSetNames);
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
/**
|
|
1405
|
+
* If a syncAllDatasets backoff sleep is in progress, marks the given data sets to be skipped
|
|
1406
|
+
* after the sleep completes.
|
|
1407
|
+
*
|
|
1408
|
+
* @param {string[]} dataSetNames - The names of the data sets to mark
|
|
1409
|
+
* @returns {void}
|
|
1410
|
+
*/
|
|
1411
|
+
private markDataSetsForSyncAllBackoffSkip(dataSetNames: string[]): void {
|
|
1412
|
+
if (this.syncAllBackoffType !== SyncAllBackoffType.NONE) {
|
|
1413
|
+
for (const name of dataSetNames) {
|
|
1414
|
+
this.dataSetsSyncedDuringBackoff.add(name);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
/**
|
|
1420
|
+
* Aborts any in-flight sync HTTP requests for the specified data sets.
|
|
1421
|
+
*
|
|
1422
|
+
* @param {string[]} dataSetNames - The names of the data sets whose syncs should be aborted
|
|
1423
|
+
* @returns {void}
|
|
1424
|
+
*/
|
|
1425
|
+
private abortInFlightSyncs(dataSetNames: string[]): void {
|
|
1426
|
+
for (const name of dataSetNames) {
|
|
1427
|
+
if (this.dataSets[name]?.syncAbortController) {
|
|
1428
|
+
LoggerProxy.logger.info(
|
|
1429
|
+
`HashTreeParser#cancelPendingSyncsForDataSets --> ${this.debugId} aborting in-flight sync for data set "${name}"`
|
|
1430
|
+
);
|
|
1431
|
+
this.dataSets[name].syncAbortController.abort();
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
/**
|
|
1437
|
+
* Enqueues a sync for the given data set. If the data set is already in the queue, the request is ignored.
|
|
1438
|
+
* This ensures that all syncs are executed sequentially and no more than 1 sync runs at a time.
|
|
1439
|
+
*
|
|
1440
|
+
* @param {string} dataSetName - The name of the data set to sync
|
|
1441
|
+
* @param {string} reason - The reason for the sync (used for logging)
|
|
1442
|
+
* @param {boolean} [isInitialization=false] - Whether this is an initialization sync (uses empty leaves data instead of hash comparison)
|
|
1443
|
+
* @returns {void}
|
|
1444
|
+
*/
|
|
1445
|
+
private enqueueSyncForDataset(
|
|
1446
|
+
dataSetName: string,
|
|
1447
|
+
reason: string,
|
|
1448
|
+
isInitialization = false
|
|
1449
|
+
): void {
|
|
1450
|
+
if (this.state === 'stopped') return;
|
|
1451
|
+
|
|
1452
|
+
const existingEntry = this.syncQueue.find((entry) => entry.dataSetName === dataSetName);
|
|
1453
|
+
|
|
1454
|
+
if (existingEntry) {
|
|
1455
|
+
if (isInitialization) {
|
|
1456
|
+
existingEntry.isInitialization = true;
|
|
1457
|
+
}
|
|
1458
|
+
LoggerProxy.logger.info(
|
|
1459
|
+
`HashTreeParser#enqueueSyncForDataset --> ${this.debugId} data set "${dataSetName}" already in sync queue, skipping`
|
|
1460
|
+
);
|
|
1461
|
+
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
this.syncQueue.push({dataSetName, reason, isInitialization});
|
|
1466
|
+
|
|
1467
|
+
if (!this.isSyncInProgress) {
|
|
1468
|
+
this.syncQueueProcessingPromise = this.processSyncQueue();
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
/**
|
|
1473
|
+
* Processes the sync queue sequentially. Only one instance of this method runs at a time.
|
|
1474
|
+
*
|
|
1475
|
+
* @returns {Promise<void>}
|
|
1476
|
+
*/
|
|
1477
|
+
private async processSyncQueue(): Promise<void> {
|
|
1478
|
+
if (this.isSyncInProgress) return;
|
|
1479
|
+
|
|
1480
|
+
this.isSyncInProgress = true;
|
|
1481
|
+
try {
|
|
1482
|
+
while (this.syncQueue.length > 0 && this.state !== 'stopped') {
|
|
1483
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1484
|
+
const {dataSetName, reason, isInitialization} = this.syncQueue.shift()!;
|
|
1485
|
+
const dataSet = this.dataSets[dataSetName];
|
|
1486
|
+
|
|
1487
|
+
if (!dataSet?.hashTree) {
|
|
1488
|
+
// eslint-disable-next-line no-continue
|
|
1489
|
+
continue;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1493
|
+
await this.performSync(dataSet, reason, isInitialization);
|
|
1494
|
+
}
|
|
1495
|
+
} finally {
|
|
1496
|
+
this.isSyncInProgress = false;
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
/**
|
|
1501
|
+
* sets the backoff type for syncAllDatasets calls, which determines the scope of datasets that will be synced after the backoff delay.
|
|
1502
|
+
*
|
|
1503
|
+
* @param {boolean} onlyLLM - Whether the backoff is for a syncAllDatasets call that is syncing only LLM datasets
|
|
1504
|
+
* @returns {void}
|
|
1505
|
+
*/
|
|
1506
|
+
private setSyncAllBackoffType(onlyLLM: boolean): void {
|
|
1507
|
+
this.syncAllBackoffType = onlyLLM ? SyncAllBackoffType.ONLY_LLM : SyncAllBackoffType.ALL;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
/**
|
|
1511
|
+
* Checks if a syncAll backoff is already in progress. If so, upgrades the scope from
|
|
1512
|
+
* onlyLLM to all datasets when the new call has a broader scope.
|
|
1513
|
+
*
|
|
1514
|
+
* @param {boolean} onlyLLM - Whether the current call is for LLM datasets only
|
|
1515
|
+
* @returns {boolean} true if a backoff is already pending (caller should return early)
|
|
1516
|
+
*/
|
|
1517
|
+
private tryUpgradePendingBackoff(onlyLLM: boolean): boolean {
|
|
1518
|
+
if (this.syncAllBackoffType !== SyncAllBackoffType.NONE) {
|
|
1519
|
+
if (!onlyLLM && this.syncAllBackoffType === SyncAllBackoffType.ONLY_LLM) {
|
|
1520
|
+
this.setSyncAllBackoffType(false);
|
|
1521
|
+
LoggerProxy.logger.info(
|
|
1522
|
+
`HashTreeParser#syncAllDatasets --> ${this.debugId} upgraded pending syncAll from onlyLLM to all datasets`
|
|
1523
|
+
);
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
return true;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
return false;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
/**
|
|
1533
|
+
* Syncs all data sets that have hash trees, one by one in sequence, using the priority order
|
|
1534
|
+
* provided by sortByInitPriority().
|
|
1535
|
+
*
|
|
1536
|
+
* If a call is already waiting in the backoff delay phase, a new call with a broader scope
|
|
1537
|
+
* (onlyLLM=false) will upgrade the pending scope, and the dataset list will be computed after
|
|
1538
|
+
* the backoff using the upgraded scope. After the backoff, the sync queue handles deduplication
|
|
1539
|
+
* so no guard is needed.
|
|
1540
|
+
*
|
|
1541
|
+
* @param {Object} [options={}] - Options for syncing
|
|
1542
|
+
* @param {boolean} [options.onlyLLM=false] - Whether to sync only LLM based data sets
|
|
1543
|
+
* @returns {Promise<void>}
|
|
1544
|
+
*/
|
|
1545
|
+
public async syncAllDatasets(options: {onlyLLM?: boolean} = {}): Promise<void> {
|
|
1546
|
+
const {onlyLLM = false} = options;
|
|
1547
|
+
if (this.state === 'stopped') return;
|
|
1548
|
+
|
|
1549
|
+
// if we're already in the backoff delay phase, try to upgrade the scope instead of starting a new one
|
|
1550
|
+
if (this.tryUpgradePendingBackoff(onlyLLM)) {
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
const dataSetsToSync = this.getSortedDataSetsWithHashTrees(onlyLLM);
|
|
1555
|
+
|
|
1556
|
+
if (dataSetsToSync.length === 0) return;
|
|
1557
|
+
|
|
1558
|
+
this.setSyncAllBackoffType(onlyLLM);
|
|
1559
|
+
|
|
1560
|
+
const delay = this.getWeightedBackoffTime(dataSetsToSync[0].backoff);
|
|
1561
|
+
|
|
1562
|
+
LoggerProxy.logger.info(
|
|
1563
|
+
`HashTreeParser#syncAllDatasets --> ${this.debugId} starting backoff delay of ${delay}ms (onlyLLM=${onlyLLM})`
|
|
1564
|
+
);
|
|
1565
|
+
|
|
1566
|
+
// delay the start of the syncs - this is a Locus requirement to avoid thundering herd issues
|
|
1567
|
+
await sleep(delay);
|
|
1568
|
+
|
|
1569
|
+
// read the (possibly upgraded) scope and clear the backoff flag
|
|
1570
|
+
const effectiveBackoffType = this.syncAllBackoffType;
|
|
1571
|
+
const skippedDataSets = this.dataSetsSyncedDuringBackoff;
|
|
1572
|
+
|
|
1573
|
+
this.syncAllBackoffType = SyncAllBackoffType.NONE;
|
|
1574
|
+
this.dataSetsSyncedDuringBackoff = new Set();
|
|
1575
|
+
|
|
1576
|
+
if ((this.state as string) === 'stopped') return;
|
|
1577
|
+
|
|
1578
|
+
// re-evaluate the dataset list after the sleep, since the scope may have been upgraded
|
|
1579
|
+
// and exclude datasets that received messages during the backoff sleep
|
|
1580
|
+
const effectiveDataSetsToSync = this.getSortedDataSetsWithHashTrees(
|
|
1581
|
+
effectiveBackoffType === SyncAllBackoffType.ONLY_LLM
|
|
1582
|
+
).filter((ds) => !skippedDataSets.has(ds.name));
|
|
1583
|
+
|
|
1584
|
+
if (skippedDataSets.size > 0) {
|
|
1585
|
+
LoggerProxy.logger.info(
|
|
1586
|
+
`HashTreeParser#syncAllDatasets --> ${
|
|
1587
|
+
this.debugId
|
|
1588
|
+
} skipping datasets that received messages during backoff: ${[...skippedDataSets].join(
|
|
1589
|
+
', '
|
|
1590
|
+
)}`
|
|
1591
|
+
);
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
LoggerProxy.logger.info(
|
|
1595
|
+
`HashTreeParser#syncAllDatasets --> ${this.debugId} syncing ${
|
|
1596
|
+
effectiveBackoffType === SyncAllBackoffType.ONLY_LLM ? 'only LLM' : 'all'
|
|
1597
|
+
} datasets: ${effectiveDataSetsToSync.map((ds) => ds.name).join(', ')}`
|
|
1598
|
+
);
|
|
1599
|
+
|
|
1600
|
+
for (const ds of effectiveDataSetsToSync) {
|
|
1601
|
+
this.enqueueSyncForDataset(ds.name, 'syncAllDatasets');
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
await this.syncQueueProcessingPromise;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
/**
|
|
1608
|
+
* Returns the list of data sets that have hash trees, sorted by the priority order provided by sortByInitPriority().
|
|
1609
|
+
*
|
|
1610
|
+
* @param {boolean} onlyLLM - Whether to include only LLM based data sets
|
|
1611
|
+
* @returns {Array<{name: string, backoff: {maxMs: number, exponent: number}}>} The sorted list of data sets with their backoff configurations
|
|
1612
|
+
*/
|
|
1613
|
+
private getSortedDataSetsWithHashTrees(onlyLLM: boolean) {
|
|
1614
|
+
let dataSets = Object.values(this.dataSets)
|
|
1615
|
+
.filter((dataSet) => dataSet?.hashTree)
|
|
1616
|
+
.map((dataSet) => ({name: dataSet.name, backoff: dataSet.backoff}));
|
|
1617
|
+
|
|
1618
|
+
if (onlyLLM) {
|
|
1619
|
+
dataSets = dataSets.filter((ds) => LLM_DATASET_NAMES.includes(ds.name));
|
|
1301
1620
|
}
|
|
1621
|
+
|
|
1622
|
+
return sortByInitPriority(dataSets, DATA_SET_INIT_PRIORITY);
|
|
1302
1623
|
}
|
|
1303
1624
|
|
|
1304
1625
|
/**
|
|
@@ -1319,21 +1640,14 @@ class HashTreeParser {
|
|
|
1319
1640
|
}
|
|
1320
1641
|
|
|
1321
1642
|
if (!dataSet.hashTree) {
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
);
|
|
1643
|
+
// no hash tree, so no need to do any syncing
|
|
1644
|
+
// we fall into this branch often, because Locus sends dataSets in messages that are not visible to us
|
|
1325
1645
|
|
|
1326
1646
|
return;
|
|
1327
1647
|
}
|
|
1328
1648
|
|
|
1329
1649
|
dataSet.hashTree.resize(receivedDataSet.leafCount);
|
|
1330
1650
|
|
|
1331
|
-
// temporary log for the workshop // todo: remove
|
|
1332
|
-
const ourCurrentRootHash = dataSet.hashTree.getRootHash();
|
|
1333
|
-
LoggerProxy.logger.info(
|
|
1334
|
-
`HashTreeParser#runSyncAlgorithm --> ${this.debugId} dataSet="${dataSet.name}" version=${dataSet.version} hashes before starting timer: ours=${ourCurrentRootHash} Locus=${dataSet.root}`
|
|
1335
|
-
);
|
|
1336
|
-
|
|
1337
1651
|
const delay = dataSet.idleMs + this.getWeightedBackoffTime(dataSet.backoff);
|
|
1338
1652
|
|
|
1339
1653
|
if (delay > 0) {
|
|
@@ -1341,11 +1655,7 @@ class HashTreeParser {
|
|
|
1341
1655
|
clearTimeout(dataSet.timer);
|
|
1342
1656
|
}
|
|
1343
1657
|
|
|
1344
|
-
|
|
1345
|
-
`HashTreeParser#runSyncAlgorithm --> ${this.debugId} setting "${dataSet.name}" sync timer for ${delay}`
|
|
1346
|
-
);
|
|
1347
|
-
|
|
1348
|
-
dataSet.timer = setTimeout(async () => {
|
|
1658
|
+
dataSet.timer = setTimeout(() => {
|
|
1349
1659
|
dataSet.timer = undefined;
|
|
1350
1660
|
|
|
1351
1661
|
if (!dataSet.hashTree) {
|
|
@@ -1359,15 +1669,10 @@ class HashTreeParser {
|
|
|
1359
1669
|
const rootHash = dataSet.hashTree.getRootHash();
|
|
1360
1670
|
|
|
1361
1671
|
if (dataSet.root !== rootHash) {
|
|
1362
|
-
|
|
1363
|
-
dataSet,
|
|
1364
|
-
rootHash,
|
|
1672
|
+
this.enqueueSyncForDataset(
|
|
1673
|
+
dataSet.name,
|
|
1365
1674
|
`Root hash mismatch: received=${dataSet.root}, ours=${rootHash}`
|
|
1366
1675
|
);
|
|
1367
|
-
} else {
|
|
1368
|
-
LoggerProxy.logger.info(
|
|
1369
|
-
`HashTreeParser#runSyncAlgorithm --> ${this.debugId} "${dataSet.name}" root hash matching: ${rootHash}, version=${dataSet.version}`
|
|
1370
|
-
);
|
|
1371
1676
|
}
|
|
1372
1677
|
}, delay);
|
|
1373
1678
|
} else {
|
|
@@ -1387,38 +1692,39 @@ class HashTreeParser {
|
|
|
1387
1692
|
* @returns {void}
|
|
1388
1693
|
*/
|
|
1389
1694
|
private resetHeartbeatWatchdogs(receivedDataSets: Array<DataSet>): void {
|
|
1390
|
-
if (!this.heartbeatIntervalMs) {
|
|
1391
|
-
return;
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
1695
|
for (const receivedDataSet of receivedDataSets) {
|
|
1395
1696
|
const dataSet = this.dataSets[receivedDataSet.name];
|
|
1396
1697
|
|
|
1397
|
-
if (!dataSet?.hashTree) {
|
|
1398
|
-
// eslint-disable-next-line no-continue
|
|
1399
|
-
continue;
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
1698
|
if (dataSet.heartbeatWatchdogTimer) {
|
|
1403
1699
|
clearTimeout(dataSet.heartbeatWatchdogTimer);
|
|
1404
1700
|
dataSet.heartbeatWatchdogTimer = undefined;
|
|
1405
1701
|
}
|
|
1406
1702
|
|
|
1703
|
+
// dataset-level heartbeatIntervalMs takes priority; fall back to top-level common value
|
|
1704
|
+
const heartbeatIntervalMs = dataSet?.heartbeatIntervalMs ?? this.topLevelHeartbeatIntervalMs;
|
|
1705
|
+
|
|
1706
|
+
if (!dataSet?.hashTree || !heartbeatIntervalMs) {
|
|
1707
|
+
// eslint-disable-next-line no-continue
|
|
1708
|
+
continue;
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1407
1711
|
const backoffTime = this.getWeightedBackoffTime(dataSet.backoff);
|
|
1408
|
-
const delay =
|
|
1712
|
+
const delay = heartbeatIntervalMs + backoffTime;
|
|
1409
1713
|
|
|
1410
|
-
dataSet.heartbeatWatchdogTimer = setTimeout(
|
|
1714
|
+
dataSet.heartbeatWatchdogTimer = setTimeout(() => {
|
|
1411
1715
|
dataSet.heartbeatWatchdogTimer = undefined;
|
|
1412
1716
|
|
|
1413
1717
|
LoggerProxy.logger.warn(
|
|
1414
1718
|
`HashTreeParser#resetHeartbeatWatchdogs --> ${this.debugId} Heartbeat watchdog fired for data set "${dataSet.name}" - no heartbeat received within expected interval, initiating sync`
|
|
1415
1719
|
);
|
|
1416
1720
|
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
dataSet.
|
|
1420
|
-
|
|
1421
|
-
|
|
1721
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.HASH_TREE_HEARTBEAT_WATCHDOG_EXPIRED, {
|
|
1722
|
+
debugId: this.debugId,
|
|
1723
|
+
dataSetName: dataSet.name,
|
|
1724
|
+
});
|
|
1725
|
+
|
|
1726
|
+
this.enqueueSyncForDataset(dataSet.name, `heartbeat watchdog expired`);
|
|
1727
|
+
this.resetHeartbeatWatchdogs([dataSet]);
|
|
1422
1728
|
}, delay);
|
|
1423
1729
|
}
|
|
1424
1730
|
}
|
|
@@ -1451,7 +1757,13 @@ class HashTreeParser {
|
|
|
1451
1757
|
`HashTreeParser#stop --> ${this.debugId} Stopping HashTreeParser, clearing timers and hash trees`
|
|
1452
1758
|
);
|
|
1453
1759
|
this.stopAllTimers();
|
|
1760
|
+
this.syncQueue = [];
|
|
1761
|
+
this.topLevelHeartbeatIntervalMs = undefined;
|
|
1762
|
+
this.syncAllBackoffType = SyncAllBackoffType.NONE;
|
|
1763
|
+
this.dataSetsSyncedDuringBackoff = new Set();
|
|
1454
1764
|
Object.values(this.dataSets).forEach((dataSet) => {
|
|
1765
|
+
dataSet.syncAbortController?.abort();
|
|
1766
|
+
dataSet.syncAbortController = undefined;
|
|
1455
1767
|
dataSet.hashTree = undefined;
|
|
1456
1768
|
});
|
|
1457
1769
|
this.visibleDataSets = [];
|
|
@@ -1459,17 +1771,27 @@ class HashTreeParser {
|
|
|
1459
1771
|
}
|
|
1460
1772
|
|
|
1461
1773
|
/**
|
|
1462
|
-
*
|
|
1774
|
+
* Cleans up the HashTreeParser, stopping all timers and clearing all internal state.
|
|
1775
|
+
* After calling this, the parser should not be used anymore.
|
|
1776
|
+
* @returns {void}
|
|
1777
|
+
*/
|
|
1778
|
+
public cleanUp() {
|
|
1779
|
+
this.stop();
|
|
1780
|
+
this.dataSets = {};
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
/**
|
|
1784
|
+
* Resumes the HashTreeParser that was previously stopped, using a hash tree message.
|
|
1463
1785
|
* @param {HashTreeMessage} message - The message to resume with, it must contain metadata with visible data sets info
|
|
1464
1786
|
* @returns {void}
|
|
1465
1787
|
*/
|
|
1466
|
-
public
|
|
1788
|
+
public resumeFromMessage(message: HashTreeMessage) {
|
|
1467
1789
|
// check that message contains metadata with visible data sets - this is essential to be able to resume
|
|
1468
1790
|
const metadataObject = message.locusStateElements?.find((el) => isMetadata(el));
|
|
1469
1791
|
|
|
1470
1792
|
if (!metadataObject?.data?.visibleDataSets) {
|
|
1471
1793
|
LoggerProxy.logger.warn(
|
|
1472
|
-
`HashTreeParser#
|
|
1794
|
+
`HashTreeParser#resumeFromMessage --> ${this.debugId} Cannot resume HashTreeParser because the message is missing metadata with visible data sets info`
|
|
1473
1795
|
);
|
|
1474
1796
|
|
|
1475
1797
|
return;
|
|
@@ -1490,7 +1812,7 @@ class HashTreeParser {
|
|
|
1490
1812
|
};
|
|
1491
1813
|
}
|
|
1492
1814
|
LoggerProxy.logger.info(
|
|
1493
|
-
`HashTreeParser#
|
|
1815
|
+
`HashTreeParser#resumeFromMessage --> ${
|
|
1494
1816
|
this.debugId
|
|
1495
1817
|
} Resuming HashTreeParser with data sets: ${Object.keys(this.dataSets).join(
|
|
1496
1818
|
', '
|
|
@@ -1501,18 +1823,47 @@ class HashTreeParser {
|
|
|
1501
1823
|
this.handleMessage(message, 'on resume');
|
|
1502
1824
|
}
|
|
1503
1825
|
|
|
1826
|
+
/**
|
|
1827
|
+
* Resumes the HashTreeParser that was previously stopped, using a Locus API response.
|
|
1828
|
+
* Unlike resumeFromMessage(), this does not require metadata/dataSets in the input,
|
|
1829
|
+
* as it fetches all necessary information from Locus via initializeFromGetLociResponse.
|
|
1830
|
+
* @param {LocusDTO} locus - locus object from an API response
|
|
1831
|
+
* @returns {Promise}
|
|
1832
|
+
*/
|
|
1833
|
+
public async resumeFromApiResponse(locus: LocusDTO) {
|
|
1834
|
+
this.state = 'active';
|
|
1835
|
+
this.dataSets = {};
|
|
1836
|
+
|
|
1837
|
+
LoggerProxy.logger.info(
|
|
1838
|
+
`HashTreeParser#resumeFromApiResponse --> ${this.debugId} Resuming HashTreeParser from API response`
|
|
1839
|
+
);
|
|
1840
|
+
|
|
1841
|
+
await this.initializeFromGetLociResponse(locus);
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1504
1844
|
private checkForSentinelHttpResponse(error: any, dataSetName?: string) {
|
|
1845
|
+
// 404 for any dataset means the locus is no longer available at this URL - could be replaced or ended
|
|
1846
|
+
// if a dataset is just not visible, we would get a 400
|
|
1847
|
+
if (error.statusCode === 404) {
|
|
1848
|
+
LoggerProxy.logger.info(
|
|
1849
|
+
`HashTreeParser#checkForSentinelHttpResponse --> ${this.debugId} Received 404 for data set "${dataSetName}", locus not found`
|
|
1850
|
+
);
|
|
1851
|
+
this.stopAllTimers();
|
|
1852
|
+
|
|
1853
|
+
throw new LocusNotFoundError();
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1505
1856
|
const isValidDataSetForSentinel =
|
|
1506
1857
|
dataSetName === undefined ||
|
|
1507
1858
|
PossibleSentinelMessageDataSetNames.includes(dataSetName.toLowerCase());
|
|
1508
1859
|
|
|
1509
1860
|
if (
|
|
1510
|
-
|
|
1511
|
-
|
|
1861
|
+
error.statusCode === 409 &&
|
|
1862
|
+
error.body?.errorCode === LocusErrorCodes.LOCUS_INACTIVE &&
|
|
1512
1863
|
isValidDataSetForSentinel
|
|
1513
1864
|
) {
|
|
1514
1865
|
LoggerProxy.logger.info(
|
|
1515
|
-
`HashTreeParser#checkForSentinelHttpResponse --> ${this.debugId} Received ${error.statusCode} for data set "${dataSetName}", indicating that the meeting has ended`
|
|
1866
|
+
`HashTreeParser#checkForSentinelHttpResponse --> ${this.debugId} Received ${error.statusCode}/${error.body?.errorCode} for data set "${dataSetName}", indicating that the meeting has ended`
|
|
1516
1867
|
);
|
|
1517
1868
|
this.stopAllTimers();
|
|
1518
1869
|
|
|
@@ -1524,7 +1875,7 @@ class HashTreeParser {
|
|
|
1524
1875
|
* Gets the current hashes from the locus for a specific data set.
|
|
1525
1876
|
* @param {string} dataSetName
|
|
1526
1877
|
* @param {string} currentRootHash
|
|
1527
|
-
* @returns {
|
|
1878
|
+
* @returns {Object|null} An object containing the hashes and leaf count, or null if the hashes match and no sync is needed
|
|
1528
1879
|
*/
|
|
1529
1880
|
private getHashesFromLocus(dataSetName: string, currentRootHash: string) {
|
|
1530
1881
|
LoggerProxy.logger.info(
|
|
@@ -1543,6 +1894,15 @@ class HashTreeParser {
|
|
|
1543
1894
|
},
|
|
1544
1895
|
})
|
|
1545
1896
|
.then((response) => {
|
|
1897
|
+
if (!response.body || isEmpty(response.body)) {
|
|
1898
|
+
// 204 with empty body means our hashes match Locus, no sync needed
|
|
1899
|
+
LoggerProxy.logger.info(
|
|
1900
|
+
`HashTreeParser#getHashesFromLocus --> ${this.debugId} Got ${response.statusCode} with empty body for data set "${dataSetName}", hashes match - no sync needed`
|
|
1901
|
+
);
|
|
1902
|
+
|
|
1903
|
+
return null;
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1546
1906
|
const hashes = response.body?.hashes as string[] | undefined;
|
|
1547
1907
|
const dataSetFromResponse = response.body?.dataSet;
|
|
1548
1908
|
|
|
@@ -1571,6 +1931,13 @@ class HashTreeParser {
|
|
|
1571
1931
|
error
|
|
1572
1932
|
);
|
|
1573
1933
|
this.checkForSentinelHttpResponse(error, dataSet.name);
|
|
1934
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.HASH_TREE_SYNC_FAILURE, {
|
|
1935
|
+
debugId: this.debugId,
|
|
1936
|
+
dataSetName,
|
|
1937
|
+
request: 'GET /hashtree',
|
|
1938
|
+
statusCode: error.statusCode,
|
|
1939
|
+
reason: error.message,
|
|
1940
|
+
});
|
|
1574
1941
|
|
|
1575
1942
|
throw error;
|
|
1576
1943
|
});
|
|
@@ -1580,29 +1947,43 @@ class HashTreeParser {
|
|
|
1580
1947
|
* Sends a sync request to Locus for the specified data set.
|
|
1581
1948
|
*
|
|
1582
1949
|
* @param {InternalDataSet} dataSet The data set to sync.
|
|
1583
|
-
* @param {
|
|
1950
|
+
* @param {Object} options Either `{ isInitialization: true }` for init syncs (uses leafCount=1 with empty leaf data) or `{ mismatchedLeavesData }` for normal syncs.
|
|
1584
1951
|
* @returns {Promise<HashTreeMessage|null>}
|
|
1585
1952
|
*/
|
|
1586
1953
|
private sendSyncRequestToLocus(
|
|
1587
1954
|
dataSet: InternalDataSet,
|
|
1588
|
-
mismatchedLeavesData: Record<number, LeafDataItem[]>
|
|
1955
|
+
options: {isInitialization: true} | {mismatchedLeavesData: Record<number, LeafDataItem[]>}
|
|
1589
1956
|
): Promise<HashTreeMessage | null> {
|
|
1590
1957
|
LoggerProxy.logger.info(
|
|
1591
1958
|
`HashTreeParser#sendSyncRequestToLocus --> ${this.debugId} Sending sync request for data set "${dataSet.name}"`
|
|
1592
1959
|
);
|
|
1593
1960
|
|
|
1961
|
+
const isInitialization = 'isInitialization' in options;
|
|
1962
|
+
|
|
1594
1963
|
const url = `${dataSet.url}/sync`;
|
|
1595
|
-
const body
|
|
1596
|
-
leafCount:
|
|
1964
|
+
const body: {
|
|
1965
|
+
leafCount: number;
|
|
1966
|
+
leafDataEntries: {leafIndex: number; elementIds: LeafDataItem[]}[];
|
|
1967
|
+
} = {
|
|
1968
|
+
leafCount: isInitialization ? 1 : dataSet.leafCount,
|
|
1597
1969
|
leafDataEntries: [],
|
|
1598
1970
|
};
|
|
1599
1971
|
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1972
|
+
if (isInitialization) {
|
|
1973
|
+
// initialization sync: Locus requires leafCount=1 with a single empty leaf
|
|
1974
|
+
body.leafDataEntries.push({leafIndex: 0, elementIds: []});
|
|
1975
|
+
} else {
|
|
1976
|
+
const {mismatchedLeavesData} = options;
|
|
1977
|
+
|
|
1978
|
+
Object.keys(mismatchedLeavesData).forEach((index) => {
|
|
1979
|
+
const leafIndex = parseInt(index, 10);
|
|
1980
|
+
|
|
1981
|
+
body.leafDataEntries.push({
|
|
1982
|
+
leafIndex,
|
|
1983
|
+
elementIds: mismatchedLeavesData[leafIndex],
|
|
1984
|
+
});
|
|
1604
1985
|
});
|
|
1605
|
-
}
|
|
1986
|
+
}
|
|
1606
1987
|
|
|
1607
1988
|
const ourCurrentRootHash = dataSet.hashTree ? dataSet.hashTree.getRootHash() : EMPTY_HASH;
|
|
1608
1989
|
|
|
@@ -1615,10 +1996,6 @@ class HashTreeParser {
|
|
|
1615
1996
|
body,
|
|
1616
1997
|
})
|
|
1617
1998
|
.then((resp) => {
|
|
1618
|
-
LoggerProxy.logger.info(
|
|
1619
|
-
`HashTreeParser#sendSyncRequestToLocus --> ${this.debugId} Sync request succeeded for "${dataSet.name}"`
|
|
1620
|
-
);
|
|
1621
|
-
|
|
1622
1999
|
if (!resp.body || isEmpty(resp.body)) {
|
|
1623
2000
|
LoggerProxy.logger.info(
|
|
1624
2001
|
`HashTreeParser#sendSyncRequestToLocus --> ${this.debugId} Got ${resp.statusCode} with empty body for sync request for data set "${dataSet.name}", data should arrive via messages`
|
|
@@ -1635,6 +2012,13 @@ class HashTreeParser {
|
|
|
1635
2012
|
error
|
|
1636
2013
|
);
|
|
1637
2014
|
this.checkForSentinelHttpResponse(error, dataSet.name);
|
|
2015
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.HASH_TREE_SYNC_FAILURE, {
|
|
2016
|
+
debugId: this.debugId,
|
|
2017
|
+
dataSetName: dataSet.name,
|
|
2018
|
+
request: 'POST /sync',
|
|
2019
|
+
statusCode: error.statusCode,
|
|
2020
|
+
reason: error.message,
|
|
2021
|
+
});
|
|
1638
2022
|
|
|
1639
2023
|
throw error;
|
|
1640
2024
|
});
|