@fluidframework/container-runtime 2.0.0-internal.2.3.0 → 2.0.0-internal.2.4.0
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/blobManager.d.ts +4 -2
- package/dist/blobManager.d.ts.map +1 -1
- package/dist/blobManager.js +43 -10
- package/dist/blobManager.js.map +1 -1
- package/dist/containerRuntime.d.ts +15 -0
- package/dist/containerRuntime.d.ts.map +1 -1
- package/dist/containerRuntime.js +28 -13
- package/dist/containerRuntime.js.map +1 -1
- package/dist/dataStoreContext.d.ts.map +1 -1
- package/dist/dataStoreContext.js +5 -8
- package/dist/dataStoreContext.js.map +1 -1
- package/dist/dataStores.d.ts +3 -3
- package/dist/dataStores.d.ts.map +1 -1
- package/dist/dataStores.js +14 -14
- package/dist/dataStores.js.map +1 -1
- package/dist/garbageCollection.d.ts +15 -7
- package/dist/garbageCollection.d.ts.map +1 -1
- package/dist/garbageCollection.js +101 -40
- package/dist/garbageCollection.js.map +1 -1
- package/dist/garbageCollectionConstants.d.ts +1 -0
- package/dist/garbageCollectionConstants.d.ts.map +1 -1
- package/dist/garbageCollectionConstants.js +4 -2
- package/dist/garbageCollectionConstants.js.map +1 -1
- package/dist/garbageCollectionTombstoneUtils.d.ts +5 -4
- package/dist/garbageCollectionTombstoneUtils.d.ts.map +1 -1
- package/dist/garbageCollectionTombstoneUtils.js +8 -13
- package/dist/garbageCollectionTombstoneUtils.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- 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/summarizer.js.map +1 -1
- package/lib/blobManager.d.ts +4 -2
- package/lib/blobManager.d.ts.map +1 -1
- package/lib/blobManager.js +44 -11
- package/lib/blobManager.js.map +1 -1
- package/lib/containerRuntime.d.ts +15 -0
- package/lib/containerRuntime.d.ts.map +1 -1
- package/lib/containerRuntime.js +27 -12
- package/lib/containerRuntime.js.map +1 -1
- package/lib/dataStoreContext.d.ts.map +1 -1
- package/lib/dataStoreContext.js +5 -8
- package/lib/dataStoreContext.js.map +1 -1
- package/lib/dataStores.d.ts +3 -3
- package/lib/dataStores.d.ts.map +1 -1
- package/lib/dataStores.js +15 -15
- package/lib/dataStores.js.map +1 -1
- package/lib/garbageCollection.d.ts +15 -7
- package/lib/garbageCollection.d.ts.map +1 -1
- package/lib/garbageCollection.js +103 -42
- package/lib/garbageCollection.js.map +1 -1
- package/lib/garbageCollectionConstants.d.ts +1 -0
- package/lib/garbageCollectionConstants.d.ts.map +1 -1
- package/lib/garbageCollectionConstants.js +3 -1
- package/lib/garbageCollectionConstants.js.map +1 -1
- package/lib/garbageCollectionTombstoneUtils.d.ts +5 -4
- package/lib/garbageCollectionTombstoneUtils.d.ts.map +1 -1
- package/lib/garbageCollectionTombstoneUtils.js +9 -14
- package/lib/garbageCollectionTombstoneUtils.js.map +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -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/summarizer.js.map +1 -1
- package/package.json +16 -16
- package/src/blobManager.ts +57 -13
- package/src/containerRuntime.ts +38 -9
- package/src/dataStoreContext.ts +13 -14
- package/src/dataStores.ts +25 -19
- package/src/garbageCollection.ts +115 -42
- package/src/garbageCollectionConstants.ts +3 -1
- package/src/garbageCollectionTombstoneUtils.ts +11 -14
- package/src/index.ts +2 -0
- package/src/packageVersion.ts +1 -1
- package/src/summarizer.ts +1 -1
package/src/containerRuntime.ts
CHANGED
|
@@ -524,6 +524,30 @@ export enum RuntimeHeaders {
|
|
|
524
524
|
viaHandle = "viaHandle",
|
|
525
525
|
}
|
|
526
526
|
|
|
527
|
+
/** True if a tombstoned object should be returned without erroring */
|
|
528
|
+
export const AllowTombstoneRequestHeaderKey = "allowTombstone"; // Belongs in the enum above, but avoiding the breaking change
|
|
529
|
+
|
|
530
|
+
/** Tombstone error responses will have this header set to true */
|
|
531
|
+
export const TombstoneResponseHeaderKey = "isTombstoned"
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* The full set of parsed header data that may be found on Runtime requests
|
|
535
|
+
*/
|
|
536
|
+
export interface RuntimeHeaderData {
|
|
537
|
+
wait?: boolean;
|
|
538
|
+
externalRequest?: boolean;
|
|
539
|
+
viaHandle?: boolean;
|
|
540
|
+
allowTombstone?: boolean;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/** Default values for Runtime Headers */
|
|
544
|
+
export const defaultRuntimeHeaderData: Required<RuntimeHeaderData> = {
|
|
545
|
+
wait: true,
|
|
546
|
+
externalRequest: false,
|
|
547
|
+
viaHandle: false,
|
|
548
|
+
allowTombstone: false,
|
|
549
|
+
}
|
|
550
|
+
|
|
527
551
|
/**
|
|
528
552
|
* Available compression algorithms for op compression.
|
|
529
553
|
*/
|
|
@@ -738,7 +762,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
738
762
|
if (loadSequenceNumberVerification === "log") {
|
|
739
763
|
logger.sendErrorEvent({ eventName: "SequenceNumberMismatch" }, error);
|
|
740
764
|
} else {
|
|
741
|
-
// Call both close and dispose as
|
|
765
|
+
// Call both close and dispose as closeFn implementation will no longer dispose runtime in future
|
|
742
766
|
context.closeFn(error);
|
|
743
767
|
context.disposeFn?.(error);
|
|
744
768
|
}
|
|
@@ -1174,6 +1198,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1174
1198
|
(fromPath: string, toPath: string) => this.garbageCollector.addedOutboundReference(fromPath, toPath),
|
|
1175
1199
|
this,
|
|
1176
1200
|
pendingRuntimeState?.pendingAttachmentBlobs,
|
|
1201
|
+
() => this.getCurrentReferenceTimestampMs(),
|
|
1177
1202
|
);
|
|
1178
1203
|
|
|
1179
1204
|
this.scheduleManager = new ScheduleManager(
|
|
@@ -1460,16 +1485,20 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1460
1485
|
}
|
|
1461
1486
|
|
|
1462
1487
|
private async getDataStoreFromRequest(id: string, request: IRequest): Promise<IFluidRouter> {
|
|
1463
|
-
const
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1488
|
+
const headerData: RuntimeHeaderData = {};
|
|
1489
|
+
if (typeof request.headers?.[RuntimeHeaders.wait] === "boolean") {
|
|
1490
|
+
headerData.wait = request.headers[RuntimeHeaders.wait];
|
|
1491
|
+
}
|
|
1492
|
+
if (typeof request.headers?.[RuntimeHeaders.viaHandle] === "boolean") {
|
|
1493
|
+
headerData.viaHandle = request.headers[RuntimeHeaders.viaHandle];
|
|
1494
|
+
}
|
|
1495
|
+
if (typeof request.headers?.[AllowTombstoneRequestHeaderKey] === "boolean") {
|
|
1496
|
+
headerData.allowTombstone = request.headers[AllowTombstoneRequestHeaderKey];
|
|
1497
|
+
}
|
|
1469
1498
|
|
|
1470
1499
|
await this.dataStores.waitIfPendingAlias(id);
|
|
1471
1500
|
const internalId = this.internalId(id);
|
|
1472
|
-
const dataStoreContext = await this.dataStores.getDataStore(internalId,
|
|
1501
|
+
const dataStoreContext = await this.dataStores.getDataStore(internalId, headerData);
|
|
1473
1502
|
|
|
1474
1503
|
/**
|
|
1475
1504
|
* If GC should run and this an external app request with "externalRequest" header, we need to return
|
|
@@ -1860,7 +1889,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1860
1889
|
private async getRootDataStoreChannel(id: string, wait = true): Promise<IFluidDataStoreChannel> {
|
|
1861
1890
|
await this.dataStores.waitIfPendingAlias(id);
|
|
1862
1891
|
const internalId = this.internalId(id);
|
|
1863
|
-
const context = await this.dataStores.getDataStore(internalId, wait
|
|
1892
|
+
const context = await this.dataStores.getDataStore(internalId, { wait });
|
|
1864
1893
|
assert(await context.isRoot(), 0x12b /* "did not get root data store" */);
|
|
1865
1894
|
return context.realize();
|
|
1866
1895
|
}
|
package/src/dataStoreContext.ts
CHANGED
|
@@ -775,20 +775,19 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
|
|
|
775
775
|
|
|
776
776
|
if (checkTombstone && this.tombstoned) {
|
|
777
777
|
const messageString = `Context is tombstoned! Call site [${callSite}]`;
|
|
778
|
-
const error = new DataCorruptionError(messageString,
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
// throwOnTombstoneUsage is set and the client is not a summarizer.
|
|
778
|
+
const error = new DataCorruptionError(messageString, safeTelemetryProps);
|
|
779
|
+
|
|
780
|
+
sendGCTombstoneEvent(
|
|
781
|
+
this.mc,
|
|
782
|
+
{
|
|
783
|
+
eventName: "GC_Tombstone_DataStore_Changed",
|
|
784
|
+
category: this.throwOnTombstoneUsage ? "error" : "generic",
|
|
785
|
+
isSummarizerClient: this.clientDetails.type === summarizerClientType,
|
|
786
|
+
callSite,
|
|
787
|
+
},
|
|
788
|
+
this.pkg,
|
|
789
|
+
error,
|
|
790
|
+
);
|
|
792
791
|
if (this.throwOnTombstoneUsage) {
|
|
793
792
|
throw error;
|
|
794
793
|
}
|
package/src/dataStores.ts
CHANGED
|
@@ -43,7 +43,7 @@ import { assert, Lazy, LazyPromise } from "@fluidframework/common-utils";
|
|
|
43
43
|
import { v4 as uuid } from "uuid";
|
|
44
44
|
import { GCDataBuilder, unpackChildNodesGCDetails, unpackChildNodesUsedRoutes } from "@fluidframework/garbage-collector";
|
|
45
45
|
import { DataStoreContexts } from "./dataStoreContexts";
|
|
46
|
-
import { ContainerRuntime } from "./containerRuntime";
|
|
46
|
+
import { ContainerRuntime, defaultRuntimeHeaderData, RuntimeHeaderData, TombstoneResponseHeaderKey } from "./containerRuntime";
|
|
47
47
|
import {
|
|
48
48
|
FluidDataStoreContext,
|
|
49
49
|
RemoteFluidDataStoreContext,
|
|
@@ -54,7 +54,7 @@ import {
|
|
|
54
54
|
import { IContainerRuntimeMetadata, nonDataStorePaths, rootHasIsolatedChannels } from "./summaryFormat";
|
|
55
55
|
import { IDataStoreAliasMessage, isDataStoreAliasMessage } from "./dataStore";
|
|
56
56
|
import { GCNodeType } from "./garbageCollection";
|
|
57
|
-
import {
|
|
57
|
+
import { throwOnTombstoneLoadKey } from "./garbageCollectionConstants";
|
|
58
58
|
import { summarizerClientType } from "./summarizerClientElection";
|
|
59
59
|
import { sendGCTombstoneEvent } from "./garbageCollectionTombstoneUtils";
|
|
60
60
|
|
|
@@ -85,7 +85,7 @@ export class DataStores implements IDisposable {
|
|
|
85
85
|
// root data stores that are added.
|
|
86
86
|
private dataStoresSinceLastGC: string[] = [];
|
|
87
87
|
/** If true, throw an error when a tombstone data store is retrieved. */
|
|
88
|
-
private readonly
|
|
88
|
+
private readonly throwOnTombstoneLoad: boolean;
|
|
89
89
|
// The handle to the container runtime. This is used mainly for GC purposes to represent outbound reference from
|
|
90
90
|
// the container runtime to other nodes.
|
|
91
91
|
private readonly containerRuntimeHandle: IFluidHandle;
|
|
@@ -118,8 +118,8 @@ export class DataStores implements IDisposable {
|
|
|
118
118
|
return baseGCDetails.get(dataStoreId);
|
|
119
119
|
};
|
|
120
120
|
// Tombstone should only throw when the feature flag is enabled and the client isn't a summarizer
|
|
121
|
-
this.
|
|
122
|
-
this.mc.config.getBoolean(
|
|
121
|
+
this.throwOnTombstoneLoad =
|
|
122
|
+
this.mc.config.getBoolean(throwOnTombstoneLoadKey) === true &&
|
|
123
123
|
this.runtime.clientDetails.type !== summarizerClientType;
|
|
124
124
|
|
|
125
125
|
// Extract stores stored inside the snapshot
|
|
@@ -430,8 +430,10 @@ export class DataStores implements IDisposable {
|
|
|
430
430
|
);
|
|
431
431
|
}
|
|
432
432
|
|
|
433
|
-
public async getDataStore(id: string,
|
|
434
|
-
const
|
|
433
|
+
public async getDataStore(id: string, requestHeaderData: RuntimeHeaderData): Promise<FluidDataStoreContext> {
|
|
434
|
+
const headerData = { ...defaultRuntimeHeaderData, ...requestHeaderData };
|
|
435
|
+
|
|
436
|
+
const context = await this.contexts.getBoundOrRemoted(id, headerData.wait);
|
|
435
437
|
const request = { url: id };
|
|
436
438
|
if (context === undefined) {
|
|
437
439
|
// The requested data store does not exits. Throw a 404 response exception.
|
|
@@ -439,24 +441,28 @@ export class DataStores implements IDisposable {
|
|
|
439
441
|
}
|
|
440
442
|
|
|
441
443
|
if (context.tombstoned) {
|
|
444
|
+
const shouldFail = this.throwOnTombstoneLoad && !headerData.allowTombstone;
|
|
445
|
+
|
|
442
446
|
// The requested data store is removed by gc. Create a 404 gc response exception.
|
|
443
|
-
const error = responseToException(createResponseError(
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
};
|
|
447
|
+
const error = responseToException(createResponseError(
|
|
448
|
+
404,
|
|
449
|
+
"DataStore was deleted",
|
|
450
|
+
request,
|
|
451
|
+
{ [TombstoneResponseHeaderKey]: true },
|
|
452
|
+
), request);
|
|
450
453
|
sendGCTombstoneEvent(
|
|
451
454
|
this.mc,
|
|
452
|
-
|
|
453
|
-
|
|
455
|
+
{
|
|
456
|
+
eventName: "GC_Tombstone_DataStore_Requested",
|
|
457
|
+
category: shouldFail ? "error" : "generic",
|
|
458
|
+
isSummarizerClient: this.runtime.clientDetails.type === summarizerClientType,
|
|
459
|
+
headers: JSON.stringify(requestHeaderData),
|
|
460
|
+
},
|
|
454
461
|
context.isLoaded ? context.packagePath : undefined,
|
|
455
462
|
error,
|
|
456
463
|
);
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
if (this.throwOnTombstoneUsage) {
|
|
464
|
+
|
|
465
|
+
if (shouldFail) {
|
|
460
466
|
throw error;
|
|
461
467
|
}
|
|
462
468
|
}
|
package/src/garbageCollection.ts
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
IGarbageCollectionNodeData,
|
|
31
31
|
IGarbageCollectionSummaryDetailsLegacy,
|
|
32
32
|
ISummaryTreeWithStats,
|
|
33
|
+
gcDeletedBlobKey,
|
|
33
34
|
} from "@fluidframework/runtime-definitions";
|
|
34
35
|
import {
|
|
35
36
|
mergeStats,
|
|
@@ -62,9 +63,9 @@ import {
|
|
|
62
63
|
runSessionExpiryKey,
|
|
63
64
|
runSweepKey,
|
|
64
65
|
stableGCVersion,
|
|
65
|
-
throwOnTombstoneUsageKey,
|
|
66
66
|
trackGCStateKey
|
|
67
67
|
} from "./garbageCollectionConstants";
|
|
68
|
+
import { sendGCTombstoneEvent } from "./garbageCollectionTombstoneUtils";
|
|
68
69
|
import { SweepReadyUsageDetectionHandler } from "./gcSweepReadyUsageDetection";
|
|
69
70
|
import {
|
|
70
71
|
getGCVersion,
|
|
@@ -172,6 +173,8 @@ export interface IGarbageCollector {
|
|
|
172
173
|
): void;
|
|
173
174
|
/** Called when a reference is added to a node. Used to identify nodes that were referenced between summaries. */
|
|
174
175
|
addedOutboundReference(fromNodePath: string, toNodePath: string): void;
|
|
176
|
+
/** Returns true if this node has been deleted by GC during sweep phase. */
|
|
177
|
+
isNodeDeleted(nodePath: string): boolean;
|
|
175
178
|
setConnectionState(connected: boolean, clientId?: string): void;
|
|
176
179
|
dispose(): void;
|
|
177
180
|
}
|
|
@@ -226,6 +229,7 @@ interface IUnreferencedEventProps {
|
|
|
226
229
|
interface IGCSummaryTrackingData {
|
|
227
230
|
serializedGCState: string | undefined;
|
|
228
231
|
serializedTombstones: string | undefined;
|
|
232
|
+
serializedDeletedNodes: string | undefined;
|
|
229
233
|
}
|
|
230
234
|
|
|
231
235
|
/**
|
|
@@ -419,7 +423,10 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
419
423
|
// Keeps a list of references (edges in the GC graph) between GC runs. Each entry has a node id and a list of
|
|
420
424
|
// outbound routes from that node.
|
|
421
425
|
private readonly newReferencesSinceLastRun: Map<string, string[]> = new Map();
|
|
426
|
+
// A list of nodes that have been tombstoned.
|
|
422
427
|
private tombstones: string[] = [];
|
|
428
|
+
// A list of nodes that have been deleted during sweep phase.
|
|
429
|
+
private deletedNodes: Set<string> = new Set();
|
|
423
430
|
|
|
424
431
|
/**
|
|
425
432
|
* Keeps track of the GC data from the latest summary successfully submitted to and acked from the server.
|
|
@@ -565,8 +572,7 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
565
572
|
// flag in GC options to false.
|
|
566
573
|
this.gcEnabled = this.gcOptions.gcAllowed !== false;
|
|
567
574
|
// The sweep phase has to be explicitly enabled by setting the sweepAllowed flag in GC options to true.
|
|
568
|
-
|
|
569
|
-
this.sweepEnabled = this.gcOptions.sweepAllowed === true || testOverrideSweepTimeoutMs !== undefined;
|
|
575
|
+
this.sweepEnabled = this.gcOptions.sweepAllowed === true;
|
|
570
576
|
|
|
571
577
|
// Set the Session Expiry only if the flag is enabled and GC is enabled.
|
|
572
578
|
if (this.mc.config.getBoolean(runSessionExpiryKey) && this.gcEnabled) {
|
|
@@ -640,8 +646,9 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
640
646
|
|
|
641
647
|
// Whether we are running in test mode. In this mode, unreferenced nodes are immediately deleted.
|
|
642
648
|
this.testMode = this.mc.config.getBoolean(gcTestModeKey) ?? this.gcOptions.runGCInTestMode === true;
|
|
643
|
-
// Whether we are running in tombstone mode. This is
|
|
644
|
-
|
|
649
|
+
// Whether we are running in tombstone mode. This is enabled by default if sweep won't run. It can be disabled
|
|
650
|
+
// via feature flags.
|
|
651
|
+
this.tombstoneMode = !this.shouldRunSweep && this.mc.config.getBoolean(disableTombstoneKey) !== true;
|
|
645
652
|
|
|
646
653
|
// If GC ran in the container that generated the base snapshot, it will have a GC tree.
|
|
647
654
|
this.wasGCRunInLatestSummary = baseSnapshot?.trees[gcTreeKey] !== undefined;
|
|
@@ -704,7 +711,9 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
704
711
|
}
|
|
705
712
|
// If there is only one node (root node just added above), either GC is disabled or we are loading from
|
|
706
713
|
// the first summary generated by detached container. In both cases, GC was not run - return undefined.
|
|
707
|
-
return Object.keys(gcState.gcNodes).length === 1
|
|
714
|
+
return Object.keys(gcState.gcNodes).length === 1
|
|
715
|
+
? undefined
|
|
716
|
+
: { gcState, tombstones: undefined, deletedNodes: undefined };
|
|
708
717
|
} catch (error) {
|
|
709
718
|
const dpe = DataProcessingError.wrapIfUnrecognized(
|
|
710
719
|
error,
|
|
@@ -782,24 +791,33 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
782
791
|
}
|
|
783
792
|
|
|
784
793
|
/**
|
|
785
|
-
* Called during container initialization. Initialize the tombstone state
|
|
786
|
-
*
|
|
787
|
-
* in use or not.
|
|
794
|
+
* Called during container initialization. Initialize from the tombstone state in the base snapshot. This is done
|
|
795
|
+
* during initialization so that deleted or tombstoned objects are marked as such before they are loaded or used.
|
|
788
796
|
*/
|
|
789
797
|
public async initializeBaseState(): Promise<void> {
|
|
790
798
|
const baseSnapshotData = await this.baseSnapshotDataP;
|
|
791
799
|
/**
|
|
792
|
-
* The base snapshot data
|
|
800
|
+
* The base snapshot data will not be present if the container is loaded from:
|
|
793
801
|
* 1. The first summary created by the detached container.
|
|
794
802
|
* 2. A summary that was generated with GC disabled.
|
|
795
803
|
* 3. A summary that was generated before GC even existed.
|
|
796
|
-
* 4. A summary that was generated with tombstone feature disabled.
|
|
797
804
|
*/
|
|
798
|
-
if (
|
|
805
|
+
if (baseSnapshotData === undefined) {
|
|
799
806
|
return;
|
|
800
807
|
}
|
|
801
|
-
|
|
802
|
-
|
|
808
|
+
|
|
809
|
+
// Initialize the deleted nodes from the snapshot. This is done irrespective of whether sweep is enabled or not
|
|
810
|
+
// to identify deleted nodes' usage.
|
|
811
|
+
if (baseSnapshotData.deletedNodes !== undefined) {
|
|
812
|
+
this.deletedNodes = new Set(baseSnapshotData.deletedNodes);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// If running in tombstone mode, initialize the tombstone state from the snapshot. Also, notify the runtime of
|
|
816
|
+
// tombstone routes.
|
|
817
|
+
if (this.tombstoneMode && baseSnapshotData.tombstones !== undefined) {
|
|
818
|
+
this.tombstones = Array.from(baseSnapshotData.tombstones);
|
|
819
|
+
this.runtime.updateTombstonedRoutes(this.tombstones);
|
|
820
|
+
}
|
|
803
821
|
}
|
|
804
822
|
|
|
805
823
|
/**
|
|
@@ -831,9 +849,29 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
831
849
|
};
|
|
832
850
|
this.unreferencedNodesState.clear();
|
|
833
851
|
|
|
834
|
-
// If
|
|
835
|
-
//
|
|
836
|
-
|
|
852
|
+
// If running sweep, the tombstone state represents the list of nodes that have been deleted during sweep.
|
|
853
|
+
// If running in tombstone mode, the tombstone state represents the list of nodes that have been marked as
|
|
854
|
+
// tombstones.
|
|
855
|
+
// If this call is because we are refreshing from a snapshot due to an ack, it is likely that the GC state
|
|
856
|
+
// in the snapshot is newer than this client's. And so, the deleted / tombstone nodes need to be updated.
|
|
857
|
+
if (this.shouldRunSweep) {
|
|
858
|
+
const snapshotDeletedNodes = snapshotData?.tombstones ? new Set(snapshotData.tombstones) : undefined;
|
|
859
|
+
// If the snapshot contains deleted nodes that are not yet deleted by this client, ask the runtime to
|
|
860
|
+
// delete them.
|
|
861
|
+
if (snapshotDeletedNodes !== undefined) {
|
|
862
|
+
const newDeletedNodes: string[] = [];
|
|
863
|
+
for (const nodeId of snapshotDeletedNodes) {
|
|
864
|
+
if (!this.deletedNodes.has(nodeId)) {
|
|
865
|
+
newDeletedNodes.push(nodeId);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
if (newDeletedNodes.length > 0) {
|
|
869
|
+
// Call container runtime to delete these nodes and add deleted nodes to this.deletedNodes.
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
} else if (this.tombstoneMode) {
|
|
873
|
+
// The snapshot may contain more or fewer tombstone nodes than this client. Update tombstone state and
|
|
874
|
+
// notify the runtime to update its state as well.
|
|
837
875
|
this.tombstones = snapshotData?.tombstones ? Array.from(snapshotData.tombstones) : [];
|
|
838
876
|
this.runtime.updateTombstonedRoutes(this.tombstones);
|
|
839
877
|
}
|
|
@@ -869,6 +907,7 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
869
907
|
this.latestSummaryData = {
|
|
870
908
|
serializedGCState: JSON.stringify(generateSortedGCState(snapshotData.gcState)),
|
|
871
909
|
serializedTombstones: JSON.stringify(snapshotData.tombstones),
|
|
910
|
+
serializedDeletedNodes: JSON.stringify(snapshotData.deletedNodes),
|
|
872
911
|
};
|
|
873
912
|
}
|
|
874
913
|
}
|
|
@@ -1020,18 +1059,26 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
1020
1059
|
}
|
|
1021
1060
|
|
|
1022
1061
|
const serializedGCState = JSON.stringify(generateSortedGCState(gcState));
|
|
1062
|
+
// Serialize and write deleted nodes, if any. This is done irrespective of whether sweep is enabled or not so
|
|
1063
|
+
// to identify deleted nodes' usage.
|
|
1064
|
+
const serializedDeletedNodes = this.deletedNodes.size > 0
|
|
1065
|
+
? JSON.stringify(Array.from(this.deletedNodes).sort())
|
|
1066
|
+
: undefined;
|
|
1067
|
+
// If running in tombstone mode, serialize and write tombstones, if any.
|
|
1023
1068
|
const serializedTombstones = this.tombstoneMode
|
|
1024
1069
|
? (this.tombstones.length > 0 ? JSON.stringify(this.tombstones.sort()) : undefined)
|
|
1025
1070
|
: undefined;
|
|
1026
1071
|
|
|
1027
1072
|
/**
|
|
1028
|
-
* Incremental summary of GC data - If
|
|
1029
|
-
*
|
|
1073
|
+
* Incremental summary of GC data - If none of GC state, deleted nodes or tombstones changed since last summary,
|
|
1074
|
+
* write summary handle instead of summary tree for GC.
|
|
1075
|
+
* Otherwise, write the GC summary tree. In the tree, for each of these that changed, write a summary blob and
|
|
1076
|
+
* for each of these that did not change, write a summary handle.
|
|
1030
1077
|
*/
|
|
1031
1078
|
if (this.trackGCState) {
|
|
1032
|
-
this.pendingSummaryData = { serializedGCState, serializedTombstones };
|
|
1079
|
+
this.pendingSummaryData = { serializedGCState, serializedTombstones, serializedDeletedNodes };
|
|
1033
1080
|
if (trackState && !fullTree && this.latestSummaryData !== undefined) {
|
|
1034
|
-
// If
|
|
1081
|
+
// If nothing changed since last summary, send a summary handle for the entire GC data.
|
|
1035
1082
|
if (this.latestSummaryData.serializedGCState === serializedGCState
|
|
1036
1083
|
&& this.latestSummaryData.serializedTombstones === serializedTombstones) {
|
|
1037
1084
|
const stats = mergeStats();
|
|
@@ -1046,26 +1093,30 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
1046
1093
|
};
|
|
1047
1094
|
}
|
|
1048
1095
|
|
|
1049
|
-
// If
|
|
1050
|
-
return this.buildGCSummaryTree(
|
|
1096
|
+
// If some state changed, build a GC summary tree.
|
|
1097
|
+
return this.buildGCSummaryTree(
|
|
1098
|
+
serializedGCState, serializedTombstones, serializedDeletedNodes, true /* trackState */);
|
|
1051
1099
|
}
|
|
1052
1100
|
}
|
|
1053
1101
|
// If not tracking GC state, build a GC summary tree without any summary handles.
|
|
1054
|
-
return this.buildGCSummaryTree(
|
|
1102
|
+
return this.buildGCSummaryTree(
|
|
1103
|
+
serializedGCState, serializedTombstones, serializedDeletedNodes, false /* trackState */);
|
|
1055
1104
|
}
|
|
1056
1105
|
|
|
1057
1106
|
/**
|
|
1058
|
-
* Builds the GC summary tree which contains GC state and
|
|
1059
|
-
* If trackState is false,
|
|
1060
|
-
* If trackState is true,
|
|
1107
|
+
* Builds the GC summary tree which contains GC state, deleted nodes and tombstones.
|
|
1108
|
+
* If trackState is false, all of GC state, deleted nodes and tombstones are written as summary blobs.
|
|
1109
|
+
* If trackState is true, only states that changed are written. Rest are written as handles.
|
|
1061
1110
|
* @param serializedGCState - The GC state serialized as string.
|
|
1062
|
-
* @param serializedTombstones -
|
|
1111
|
+
* @param serializedTombstones - The tombstone state serialized as string.
|
|
1112
|
+
* @param serializedDeletedNodes - Deleted nodes serialized as string.
|
|
1063
1113
|
* @param trackState - Whether we are tracking GC state across summaries.
|
|
1064
1114
|
* @returns the GC summary tree.
|
|
1065
1115
|
*/
|
|
1066
1116
|
private buildGCSummaryTree(
|
|
1067
1117
|
serializedGCState: string,
|
|
1068
1118
|
serializedTombstones: string | undefined,
|
|
1119
|
+
serializedDeletedNodes: string | undefined,
|
|
1069
1120
|
trackState: boolean,
|
|
1070
1121
|
): ISummaryTreeWithStats {
|
|
1071
1122
|
const gcStateBlobKey = `${gcBlobPrefix}_root`;
|
|
@@ -1078,16 +1129,26 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
1078
1129
|
builder.addBlob(gcStateBlobKey, serializedGCState);
|
|
1079
1130
|
}
|
|
1080
1131
|
|
|
1081
|
-
// If
|
|
1082
|
-
|
|
1132
|
+
// If tombstones exist, write a summary handle if it hasn't changed. If it has changed, write a
|
|
1133
|
+
// summary blob.
|
|
1134
|
+
if (serializedTombstones !== undefined) {
|
|
1135
|
+
if (this.latestSummaryData?.serializedTombstones === serializedTombstones && trackState) {
|
|
1136
|
+
builder.addHandle(gcTombstoneBlobKey, SummaryType.Blob, `/${gcTreeKey}/${gcTombstoneBlobKey}`);
|
|
1137
|
+
} else {
|
|
1138
|
+
builder.addBlob(gcTombstoneBlobKey, serializedTombstones);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// If there are no deleted nodes, return the summary tree.
|
|
1143
|
+
if (serializedDeletedNodes === undefined) {
|
|
1083
1144
|
return builder.getSummaryTree();
|
|
1084
1145
|
}
|
|
1085
1146
|
|
|
1086
|
-
// If the
|
|
1087
|
-
if (this.latestSummaryData?.
|
|
1088
|
-
builder.addHandle(
|
|
1147
|
+
// If the deleted nodes hasn't changed, write a summary handle, else write a summary blob for it.
|
|
1148
|
+
if (this.latestSummaryData?.serializedDeletedNodes === serializedDeletedNodes && trackState) {
|
|
1149
|
+
builder.addHandle(gcDeletedBlobKey, SummaryType.Blob, `/${gcTreeKey}/${gcDeletedBlobKey}`);
|
|
1089
1150
|
} else {
|
|
1090
|
-
builder.addBlob(
|
|
1151
|
+
builder.addBlob(gcDeletedBlobKey, serializedDeletedNodes);
|
|
1091
1152
|
}
|
|
1092
1153
|
return builder.getSummaryTree();
|
|
1093
1154
|
}
|
|
@@ -1243,16 +1304,28 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
1243
1304
|
eventName = "GC_Tombstone_Blob_Revived";
|
|
1244
1305
|
}
|
|
1245
1306
|
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1307
|
+
sendGCTombstoneEvent(
|
|
1308
|
+
this.mc,
|
|
1309
|
+
{
|
|
1310
|
+
eventName,
|
|
1311
|
+
category: "generic",
|
|
1312
|
+
isSummarizerClient: this.isSummarizerClient,
|
|
1313
|
+
url: trimLeadingSlashes(toNodePath),
|
|
1314
|
+
nodeType,
|
|
1315
|
+
},
|
|
1316
|
+
undefined /* packagePath */,
|
|
1317
|
+
);
|
|
1253
1318
|
}
|
|
1254
1319
|
}
|
|
1255
1320
|
|
|
1321
|
+
/**
|
|
1322
|
+
* Returns whether a node with the given path has been deleted or not. This can be used by the runtime to identify
|
|
1323
|
+
* cases where objects are used after they are deleted and throw / log errors accordingly.
|
|
1324
|
+
*/
|
|
1325
|
+
public isNodeDeleted(nodePath: string): boolean {
|
|
1326
|
+
return this.deletedNodes.has(nodePath);
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1256
1329
|
public dispose(): void {
|
|
1257
1330
|
this.sessionExpiryTimer?.clear();
|
|
1258
1331
|
this.sessionExpiryTimer = undefined;
|
|
@@ -1636,7 +1709,7 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
1636
1709
|
}
|
|
1637
1710
|
}
|
|
1638
1711
|
|
|
1639
|
-
// If SweepReady Usage Detection is
|
|
1712
|
+
// If SweepReady Usage Detection is enabled, the handler may close the interactive container.
|
|
1640
1713
|
// Once Sweep is fully implemented, this will be removed since the objects will be gone
|
|
1641
1714
|
// and errors will arise elsewhere in the runtime
|
|
1642
1715
|
if (state === UnreferencedState.SweepReady) {
|
|
@@ -24,7 +24,9 @@ export const trackGCStateKey = "Fluid.GarbageCollection.TrackGCState";
|
|
|
24
24
|
export const disableSweepLogKey = "Fluid.GarbageCollection.DisableSweepLog";
|
|
25
25
|
// Feature gate key to disable the tombstone feature, i.e., tombstone information is not read / written into summary.
|
|
26
26
|
export const disableTombstoneKey = "Fluid.GarbageCollection.DisableTombstone";
|
|
27
|
-
// Feature gate to enable throwing an error when tombstone object is
|
|
27
|
+
// Feature gate to enable throwing an error when tombstone object is loaded (requested).
|
|
28
|
+
export const throwOnTombstoneLoadKey = "Fluid.GarbageCollection.ThrowOnTombstoneLoad";
|
|
29
|
+
// Feature gate to enable throwing an error when tombstone object is used (e.g. outgoing or incoming ops).
|
|
28
30
|
export const throwOnTombstoneUsageKey = "Fluid.GarbageCollection.ThrowOnTombstoneUsage";
|
|
29
31
|
// Feature gate to enable GC version upgrade.
|
|
30
32
|
export const gcVersionUpgradeToV2Key = "Fluid.GarbageCollection.GCVersionUpgradeToV2";
|
|
@@ -6,26 +6,23 @@
|
|
|
6
6
|
import { ITelemetryGenericEvent } from "@fluidframework/common-definitions";
|
|
7
7
|
import { packagePathToTelemetryProperty } from "@fluidframework/runtime-utils";
|
|
8
8
|
import { MonitoringContext } from "@fluidframework/telemetry-utils";
|
|
9
|
-
import { throwOnTombstoneUsageKey } from "./garbageCollectionConstants";
|
|
9
|
+
import { disableTombstoneKey, throwOnTombstoneLoadKey, throwOnTombstoneUsageKey } from "./garbageCollectionConstants";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* Adds isSummarizerClient, packagePath, and error to telemetry properties.
|
|
12
|
+
* Consolidates info / logic for logging when we encounter a Tombstone
|
|
15
13
|
*/
|
|
16
14
|
export function sendGCTombstoneEvent(
|
|
17
15
|
mc: MonitoringContext,
|
|
18
|
-
event: ITelemetryGenericEvent,
|
|
19
|
-
isSummarizerClient: boolean,
|
|
16
|
+
event: ITelemetryGenericEvent & { category: "error" | "generic", isSummarizerClient: boolean },
|
|
20
17
|
packagePath: readonly string[] | undefined,
|
|
21
|
-
error?:
|
|
18
|
+
error?: unknown,
|
|
22
19
|
) {
|
|
23
|
-
const throwOnTombstoneUsage = event.throwOnTombstoneUsage = mc.config.getBoolean(throwOnTombstoneUsageKey) ?? false;
|
|
24
20
|
event.pkg = packagePathToTelemetryProperty(packagePath);
|
|
25
|
-
event.
|
|
26
|
-
|
|
27
|
-
mc.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
21
|
+
event.tombstoneFlags = JSON.stringify({
|
|
22
|
+
DisableTombstone: mc.config.getBoolean(disableTombstoneKey),
|
|
23
|
+
ThrowOnTombstoneUsage: mc.config.getBoolean(throwOnTombstoneUsageKey),
|
|
24
|
+
ThrowOnTombstoneLoad: mc.config.getBoolean(throwOnTombstoneLoadKey),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
mc.logger.sendTelemetryEvent(event, error);
|
|
31
28
|
}
|
package/src/index.ts
CHANGED
package/src/packageVersion.ts
CHANGED
package/src/summarizer.ts
CHANGED
|
@@ -160,7 +160,7 @@ export class Summarizer extends EventEmitter implements ISummarizer {
|
|
|
160
160
|
// This will result in "summarizerClientDisconnected" stop reason recorded in telemetry,
|
|
161
161
|
// unless stop() was called earlier
|
|
162
162
|
this.dispose();
|
|
163
|
-
(this.runtime.disposeFn ?? this.runtime.closeFn)()
|
|
163
|
+
(this.runtime.disposeFn ?? this.runtime.closeFn)();
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
private async runCore(onBehalfOf: string): Promise<SummarizerStopReason> {
|