@fluidframework/container-runtime 0.57.0-51086 → 0.58.0-55561
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/dist/batchTracker.d.ts +26 -0
- package/dist/batchTracker.d.ts.map +1 -0
- package/dist/batchTracker.js +59 -0
- package/dist/batchTracker.js.map +1 -0
- package/dist/containerRuntime.d.ts +12 -7
- package/dist/containerRuntime.d.ts.map +1 -1
- package/dist/containerRuntime.js +125 -55
- package/dist/containerRuntime.js.map +1 -1
- package/dist/dataStore.d.ts +1 -36
- package/dist/dataStore.d.ts.map +1 -1
- package/dist/dataStore.js +5 -27
- package/dist/dataStore.js.map +1 -1
- package/dist/dataStoreContext.d.ts +5 -7
- package/dist/dataStoreContext.d.ts.map +1 -1
- package/dist/dataStoreContext.js +12 -7
- package/dist/dataStoreContext.js.map +1 -1
- package/dist/dataStores.d.ts +1 -1
- package/dist/dataStores.d.ts.map +1 -1
- package/dist/dataStores.js +3 -3
- package/dist/dataStores.js.map +1 -1
- package/dist/garbageCollection.d.ts +25 -11
- package/dist/garbageCollection.d.ts.map +1 -1
- package/dist/garbageCollection.js +100 -57
- package/dist/garbageCollection.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -3
- package/dist/index.js.map +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/dist/pendingStateManager.d.ts.map +1 -1
- package/dist/pendingStateManager.js +1 -6
- package/dist/pendingStateManager.js.map +1 -1
- package/dist/runningSummarizer.d.ts +1 -1
- package/dist/runningSummarizer.d.ts.map +1 -1
- package/dist/runningSummarizer.js +1 -1
- package/dist/runningSummarizer.js.map +1 -1
- package/dist/summarizer.d.ts +3 -4
- package/dist/summarizer.d.ts.map +1 -1
- package/dist/summarizer.js +8 -9
- package/dist/summarizer.js.map +1 -1
- package/dist/summaryGenerator.d.ts +1 -1
- package/dist/summaryGenerator.d.ts.map +1 -1
- package/dist/summaryGenerator.js +1 -1
- package/dist/summaryGenerator.js.map +1 -1
- package/dist/summaryManager.d.ts +2 -6
- package/dist/summaryManager.d.ts.map +1 -1
- package/dist/summaryManager.js +4 -10
- package/dist/summaryManager.js.map +1 -1
- package/lib/batchTracker.d.ts +26 -0
- package/lib/batchTracker.d.ts.map +1 -0
- package/lib/batchTracker.js +54 -0
- package/lib/batchTracker.js.map +1 -0
- package/lib/containerRuntime.d.ts +12 -7
- package/lib/containerRuntime.d.ts.map +1 -1
- package/lib/containerRuntime.js +126 -56
- package/lib/containerRuntime.js.map +1 -1
- package/lib/dataStore.d.ts +1 -36
- package/lib/dataStore.d.ts.map +1 -1
- package/lib/dataStore.js +4 -26
- package/lib/dataStore.js.map +1 -1
- package/lib/dataStoreContext.d.ts +5 -7
- package/lib/dataStoreContext.d.ts.map +1 -1
- package/lib/dataStoreContext.js +13 -8
- package/lib/dataStoreContext.js.map +1 -1
- package/lib/dataStores.d.ts +1 -1
- package/lib/dataStores.d.ts.map +1 -1
- package/lib/dataStores.js +3 -3
- package/lib/dataStores.js.map +1 -1
- package/lib/garbageCollection.d.ts +25 -11
- package/lib/garbageCollection.d.ts.map +1 -1
- package/lib/garbageCollection.js +98 -55
- package/lib/garbageCollection.js.map +1 -1
- package/lib/index.d.ts +0 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +0 -1
- package/lib/index.js.map +1 -1
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/lib/pendingStateManager.d.ts.map +1 -1
- package/lib/pendingStateManager.js +1 -6
- package/lib/pendingStateManager.js.map +1 -1
- package/lib/runningSummarizer.d.ts +1 -1
- package/lib/runningSummarizer.d.ts.map +1 -1
- package/lib/runningSummarizer.js +1 -1
- package/lib/runningSummarizer.js.map +1 -1
- package/lib/summarizer.d.ts +3 -4
- package/lib/summarizer.d.ts.map +1 -1
- package/lib/summarizer.js +8 -9
- package/lib/summarizer.js.map +1 -1
- package/lib/summaryGenerator.d.ts +1 -1
- package/lib/summaryGenerator.d.ts.map +1 -1
- package/lib/summaryGenerator.js +1 -1
- package/lib/summaryGenerator.js.map +1 -1
- package/lib/summaryManager.d.ts +2 -6
- package/lib/summaryManager.d.ts.map +1 -1
- package/lib/summaryManager.js +5 -11
- package/lib/summaryManager.js.map +1 -1
- package/package.json +12 -12
- package/src/batchTracker.ts +80 -0
- package/src/containerRuntime.ts +180 -63
- package/src/dataStore.ts +6 -42
- package/src/dataStoreContext.ts +17 -15
- package/src/dataStores.ts +9 -4
- package/src/garbageCollection.ts +125 -67
- package/src/index.ts +0 -1
- package/src/packageVersion.ts +1 -1
- package/src/pendingStateManager.ts +4 -8
- package/src/runningSummarizer.ts +3 -3
- package/src/summarizer.ts +8 -8
- package/src/summaryGenerator.ts +2 -2
- package/src/summaryManager.ts +5 -20
package/src/containerRuntime.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
|
3
3
|
* Licensed under the MIT License.
|
|
4
4
|
*/
|
|
5
|
-
|
|
5
|
+
// See #9219
|
|
6
|
+
/* eslint-disable max-lines */
|
|
6
7
|
import { EventEmitter } from "events";
|
|
7
8
|
import { ITelemetryBaseLogger, ITelemetryGenericEvent, ITelemetryLogger } from "@fluidframework/common-definitions";
|
|
8
9
|
import {
|
|
@@ -21,7 +22,6 @@ import {
|
|
|
21
22
|
IDeltaManager,
|
|
22
23
|
IDeltaSender,
|
|
23
24
|
IRuntime,
|
|
24
|
-
ContainerWarning,
|
|
25
25
|
ICriticalContainerError,
|
|
26
26
|
AttachState,
|
|
27
27
|
ILoaderOptions,
|
|
@@ -86,6 +86,7 @@ import {
|
|
|
86
86
|
SummarizeInternalFn,
|
|
87
87
|
channelsTreeName,
|
|
88
88
|
IAttachMessage,
|
|
89
|
+
IDataStore,
|
|
89
90
|
} from "@fluidframework/runtime-definitions";
|
|
90
91
|
import {
|
|
91
92
|
addBlobToSummary,
|
|
@@ -146,12 +147,11 @@ import {
|
|
|
146
147
|
IGCStats,
|
|
147
148
|
} from "./garbageCollection";
|
|
148
149
|
import {
|
|
149
|
-
AliasResult,
|
|
150
150
|
channelToDataStore,
|
|
151
|
-
IDataStore,
|
|
152
151
|
IDataStoreAliasMessage,
|
|
153
152
|
isDataStoreAliasMessage,
|
|
154
153
|
} from "./dataStore";
|
|
154
|
+
import { BindBatchTracker } from "./batchTracker";
|
|
155
155
|
|
|
156
156
|
export enum ContainerMessageType {
|
|
157
157
|
// An op to be delivered to store
|
|
@@ -329,13 +329,23 @@ export enum RuntimeHeaders {
|
|
|
329
329
|
* have the untagged logger, so to accommodate that scenario the below interface is used. It can be removed once
|
|
330
330
|
* its usage is removed from TaggedLoggerAdapter fallback.
|
|
331
331
|
*/
|
|
332
|
-
interface OldContainerContextWithLogger extends IContainerContext {
|
|
332
|
+
interface OldContainerContextWithLogger extends Omit<IContainerContext, "taggedLogger"> {
|
|
333
333
|
logger: ITelemetryBaseLogger;
|
|
334
|
+
taggedLogger: undefined;
|
|
334
335
|
}
|
|
335
336
|
|
|
336
|
-
// Local storage key to set the default flush mode to TurnBased
|
|
337
|
-
const turnBasedFlushModeKey = "Fluid.ContainerRuntime.FlushModeTurnBased";
|
|
338
337
|
const useDataStoreAliasingKey = "Fluid.ContainerRuntime.UseDataStoreAliasing";
|
|
338
|
+
const maxConsecutiveReconnectsKey = "Fluid.ContainerRuntime.MaxConsecutiveReconnects";
|
|
339
|
+
|
|
340
|
+
// Feature gate for the max op size. If the value is negative, chunking is enabled
|
|
341
|
+
// and all ops over 16k would be chunked. If the value is positive, all ops with
|
|
342
|
+
// a size strictly larger will be rejected and the container closed with an error.
|
|
343
|
+
const maxOpSizeInBytesKey = "Fluid.ContainerRuntime.MaxOpSizeInBytes";
|
|
344
|
+
|
|
345
|
+
// By default, we should reject any op larger than 768KB,
|
|
346
|
+
// in order to account for some extra overhead from serialization
|
|
347
|
+
// to not reach the 1MB limits in socket.io and Kafka.
|
|
348
|
+
const defaultMaxOpSizeInBytes = 768000;
|
|
339
349
|
|
|
340
350
|
export enum RuntimeMessage {
|
|
341
351
|
FluidDataStoreOp = "component",
|
|
@@ -686,8 +696,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
686
696
|
): Promise<ContainerRuntime> {
|
|
687
697
|
// If taggedLogger exists, use it. Otherwise, wrap the vanilla logger:
|
|
688
698
|
// back-compat: Remove the TaggedLoggerAdapter fallback once all the host are using loader > 0.45
|
|
689
|
-
const
|
|
690
|
-
|
|
699
|
+
const backCompatContext: IContainerContext | OldContainerContextWithLogger = context;
|
|
700
|
+
const passLogger = backCompatContext.taggedLogger ??
|
|
701
|
+
new TaggedLoggerAdapter((backCompatContext as OldContainerContextWithLogger).logger);
|
|
691
702
|
const logger = ChildLogger.create(passLogger, undefined, {
|
|
692
703
|
all: {
|
|
693
704
|
runtimeVersion: pkgVersion,
|
|
@@ -769,7 +780,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
769
780
|
if (loadSequenceNumberVerification !== "bypass" && runtimeSequenceNumber !== protocolSequenceNumber) {
|
|
770
781
|
// "Load from summary, runtime metadata sequenceNumber !== initialSequenceNumber"
|
|
771
782
|
const error = new DataCorruptionError(
|
|
772
|
-
"
|
|
783
|
+
"Summary metadata mismatch",
|
|
773
784
|
{ runtimeSequenceNumber, protocolSequenceNumber },
|
|
774
785
|
);
|
|
775
786
|
|
|
@@ -891,9 +902,13 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
891
902
|
|
|
892
903
|
private readonly summarizerNode: IRootSummarizerNodeWithGC;
|
|
893
904
|
private readonly _aliasingEnabled: boolean;
|
|
905
|
+
private readonly _maxOpSizeInBytes: number;
|
|
906
|
+
|
|
907
|
+
private readonly maxConsecutiveReconnects: number;
|
|
908
|
+
private readonly defaultMaxConsecutiveReconnects = 15;
|
|
894
909
|
|
|
895
910
|
private _orderSequentiallyCalls: number = 0;
|
|
896
|
-
private _flushMode: FlushMode;
|
|
911
|
+
private _flushMode: FlushMode = FlushMode.TurnBased;
|
|
897
912
|
private needsFlush = false;
|
|
898
913
|
private flushTrigger = false;
|
|
899
914
|
|
|
@@ -901,6 +916,8 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
901
916
|
|
|
902
917
|
private paused: boolean = false;
|
|
903
918
|
|
|
919
|
+
private consecutiveReconnects = 0;
|
|
920
|
+
|
|
904
921
|
public get connected(): boolean {
|
|
905
922
|
return this._connected;
|
|
906
923
|
}
|
|
@@ -927,8 +944,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
927
944
|
private dirtyContainer: boolean;
|
|
928
945
|
private emitDirtyDocumentEvent = true;
|
|
929
946
|
|
|
930
|
-
private readonly summarizerWarning = (warning: ContainerWarning) =>
|
|
931
|
-
this.mc.logger.sendTelemetryEvent({ eventName: "summarizerWarning" }, warning);
|
|
932
947
|
/**
|
|
933
948
|
* Summarizer is responsible for coordinating when to send generate and send summaries.
|
|
934
949
|
* It is the main entry point for summary work.
|
|
@@ -952,8 +967,8 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
952
967
|
* and is the single source of truth for this container.
|
|
953
968
|
*/
|
|
954
969
|
public readonly disableIsolatedChannels: boolean;
|
|
955
|
-
/** The message
|
|
956
|
-
private
|
|
970
|
+
/** The last message processed at the time of the last summary. */
|
|
971
|
+
private messageAtLastSummary: ISummaryMetadataMessage | undefined;
|
|
957
972
|
|
|
958
973
|
private get summarizer(): Summarizer {
|
|
959
974
|
assert(this._summarizer !== undefined, 0x257 /* "This is not summarizing container" */);
|
|
@@ -984,7 +999,8 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
984
999
|
private _storage?: IDocumentStorageService,
|
|
985
1000
|
) {
|
|
986
1001
|
super();
|
|
987
|
-
|
|
1002
|
+
|
|
1003
|
+
this.messageAtLastSummary = metadata?.message;
|
|
988
1004
|
|
|
989
1005
|
// If this is an existing container, we get values from metadata.
|
|
990
1006
|
// otherwise, we initialize them.
|
|
@@ -1012,33 +1028,25 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1012
1028
|
this.mc = loggerToMonitoringContext(
|
|
1013
1029
|
ChildLogger.create(this.logger, "ContainerRuntime"));
|
|
1014
1030
|
|
|
1015
|
-
this._flushMode =
|
|
1016
|
-
this.mc.config.getBoolean(turnBasedFlushModeKey) ?? false
|
|
1017
|
-
? FlushMode.TurnBased : FlushMode.Immediate;
|
|
1018
|
-
|
|
1019
1031
|
this._aliasingEnabled =
|
|
1020
1032
|
(this.mc.config.getBoolean(useDataStoreAliasingKey) ?? false) ||
|
|
1021
1033
|
(runtimeOptions.useDataStoreAliasing ?? false);
|
|
1022
1034
|
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
* we don't have an op (on demand summaries for instance). In those cases, we will use the timestamp
|
|
1028
|
-
* of this client's connection.
|
|
1029
|
-
*/
|
|
1030
|
-
const getCurrentTimestamp = () => {
|
|
1031
|
-
const client = this.clientId !== undefined ? this.getAudience().getMember(this.clientId) : undefined;
|
|
1032
|
-
const timestamp = client?.timestamp;
|
|
1033
|
-
return this.deltaManager.lastMessage?.timestamp ?? timestamp ?? Date.now();
|
|
1034
|
-
};
|
|
1035
|
+
this._maxOpSizeInBytes = (this.mc.config.getNumber(maxOpSizeInBytesKey) ?? defaultMaxOpSizeInBytes);
|
|
1036
|
+
this.maxConsecutiveReconnects =
|
|
1037
|
+
this.mc.config.getNumber(maxConsecutiveReconnectsKey) ?? this.defaultMaxConsecutiveReconnects;
|
|
1038
|
+
|
|
1035
1039
|
this.garbageCollector = GarbageCollector.create(
|
|
1036
1040
|
this,
|
|
1037
1041
|
this.runtimeOptions.gcOptions,
|
|
1038
1042
|
(unusedRoutes: string[]) => this.dataStores.deleteUnusedRoutes(unusedRoutes),
|
|
1039
1043
|
(nodePath: string) => this.dataStores.getNodePackagePath(nodePath),
|
|
1040
|
-
|
|
1041
|
-
|
|
1044
|
+
/**
|
|
1045
|
+
* Returns the timestamp of the last message seen by this client. This is used by garbage collector as
|
|
1046
|
+
* the current reference timestamp for tracking unreferenced objects.
|
|
1047
|
+
*/
|
|
1048
|
+
() => this.deltaManager.lastMessage?.timestamp ?? this.messageAtLastSummary?.timestamp,
|
|
1049
|
+
() => this.messageAtLastSummary?.timestamp,
|
|
1042
1050
|
context.baseSnapshot,
|
|
1043
1051
|
async <T>(id: string) => readAndParse<T>(this.storage, id),
|
|
1044
1052
|
this.mc.logger,
|
|
@@ -1090,8 +1098,12 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1090
1098
|
(id: string) => this.summarizerNode.deleteChild(id),
|
|
1091
1099
|
this.mc.logger,
|
|
1092
1100
|
async () => this.garbageCollector.getDataStoreBaseGCDetails(),
|
|
1093
|
-
(
|
|
1094
|
-
|
|
1101
|
+
(path: string, timestampMs: number, packagePath?: readonly string[]) => this.garbageCollector.nodeUpdated(
|
|
1102
|
+
path,
|
|
1103
|
+
"Changed",
|
|
1104
|
+
timestampMs,
|
|
1105
|
+
packagePath,
|
|
1106
|
+
),
|
|
1095
1107
|
new Map<string, string>(dataStoreAliasMap),
|
|
1096
1108
|
this.garbageCollector.writeDataAtRoot,
|
|
1097
1109
|
);
|
|
@@ -1213,7 +1225,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1213
1225
|
},
|
|
1214
1226
|
this.runtimeOptions.summaryOptions.summarizerOptions,
|
|
1215
1227
|
);
|
|
1216
|
-
this.summaryManager.on("summarizerWarning", this.summarizerWarning);
|
|
1217
1228
|
this.summaryManager.start();
|
|
1218
1229
|
}
|
|
1219
1230
|
}
|
|
@@ -1263,6 +1274,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1263
1274
|
});
|
|
1264
1275
|
|
|
1265
1276
|
ReportOpPerfTelemetry(this.context.clientId, this.deltaManager, this.logger);
|
|
1277
|
+
BindBatchTracker(this, this.logger);
|
|
1266
1278
|
}
|
|
1267
1279
|
|
|
1268
1280
|
public dispose(error?: Error): void {
|
|
@@ -1279,7 +1291,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1279
1291
|
}, error);
|
|
1280
1292
|
|
|
1281
1293
|
if (this.summaryManager !== undefined) {
|
|
1282
|
-
this.summaryManager.off("summarizerWarning", this.summarizerWarning);
|
|
1283
1294
|
this.summaryManager.dispose();
|
|
1284
1295
|
}
|
|
1285
1296
|
this.garbageCollector.dispose();
|
|
@@ -1393,9 +1404,13 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1393
1404
|
}
|
|
1394
1405
|
|
|
1395
1406
|
const dataStoreChannel = await dataStoreContext.realize();
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1407
|
+
this.garbageCollector.nodeUpdated(
|
|
1408
|
+
`/${id}`,
|
|
1409
|
+
"Loaded",
|
|
1410
|
+
undefined /* timestampMs */,
|
|
1411
|
+
dataStoreContext.packagePath,
|
|
1412
|
+
request?.headers,
|
|
1413
|
+
);
|
|
1399
1414
|
return dataStoreChannel;
|
|
1400
1415
|
}
|
|
1401
1416
|
|
|
@@ -1406,9 +1421,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1406
1421
|
summaryFormatVersion: 1,
|
|
1407
1422
|
disableIsolatedChannels: this.disableIsolatedChannels || undefined,
|
|
1408
1423
|
gcFeature: this.garbageCollector.gcSummaryFeatureVersion,
|
|
1409
|
-
// The last message processed at the time of summary. If there are no messages,
|
|
1410
|
-
//
|
|
1411
|
-
message: extractSummaryMetadataMessage(this.deltaManager.lastMessage) ?? this.
|
|
1424
|
+
// The last message processed at the time of summary. If there are no new messages, use the message from the
|
|
1425
|
+
// last summary.
|
|
1426
|
+
message: extractSummaryMetadataMessage(this.deltaManager.lastMessage) ?? this.messageAtLastSummary,
|
|
1412
1427
|
sessionExpiryTimeoutMs: this.garbageCollector.sessionExpiryTimeoutMs,
|
|
1413
1428
|
};
|
|
1414
1429
|
}
|
|
@@ -1446,6 +1461,42 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1446
1461
|
}
|
|
1447
1462
|
}
|
|
1448
1463
|
|
|
1464
|
+
// Track how many times the container tries to reconnect with pending messages.
|
|
1465
|
+
// This happens when the connection state is changed and we reset the counter
|
|
1466
|
+
// when we are able to process a local op or when there are no pending messages.
|
|
1467
|
+
// If this counter reaches a max, it's a good indicator that the container
|
|
1468
|
+
// is not making progress and it is stuck in a retry loop.
|
|
1469
|
+
private shouldContinueReconnecting(): boolean {
|
|
1470
|
+
if (this.maxConsecutiveReconnects <= 0) {
|
|
1471
|
+
// Feature disabled, we never stop reconnecting
|
|
1472
|
+
return true;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
if (!this.pendingStateManager.hasPendingMessages()) {
|
|
1476
|
+
// If there are no pending messages, we can always reconnect
|
|
1477
|
+
this.resetReconnectCount();
|
|
1478
|
+
return true;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
this.consecutiveReconnects++;
|
|
1482
|
+
if (this.consecutiveReconnects === Math.floor(this.maxConsecutiveReconnects / 2)) {
|
|
1483
|
+
// If we're halfway through the max reconnects, send an event in order
|
|
1484
|
+
// to better identify false positives, if any. If the rate of this event
|
|
1485
|
+
// matches Container Close count below, we can safely cut down
|
|
1486
|
+
// maxConsecutiveReconnects to half.
|
|
1487
|
+
this.mc.logger.sendTelemetryEvent({
|
|
1488
|
+
eventName: "ReconnectsWithNoProgress",
|
|
1489
|
+
attempts: this.consecutiveReconnects,
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
return this.consecutiveReconnects < this.maxConsecutiveReconnects;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
private resetReconnectCount() {
|
|
1497
|
+
this.consecutiveReconnects = 0;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1449
1500
|
private replayPendingStates() {
|
|
1450
1501
|
// We need to be able to send ops to replay states
|
|
1451
1502
|
if (!this.canSendOps()) { return; }
|
|
@@ -1526,6 +1577,14 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1526
1577
|
if (changeOfState) {
|
|
1527
1578
|
this.deltaManager.off("op", this.onOp);
|
|
1528
1579
|
this.context.pendingLocalState = undefined;
|
|
1580
|
+
if (!this.shouldContinueReconnecting()) {
|
|
1581
|
+
this.closeFn(new GenericError(
|
|
1582
|
+
"Runtime detected too many reconnects with no progress syncing local ops",
|
|
1583
|
+
undefined, // error
|
|
1584
|
+
{ attempts: this.consecutiveReconnects }));
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1529
1588
|
this.replayPendingStates();
|
|
1530
1589
|
}
|
|
1531
1590
|
|
|
@@ -1589,6 +1648,13 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1589
1648
|
|
|
1590
1649
|
this.emit("op", message);
|
|
1591
1650
|
this.scheduleManager.afterOpProcessing(undefined, message);
|
|
1651
|
+
|
|
1652
|
+
if (local) {
|
|
1653
|
+
// If we have processed a local op, this means that the container is
|
|
1654
|
+
// making progress and we can reset the counter for how many times
|
|
1655
|
+
// we have consecutively replayed the pending states
|
|
1656
|
+
this.resetReconnectCount();
|
|
1657
|
+
}
|
|
1592
1658
|
} catch (e) {
|
|
1593
1659
|
this.scheduleManager.afterOpProcessing(e, message);
|
|
1594
1660
|
throw e;
|
|
@@ -1696,7 +1762,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1696
1762
|
this._orderSequentiallyCalls++;
|
|
1697
1763
|
callback();
|
|
1698
1764
|
} catch (error) {
|
|
1699
|
-
this.closeFn(new GenericError("
|
|
1765
|
+
this.closeFn(new GenericError("orderSequentially callback exception", error));
|
|
1700
1766
|
throw error; // throw the original error for the consumer of the runtime
|
|
1701
1767
|
} finally {
|
|
1702
1768
|
this._orderSequentiallyCalls--;
|
|
@@ -1742,12 +1808,12 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1742
1808
|
* @param props - Properties for the data store
|
|
1743
1809
|
* @returns - An aliased data store which can can be found / loaded by alias.
|
|
1744
1810
|
*/
|
|
1745
|
-
private async createAndAliasDataStore(pkg: string | string[], alias: string, props?: any): Promise<
|
|
1811
|
+
private async createAndAliasDataStore(pkg: string | string[], alias: string, props?: any): Promise<IDataStore> {
|
|
1746
1812
|
const internalId = uuid();
|
|
1747
1813
|
const dataStore = await this._createDataStore(pkg, false /* isRoot */, internalId, props);
|
|
1748
|
-
const aliasedDataStore = channelToDataStore(dataStore, internalId, this,this.dataStores, this.mc.logger);
|
|
1814
|
+
const aliasedDataStore = channelToDataStore(dataStore, internalId, this, this.dataStores, this.mc.logger);
|
|
1749
1815
|
const result = await aliasedDataStore.trySetAlias(alias);
|
|
1750
|
-
if (result !==
|
|
1816
|
+
if (result !== "Success") {
|
|
1751
1817
|
throw new GenericError(
|
|
1752
1818
|
"dataStoreAliasFailure",
|
|
1753
1819
|
undefined /* error */,
|
|
@@ -1789,13 +1855,13 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1789
1855
|
props?: any,
|
|
1790
1856
|
id = uuid(),
|
|
1791
1857
|
isRoot = false,
|
|
1792
|
-
): Promise<
|
|
1858
|
+
): Promise<IDataStore> {
|
|
1793
1859
|
const fluidDataStore = await this.dataStores._createFluidDataStoreContext(
|
|
1794
1860
|
Array.isArray(pkg) ? pkg : [pkg], id, isRoot, props).realize();
|
|
1795
1861
|
if (isRoot) {
|
|
1796
1862
|
fluidDataStore.bindToContext();
|
|
1797
1863
|
}
|
|
1798
|
-
return fluidDataStore;
|
|
1864
|
+
return channelToDataStore(fluidDataStore, id, this, this.dataStores, this.mc.logger);
|
|
1799
1865
|
}
|
|
1800
1866
|
|
|
1801
1867
|
public async _createDataStoreWithProps(
|
|
@@ -1803,7 +1869,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1803
1869
|
props?: any,
|
|
1804
1870
|
id = uuid(),
|
|
1805
1871
|
isRoot = false,
|
|
1806
|
-
): Promise<
|
|
1872
|
+
): Promise<IDataStore> {
|
|
1807
1873
|
return this._aliasingEnabled === true && isRoot ?
|
|
1808
1874
|
this.createAndAliasDataStore(pkg, id, props) :
|
|
1809
1875
|
this._createDataStoreWithPropsLegacy(pkg, props, id, isRoot);
|
|
@@ -2076,6 +2142,15 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2076
2142
|
const summaryRefSeqNum = this.deltaManager.lastSequenceNumber;
|
|
2077
2143
|
const message = `Summary @${summaryRefSeqNum}:${this.deltaManager.minimumSequenceNumber}`;
|
|
2078
2144
|
|
|
2145
|
+
// We should be here is we haven't processed be here. If we are of if the last message's sequence number
|
|
2146
|
+
// doesn't match the last processed sequence number, log an error.
|
|
2147
|
+
if (summaryRefSeqNum !== this.deltaManager.lastMessage?.sequenceNumber) {
|
|
2148
|
+
summaryLogger.sendErrorEvent({
|
|
2149
|
+
eventName: "LastSequenceMismatch",
|
|
2150
|
+
message,
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2079
2154
|
this.summarizerNode.startSummary(summaryRefSeqNum, summaryLogger);
|
|
2080
2155
|
|
|
2081
2156
|
// Helper function to check whether we should still continue between each async step.
|
|
@@ -2136,6 +2211,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2136
2211
|
}
|
|
2137
2212
|
const { summary: summaryTree, stats: partialStats } = summarizeResult;
|
|
2138
2213
|
|
|
2214
|
+
// Now that we have generated the summary, update the message at last summary to the last message processed.
|
|
2215
|
+
this.messageAtLastSummary = this.deltaManager.lastMessage;
|
|
2216
|
+
|
|
2139
2217
|
// Counting dataStores and handles
|
|
2140
2218
|
// Because handles are unchanged dataStores in the current logic,
|
|
2141
2219
|
// summarized dataStore count is total dataStore count minus handle count
|
|
@@ -2342,18 +2420,13 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2342
2420
|
}
|
|
2343
2421
|
}
|
|
2344
2422
|
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
/* batch: */ this._flushMode === FlushMode.TurnBased,
|
|
2353
|
-
opMetadataInternal);
|
|
2354
|
-
} else {
|
|
2355
|
-
clientSequenceNumber = this.submitChunkedMessage(type, serializedContent, maxOpSize);
|
|
2356
|
-
}
|
|
2423
|
+
clientSequenceNumber = this.submitMaybeChunkedMessages(
|
|
2424
|
+
type,
|
|
2425
|
+
content,
|
|
2426
|
+
serializedContent,
|
|
2427
|
+
maxOpSize,
|
|
2428
|
+
this._flushMode === FlushMode.TurnBased,
|
|
2429
|
+
opMetadataInternal);
|
|
2357
2430
|
}
|
|
2358
2431
|
|
|
2359
2432
|
// Let the PendingStateManager know that a message was submitted.
|
|
@@ -2370,6 +2443,48 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2370
2443
|
}
|
|
2371
2444
|
}
|
|
2372
2445
|
|
|
2446
|
+
private submitMaybeChunkedMessages(
|
|
2447
|
+
type: ContainerMessageType,
|
|
2448
|
+
content: any,
|
|
2449
|
+
serializedContent: string,
|
|
2450
|
+
serverMaxOpSize: number,
|
|
2451
|
+
batch: boolean,
|
|
2452
|
+
opMetadataInternal: unknown = undefined,
|
|
2453
|
+
): number {
|
|
2454
|
+
if (this._maxOpSizeInBytes >= 0) {
|
|
2455
|
+
// Chunking disabled
|
|
2456
|
+
if (!serializedContent || serializedContent.length <= this._maxOpSizeInBytes) {
|
|
2457
|
+
return this.submitRuntimeMessage(type, content, batch, opMetadataInternal);
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
// When chunking is disabled, we ignore the server max message size
|
|
2461
|
+
// and if the content length is larger than the client configured message size
|
|
2462
|
+
// instead of splitting the content, we will fail by explicitly close the container
|
|
2463
|
+
this.closeFn(new GenericError(
|
|
2464
|
+
"OpTooLarge",
|
|
2465
|
+
/* error */ undefined,
|
|
2466
|
+
{
|
|
2467
|
+
length: {
|
|
2468
|
+
value: serializedContent.length,
|
|
2469
|
+
tag: TelemetryDataTag.PackageData,
|
|
2470
|
+
},
|
|
2471
|
+
limit: {
|
|
2472
|
+
value: this._maxOpSizeInBytes,
|
|
2473
|
+
tag: TelemetryDataTag.PackageData,
|
|
2474
|
+
},
|
|
2475
|
+
}));
|
|
2476
|
+
return -1;
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
// Chunking enabled, fallback on the server's max message size
|
|
2480
|
+
// and split the content accordingly
|
|
2481
|
+
if (!serializedContent || serializedContent.length <= serverMaxOpSize) {
|
|
2482
|
+
return this.submitRuntimeMessage(type, content, batch, opMetadataInternal);
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
return this.submitChunkedMessage(type, serializedContent, serverMaxOpSize);
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2373
2488
|
private submitChunkedMessage(type: ContainerMessageType, content: string, maxOpSize: number): number {
|
|
2374
2489
|
const contentLength = content.length;
|
|
2375
2490
|
const chunkN = Math.floor((contentLength - 1) / maxOpSize) + 1;
|
|
@@ -2485,6 +2600,8 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2485
2600
|
summaryRefSeq,
|
|
2486
2601
|
async () => this.fetchSnapshotFromStorage(ackHandle, summaryLogger, {
|
|
2487
2602
|
eventName: "RefreshLatestSummaryGetSnapshot",
|
|
2603
|
+
ackHandle,
|
|
2604
|
+
summaryRefSeq,
|
|
2488
2605
|
fetchLatest: false,
|
|
2489
2606
|
}),
|
|
2490
2607
|
readAndParseBlob,
|
package/src/dataStore.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
import { ITelemetryLogger } from "@fluidframework/common-definitions";
|
|
7
7
|
import { unreachableCase } from "@fluidframework/common-utils";
|
|
8
8
|
import { AttachState } from "@fluidframework/container-definitions";
|
|
9
|
-
import {
|
|
10
|
-
import { IFluidDataStoreChannel } from "@fluidframework/runtime-definitions";
|
|
9
|
+
import { IRequest, IResponse } from "@fluidframework/core-interfaces";
|
|
10
|
+
import { AliasResult, IDataStore, IFluidDataStoreChannel } from "@fluidframework/runtime-definitions";
|
|
11
11
|
import { TelemetryDataTag } from "@fluidframework/telemetry-utils";
|
|
12
12
|
import { ContainerRuntime } from "./containerRuntime";
|
|
13
13
|
import { DataStores } from "./dataStores";
|
|
@@ -36,42 +36,6 @@ export interface IDataStoreAliasMessage {
|
|
|
36
36
|
&& typeof maybeDataStoreAliasMessage?.alias === "string";
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
-
/**
|
|
40
|
-
* Encapsulates the return codes of the aliasing API
|
|
41
|
-
*/
|
|
42
|
-
export enum AliasResult {
|
|
43
|
-
/**
|
|
44
|
-
* The datastore has been successfully aliased
|
|
45
|
-
*/
|
|
46
|
-
Success = "Success",
|
|
47
|
-
/**
|
|
48
|
-
* There is already a datastore bound to the provided alias
|
|
49
|
-
*/
|
|
50
|
-
Conflict = "Conflict",
|
|
51
|
-
/**
|
|
52
|
-
* The datastore is currently in the process of being aliased
|
|
53
|
-
*/
|
|
54
|
-
Aliasing = "Aliasing",
|
|
55
|
-
/**
|
|
56
|
-
* The datastore has been attempted to be aliased before
|
|
57
|
-
*/
|
|
58
|
-
AlreadyAliased = "AlreadyAliased",
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* A fluid router with the capability of being assigned an alias
|
|
63
|
-
*/
|
|
64
|
-
export interface IDataStore extends IFluidRouter {
|
|
65
|
-
/**
|
|
66
|
-
* Attempt to assign an alias to the datastore.
|
|
67
|
-
* If the operation succeeds, the datastore can be referenced
|
|
68
|
-
* by the supplied alias.
|
|
69
|
-
*
|
|
70
|
-
* @param alias - Given alias for this datastore.
|
|
71
|
-
*/
|
|
72
|
-
trySetAlias(alias: string): Promise<AliasResult>;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
39
|
export const channelToDataStore = (
|
|
76
40
|
fluidDataStoreChannel: IFluidDataStoreChannel,
|
|
77
41
|
internalId: string,
|
|
@@ -94,11 +58,11 @@ class DataStore implements IDataStore {
|
|
|
94
58
|
switch (this.aliasState) {
|
|
95
59
|
// If we're already aliasing, throw an exception
|
|
96
60
|
case AliasState.Aliasing:
|
|
97
|
-
return
|
|
61
|
+
return "Aliasing";
|
|
98
62
|
// If this datastore is already aliased, return true only if this
|
|
99
63
|
// is a repeated call for the same alias
|
|
100
64
|
case AliasState.Aliased:
|
|
101
|
-
return this.alias === alias ?
|
|
65
|
+
return this.alias === alias ? "Success" : "AlreadyAliased";
|
|
102
66
|
// There is no current or past alias operation for this datastore,
|
|
103
67
|
// it is safe to continue execution
|
|
104
68
|
case AliasState.None: break;
|
|
@@ -118,7 +82,7 @@ class DataStore implements IDataStore {
|
|
|
118
82
|
// Explicitly Lock-out future attempts of aliasing,
|
|
119
83
|
// regardless of result
|
|
120
84
|
this.aliasState = AliasState.Aliased;
|
|
121
|
-
return localResult ?
|
|
85
|
+
return localResult ? "Success" : "Conflict";
|
|
122
86
|
}
|
|
123
87
|
|
|
124
88
|
const aliased = await this.ackBasedPromise<boolean>((resolve) => {
|
|
@@ -148,7 +112,7 @@ class DataStore implements IDataStore {
|
|
|
148
112
|
return false;
|
|
149
113
|
});
|
|
150
114
|
|
|
151
|
-
return aliased ?
|
|
115
|
+
return aliased ? "Success" : "Conflict";
|
|
152
116
|
}
|
|
153
117
|
|
|
154
118
|
async request(request: IRequest): Promise<IResponse> {
|
package/src/dataStoreContext.ts
CHANGED
|
@@ -66,7 +66,7 @@ import {
|
|
|
66
66
|
TelemetryDataTag,
|
|
67
67
|
ThresholdCounter,
|
|
68
68
|
} from "@fluidframework/telemetry-utils";
|
|
69
|
-
import {
|
|
69
|
+
import { DataProcessingError } from "@fluidframework/container-utils";
|
|
70
70
|
|
|
71
71
|
import { ContainerRuntime } from "./containerRuntime";
|
|
72
72
|
import {
|
|
@@ -106,11 +106,6 @@ export function createAttributesBlob(
|
|
|
106
106
|
|
|
107
107
|
interface ISnapshotDetails {
|
|
108
108
|
pkg: readonly string[];
|
|
109
|
-
/**
|
|
110
|
-
* This tells whether a data store is root. Root data stores are never collected.
|
|
111
|
-
* Non-root data stores may be collected if they are not used.
|
|
112
|
-
*/
|
|
113
|
-
isRootDataStore: boolean;
|
|
114
109
|
snapshot?: ISnapshotTree;
|
|
115
110
|
}
|
|
116
111
|
|
|
@@ -212,7 +207,10 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
|
|
|
212
207
|
}
|
|
213
208
|
|
|
214
209
|
public async isRoot(): Promise<boolean> {
|
|
215
|
-
|
|
210
|
+
// This call updates this.isRootDataStore if it has not yet been updated
|
|
211
|
+
// The initial value is stored in the initial snapshot of the data store
|
|
212
|
+
await this.getInitialSnapshotDetails();
|
|
213
|
+
return this.isRootDataStore;
|
|
216
214
|
}
|
|
217
215
|
|
|
218
216
|
protected registry: IFluidDataStoreRegistry | undefined;
|
|
@@ -225,6 +223,7 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
|
|
|
225
223
|
protected channelDeferred: Deferred<IFluidDataStoreChannel> | undefined;
|
|
226
224
|
private _baseSnapshot: ISnapshotTree | undefined;
|
|
227
225
|
protected _attachState: AttachState;
|
|
226
|
+
protected isRootDataStore: boolean = false;
|
|
228
227
|
protected readonly summarizerNode: ISummarizerNodeWithGC;
|
|
229
228
|
private readonly subLogger: ITelemetryLogger;
|
|
230
229
|
private readonly thresholdOpsCounter: ThresholdCounter;
|
|
@@ -311,7 +310,7 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
|
|
|
311
310
|
if (!this.channelDeferred) {
|
|
312
311
|
this.channelDeferred = new Deferred<IFluidDataStoreChannel>();
|
|
313
312
|
this.realizeCore(this.existing).catch((error) => {
|
|
314
|
-
const errorWrapped =
|
|
313
|
+
const errorWrapped = DataProcessingError.wrapIfUnrecognized(error, "realizeFluidDataStoreContext");
|
|
315
314
|
errorWrapped.addTelemetryProperties({ fluidDataStoreId: { value: this.id, tag: "PackageData"} });
|
|
316
315
|
this.channelDeferred?.reject(errorWrapped);
|
|
317
316
|
this.logger.sendErrorEvent({ eventName: "RealizeError"}, errorWrapped);
|
|
@@ -450,8 +449,8 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
|
|
|
450
449
|
}
|
|
451
450
|
|
|
452
451
|
// Add data store's attributes to the summary.
|
|
453
|
-
const { pkg
|
|
454
|
-
const attributes = createAttributes(pkg, isRootDataStore, this.disableIsolatedChannels);
|
|
452
|
+
const { pkg } = await this.getInitialSnapshotDetails();
|
|
453
|
+
const attributes = createAttributes(pkg, this.isRootDataStore, this.disableIsolatedChannels);
|
|
455
454
|
addBlobToSummary(summarizeResult, dataStoreAttributesBlobName, JSON.stringify(attributes));
|
|
456
455
|
|
|
457
456
|
// Add GC data to the summary if it's not written at the root.
|
|
@@ -729,7 +728,6 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
|
|
|
729
728
|
}
|
|
730
729
|
|
|
731
730
|
export class RemoteFluidDataStoreContext extends FluidDataStoreContext {
|
|
732
|
-
private isRootDataStore: boolean | undefined;
|
|
733
731
|
private readonly initSnapshotValue: ISnapshotTree | string | undefined;
|
|
734
732
|
private readonly baseGCDetailsP: Promise<IGarbageCollectionDetailsBase>;
|
|
735
733
|
|
|
@@ -804,11 +802,12 @@ export class RemoteFluidDataStoreContext extends FluidDataStoreContext {
|
|
|
804
802
|
}
|
|
805
803
|
}
|
|
806
804
|
|
|
805
|
+
this.isRootDataStore = isRootDataStore;
|
|
806
|
+
|
|
807
807
|
return {
|
|
808
808
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
809
809
|
pkg: this.pkg!,
|
|
810
810
|
snapshot: tree,
|
|
811
|
-
isRootDataStore,
|
|
812
811
|
};
|
|
813
812
|
});
|
|
814
813
|
|
|
@@ -846,7 +845,10 @@ export class RemoteFluidDataStoreContext extends FluidDataStoreContext {
|
|
|
846
845
|
*/
|
|
847
846
|
export class LocalFluidDataStoreContextBase extends FluidDataStoreContext {
|
|
848
847
|
private readonly snapshotTree: ISnapshotTree | undefined;
|
|
849
|
-
|
|
848
|
+
/**
|
|
849
|
+
* @deprecated 0.16 Issue #1635, #3631
|
|
850
|
+
*/
|
|
851
|
+
public readonly createProps?: any;
|
|
850
852
|
|
|
851
853
|
constructor(props: ILocalFluidDataStoreContextProps) {
|
|
852
854
|
super(
|
|
@@ -858,7 +860,8 @@ export class LocalFluidDataStoreContextBase extends FluidDataStoreContext {
|
|
|
858
860
|
);
|
|
859
861
|
|
|
860
862
|
this.snapshotTree = props.snapshotTree;
|
|
861
|
-
this.isRootDataStore = props.isRootDataStore;
|
|
863
|
+
this.isRootDataStore = props.isRootDataStore ?? false;
|
|
864
|
+
this.createProps = props.createProps;
|
|
862
865
|
this.attachListeners();
|
|
863
866
|
}
|
|
864
867
|
|
|
@@ -933,7 +936,6 @@ export class LocalFluidDataStoreContextBase extends FluidDataStoreContext {
|
|
|
933
936
|
return {
|
|
934
937
|
pkg: this.pkg,
|
|
935
938
|
snapshot,
|
|
936
|
-
isRootDataStore: this.isRootDataStore,
|
|
937
939
|
};
|
|
938
940
|
}
|
|
939
941
|
|