@fluidframework/container-runtime 2.0.0-internal.2.0.2 → 2.0.0-internal.2.1.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/containerRuntime.d.ts +2 -1
- package/dist/containerRuntime.d.ts.map +1 -1
- package/dist/containerRuntime.js +63 -23
- package/dist/containerRuntime.js.map +1 -1
- package/dist/dataStore.d.ts.map +1 -1
- package/dist/dataStore.js +6 -0
- package/dist/dataStore.js.map +1 -1
- package/dist/dataStoreContext.d.ts +7 -0
- package/dist/dataStoreContext.d.ts.map +1 -1
- package/dist/dataStoreContext.js +34 -8
- package/dist/dataStoreContext.js.map +1 -1
- package/dist/dataStoreContexts.js +1 -1
- package/dist/dataStoreContexts.js.map +1 -1
- package/dist/dataStores.d.ts +3 -2
- package/dist/dataStores.d.ts.map +1 -1
- package/dist/dataStores.js +29 -3
- package/dist/dataStores.js.map +1 -1
- package/dist/garbageCollection.d.ts +19 -5
- package/dist/garbageCollection.d.ts.map +1 -1
- package/dist/garbageCollection.js +120 -37
- package/dist/garbageCollection.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -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/summarizerClientElection.js +1 -1
- package/dist/summarizerClientElection.js.map +1 -1
- package/dist/summaryGenerator.d.ts.map +1 -1
- package/dist/summaryGenerator.js +3 -2
- package/dist/summaryGenerator.js.map +1 -1
- package/garbageCollection.md +27 -22
- package/lib/containerRuntime.d.ts +2 -1
- package/lib/containerRuntime.d.ts.map +1 -1
- package/lib/containerRuntime.js +64 -24
- package/lib/containerRuntime.js.map +1 -1
- package/lib/dataStore.d.ts.map +1 -1
- package/lib/dataStore.js +6 -0
- package/lib/dataStore.js.map +1 -1
- package/lib/dataStoreContext.d.ts +7 -0
- package/lib/dataStoreContext.d.ts.map +1 -1
- package/lib/dataStoreContext.js +35 -9
- package/lib/dataStoreContext.js.map +1 -1
- package/lib/dataStoreContexts.js +1 -1
- package/lib/dataStoreContexts.js.map +1 -1
- package/lib/dataStores.d.ts +3 -2
- package/lib/dataStores.d.ts.map +1 -1
- package/lib/dataStores.js +30 -4
- package/lib/dataStores.js.map +1 -1
- package/lib/garbageCollection.d.ts +19 -5
- package/lib/garbageCollection.d.ts.map +1 -1
- package/lib/garbageCollection.js +119 -36
- package/lib/garbageCollection.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/summarizerClientElection.js +1 -1
- package/lib/summarizerClientElection.js.map +1 -1
- package/lib/summaryGenerator.d.ts.map +1 -1
- package/lib/summaryGenerator.js +3 -2
- package/lib/summaryGenerator.js.map +1 -1
- package/package.json +26 -23
- package/src/containerRuntime.ts +77 -26
- package/src/dataStore.ts +13 -1
- package/src/dataStoreContext.ts +48 -10
- package/src/dataStoreContexts.ts +1 -1
- package/src/dataStores.ts +34 -3
- package/src/garbageCollection.ts +144 -44
- package/src/index.ts +1 -1
- package/src/packageVersion.ts +1 -1
- package/src/summarizerClientElection.ts +1 -1
- package/src/summaryGenerator.ts +3 -2
package/src/dataStoreContext.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Licensed under the MIT License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { IDisposable, ITelemetryLogger } from "@fluidframework/common-definitions";
|
|
6
|
+
import { IDisposable, ITelemetryLogger, ITelemetryProperties } from "@fluidframework/common-definitions";
|
|
7
7
|
import {
|
|
8
8
|
FluidObject,
|
|
9
9
|
IRequest,
|
|
@@ -65,7 +65,11 @@ import {
|
|
|
65
65
|
TelemetryDataTag,
|
|
66
66
|
ThresholdCounter,
|
|
67
67
|
} from "@fluidframework/telemetry-utils";
|
|
68
|
-
import {
|
|
68
|
+
import {
|
|
69
|
+
DataCorruptionError,
|
|
70
|
+
DataProcessingError,
|
|
71
|
+
extractSafePropertiesFromMessage,
|
|
72
|
+
} from "@fluidframework/container-utils";
|
|
69
73
|
|
|
70
74
|
import { ContainerRuntime } from "./containerRuntime";
|
|
71
75
|
import {
|
|
@@ -190,6 +194,13 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
|
|
|
190
194
|
private _disposed = false;
|
|
191
195
|
public get disposed() { return this._disposed; }
|
|
192
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Tombstone is a temporary feature that prevents a data store from sending / receiving ops, signals and from
|
|
199
|
+
* loading.
|
|
200
|
+
*/
|
|
201
|
+
private _tombstoned = false;
|
|
202
|
+
public get tombstoned() { return this._tombstoned; }
|
|
203
|
+
|
|
193
204
|
public get attachState(): AttachState {
|
|
194
205
|
return this._attachState;
|
|
195
206
|
}
|
|
@@ -305,6 +316,14 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
|
|
|
305
316
|
}
|
|
306
317
|
}
|
|
307
318
|
|
|
319
|
+
public setTombstone(tombstone: boolean) {
|
|
320
|
+
if (this.tombstoned === tombstone) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
this._tombstoned = tombstone;
|
|
325
|
+
}
|
|
326
|
+
|
|
308
327
|
private rejectDeferredRealize(reason: string, packageName?: string): never {
|
|
309
328
|
throw new LoggingError(reason, { packageName: { value: packageName, tag: TelemetryDataTag.CodeArtifact } });
|
|
310
329
|
}
|
|
@@ -381,7 +400,8 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
|
|
|
381
400
|
* its new client ID when we are connecting or connected.
|
|
382
401
|
*/
|
|
383
402
|
public setConnectionState(connected: boolean, clientId?: string) {
|
|
384
|
-
this
|
|
403
|
+
// ConnectionState should not fail in tombstone mode as this is internally run
|
|
404
|
+
this.verifyNotClosed("setConnectionState", false /* checkTombstone */);
|
|
385
405
|
|
|
386
406
|
// Connection events are ignored if the store is not yet loaded
|
|
387
407
|
if (!this.loaded) {
|
|
@@ -395,7 +415,7 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
|
|
|
395
415
|
}
|
|
396
416
|
|
|
397
417
|
public process(messageArg: ISequencedDocumentMessage, local: boolean, localOpMetadata: unknown): void {
|
|
398
|
-
this.verifyNotClosed();
|
|
418
|
+
this.verifyNotClosed("process", true, extractSafePropertiesFromMessage(messageArg));
|
|
399
419
|
|
|
400
420
|
const innerContents = messageArg.contents as FluidDataStoreMessage;
|
|
401
421
|
const message = {
|
|
@@ -417,7 +437,7 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
|
|
|
417
437
|
}
|
|
418
438
|
|
|
419
439
|
public processSignal(message: IInboundSignalMessage, local: boolean): void {
|
|
420
|
-
this.verifyNotClosed();
|
|
440
|
+
this.verifyNotClosed("processSignal");
|
|
421
441
|
|
|
422
442
|
// Signals are ignored if the store is not yet loaded
|
|
423
443
|
if (!this.loaded) {
|
|
@@ -582,7 +602,7 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
|
|
|
582
602
|
}
|
|
583
603
|
|
|
584
604
|
public submitMessage(type: string, content: any, localOpMetadata: unknown): void {
|
|
585
|
-
this.verifyNotClosed();
|
|
605
|
+
this.verifyNotClosed("submitMessage");
|
|
586
606
|
assert(!!this.channel, 0x146 /* "Channel must exist when submitting message" */);
|
|
587
607
|
const fluidDataStoreContent: FluidDataStoreMessage = {
|
|
588
608
|
content,
|
|
@@ -604,7 +624,7 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
|
|
|
604
624
|
*
|
|
605
625
|
*/
|
|
606
626
|
public setChannelDirty(address: string): void {
|
|
607
|
-
this.verifyNotClosed();
|
|
627
|
+
this.verifyNotClosed("setChannelDirty");
|
|
608
628
|
|
|
609
629
|
// Get the latest sequence number.
|
|
610
630
|
const latestSequenceNumber = this.deltaManager.lastSequenceNumber;
|
|
@@ -619,7 +639,8 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
|
|
|
619
639
|
}
|
|
620
640
|
|
|
621
641
|
public submitSignal(type: string, content: any) {
|
|
622
|
-
this.verifyNotClosed();
|
|
642
|
+
this.verifyNotClosed("submitSignal");
|
|
643
|
+
|
|
623
644
|
assert(!!this.channel, 0x147 /* "Channel must exist on submitting signal" */);
|
|
624
645
|
return this._containerRuntime.submitDataStoreSignal(this.id, type, content);
|
|
625
646
|
}
|
|
@@ -732,9 +753,17 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter<IFluidData
|
|
|
732
753
|
return this.channel.applyStashedOp(innerContents.content);
|
|
733
754
|
}
|
|
734
755
|
|
|
735
|
-
private verifyNotClosed() {
|
|
756
|
+
private verifyNotClosed(callSite: string, checkTombstone = true, safeTelemetryProps: ITelemetryProperties = {}) {
|
|
736
757
|
if (this._disposed) {
|
|
737
|
-
throw new Error(
|
|
758
|
+
throw new Error(`Context is closed! Call site [${callSite}]`);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (checkTombstone && this.tombstoned) {
|
|
762
|
+
const messageString = `Context is tombstoned! Call site [${callSite}]`;
|
|
763
|
+
throw new DataCorruptionError(messageString, {
|
|
764
|
+
errorMessage: messageString,
|
|
765
|
+
...safeTelemetryProps,
|
|
766
|
+
});
|
|
738
767
|
}
|
|
739
768
|
}
|
|
740
769
|
|
|
@@ -991,6 +1020,15 @@ export class LocalDetachedFluidDataStoreContext
|
|
|
991
1020
|
|
|
992
1021
|
super.bindRuntime(dataStoreChannel);
|
|
993
1022
|
|
|
1023
|
+
// Load the handle to the data store's entryPoint to make sure that for a detached data store, the entryPoint
|
|
1024
|
+
// initialization function is called before the data store gets attached and potentially connected to the
|
|
1025
|
+
// delta stream, so it gets a chance to do things while the data store is still "purely local".
|
|
1026
|
+
// This preserves the behavior from before we introduced entryPoints, where the instantiateDataStore method
|
|
1027
|
+
// of data store factories tends to construct the data object (at least kick off an async method that returns
|
|
1028
|
+
// it); that code moved to the entryPoint initialization function, so we want to ensure it still executes
|
|
1029
|
+
// before the data store is attached.
|
|
1030
|
+
await dataStoreChannel.entryPoint?.get();
|
|
1031
|
+
|
|
994
1032
|
if (await this.isRoot()) {
|
|
995
1033
|
dataStoreChannel.makeVisibleAndAttachGraph();
|
|
996
1034
|
}
|
package/src/dataStoreContexts.ts
CHANGED
package/src/dataStores.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
convertSnapshotTreeToSummaryTree,
|
|
33
33
|
convertToSummaryTree,
|
|
34
34
|
create404Response,
|
|
35
|
+
createResponseError,
|
|
35
36
|
responseToException,
|
|
36
37
|
SummaryTreeBuilder,
|
|
37
38
|
} from "@fluidframework/runtime-utils";
|
|
@@ -419,14 +420,25 @@ export class DataStores implements IDisposable {
|
|
|
419
420
|
);
|
|
420
421
|
}
|
|
421
422
|
|
|
422
|
-
public async getDataStore(id: string, wait: boolean): Promise<FluidDataStoreContext> {
|
|
423
|
+
public async getDataStore(id: string, wait: boolean, viaHandle: boolean): Promise<FluidDataStoreContext> {
|
|
423
424
|
const context = await this.contexts.getBoundOrRemoted(id, wait);
|
|
425
|
+
const request = { url: id };
|
|
424
426
|
if (context === undefined) {
|
|
425
427
|
// The requested data store does not exits. Throw a 404 response exception.
|
|
426
|
-
const request = { url: id };
|
|
427
428
|
throw responseToException(create404Response(request), request);
|
|
428
429
|
}
|
|
429
430
|
|
|
431
|
+
if (context.tombstoned) {
|
|
432
|
+
// Note: if a user writes a request to look like it's viaHandle, we will also send this telemetry event
|
|
433
|
+
this.logger.sendErrorEvent({
|
|
434
|
+
eventName: "TombstonedDataStoreRequested",
|
|
435
|
+
url: request.url,
|
|
436
|
+
viaHandle,
|
|
437
|
+
});
|
|
438
|
+
// The requested data store is removed by gc. Throw a 404 gc response exception.
|
|
439
|
+
throw responseToException(createResponseError(404, "Datastore removed by gc", request), request);
|
|
440
|
+
}
|
|
441
|
+
|
|
430
442
|
return context;
|
|
431
443
|
}
|
|
432
444
|
|
|
@@ -597,6 +609,11 @@ export class DataStores implements IDisposable {
|
|
|
597
609
|
// Verify that the used routes are correct.
|
|
598
610
|
for (const [id] of usedDataStoreRoutes) {
|
|
599
611
|
assert(this.contexts.has(id), 0x167 /* "Used route does not belong to any known data store" */);
|
|
612
|
+
|
|
613
|
+
// Revive datastores regardless of whether or not tombstone the tombstone flag is flipped
|
|
614
|
+
const dataStore = this.contexts.get(id);
|
|
615
|
+
assert(dataStore !== undefined, 0x46e /* No data store retrieved with specified id */);
|
|
616
|
+
dataStore.setTombstone(false /* tombstone */);
|
|
600
617
|
}
|
|
601
618
|
|
|
602
619
|
// Update the used routes in each data store. Used routes is empty for unused data stores.
|
|
@@ -609,8 +626,9 @@ export class DataStores implements IDisposable {
|
|
|
609
626
|
* When running GC in test mode, this is called to delete objects whose routes are unused. This enables testing
|
|
610
627
|
* scenarios with accessing deleted content.
|
|
611
628
|
* @param unusedRoutes - The routes that are unused in all data stores in this Container.
|
|
629
|
+
* @param tombstone - set the objects corresponding to routes as tombstones.
|
|
612
630
|
*/
|
|
613
|
-
public deleteUnusedRoutes(unusedRoutes: string[]) {
|
|
631
|
+
public deleteUnusedRoutes(unusedRoutes: string[], tombstone: boolean = false) {
|
|
614
632
|
for (const route of unusedRoutes) {
|
|
615
633
|
const pathParts = route.split("/");
|
|
616
634
|
// Delete data store only if its route (/datastoreId) is in unusedRoutes. We don't want to delete a data
|
|
@@ -620,6 +638,19 @@ export class DataStores implements IDisposable {
|
|
|
620
638
|
}
|
|
621
639
|
const dataStoreId = pathParts[1];
|
|
622
640
|
assert(this.contexts.has(dataStoreId), 0x2d7 /* No data store with specified id */);
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* When running GC in tombstone mode, datastore contexts are tombstoned. Tombstoned datastore contexts
|
|
644
|
+
* enable testing scenarios with accessing deleted content without actually deleting content from
|
|
645
|
+
* summaries.
|
|
646
|
+
*/
|
|
647
|
+
if (tombstone) {
|
|
648
|
+
const dataStore = this.contexts.get(dataStoreId);
|
|
649
|
+
assert(dataStore !== undefined, 0x442 /* No data store retrieved with specified id */);
|
|
650
|
+
dataStore.setTombstone(true /* tombstone */);
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
|
|
623
654
|
// Delete the contexts of unused data stores.
|
|
624
655
|
this.contexts.delete(dataStoreId);
|
|
625
656
|
// Delete the summarizer node of the unused data stores.
|
package/src/garbageCollection.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
ISummarizeResult,
|
|
26
26
|
ITelemetryContext,
|
|
27
27
|
IGarbageCollectionNodeData,
|
|
28
|
+
ISummaryTreeWithStats,
|
|
28
29
|
} from "@fluidframework/runtime-definitions";
|
|
29
30
|
import {
|
|
30
31
|
mergeStats,
|
|
@@ -61,6 +62,8 @@ const GCVersion = 1;
|
|
|
61
62
|
export const gcTreeKey = "gc";
|
|
62
63
|
// They prefix for GC blobs in the GC tree in summary.
|
|
63
64
|
export const gcBlobPrefix = "__gc";
|
|
65
|
+
// The key for tombstone blob in the GC tree in summary.
|
|
66
|
+
export const gcTombstoneBlobKey = "__tombstones";
|
|
64
67
|
|
|
65
68
|
// Feature gate key to turn GC on / off.
|
|
66
69
|
export const runGCKey = "Fluid.GarbageCollection.RunGC";
|
|
@@ -74,6 +77,8 @@ export const runSessionExpiryKey = "Fluid.GarbageCollection.RunSessionExpiry";
|
|
|
74
77
|
export const trackGCStateKey = "Fluid.GarbageCollection.TrackGCState";
|
|
75
78
|
// Feature gate key to turn GC sweep log off.
|
|
76
79
|
export const disableSweepLogKey = "Fluid.GarbageCollection.DisableSweepLog";
|
|
80
|
+
// Feature gate key to tombstone datastores.
|
|
81
|
+
export const testTombstoneKey = "Fluid.GarbageCollection.Test.Tombstone";
|
|
77
82
|
|
|
78
83
|
// One day in milliseconds.
|
|
79
84
|
export const oneDayMs = 1 * 24 * 60 * 60 * 1000;
|
|
@@ -214,6 +219,14 @@ interface IUnreferencedEventProps {
|
|
|
214
219
|
viaHandle?: boolean;
|
|
215
220
|
}
|
|
216
221
|
|
|
222
|
+
/**
|
|
223
|
+
* The GC data that is tracked for a summary that is submitted.
|
|
224
|
+
*/
|
|
225
|
+
interface IGCSummaryTrackingData {
|
|
226
|
+
serializedGCState: string | undefined;
|
|
227
|
+
serializedTombstones: string | undefined;
|
|
228
|
+
}
|
|
229
|
+
|
|
217
230
|
/**
|
|
218
231
|
* Helper class that tracks the state of an unreferenced node such as the time it was unreferenced and if it can
|
|
219
232
|
* be deleted by the sweep phase.
|
|
@@ -370,6 +383,7 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
370
383
|
public readonly trackGCState: boolean;
|
|
371
384
|
|
|
372
385
|
private readonly testMode: boolean;
|
|
386
|
+
private readonly tombstoneMode: boolean;
|
|
373
387
|
private readonly mc: MonitoringContext;
|
|
374
388
|
|
|
375
389
|
/**
|
|
@@ -393,17 +407,19 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
393
407
|
|
|
394
408
|
// Keeps track of the GC state from the last run.
|
|
395
409
|
private previousGCDataFromLastRun: IGarbageCollectionData | undefined;
|
|
410
|
+
// Keeps a list of references (edges in the GC graph) between GC runs. Each entry has a node id and a list of
|
|
411
|
+
// outbound routes from that node.
|
|
412
|
+
private readonly newReferencesSinceLastRun: Map<string, string[]> = new Map();
|
|
413
|
+
private tombstones: string[] = [];
|
|
414
|
+
|
|
396
415
|
/**
|
|
397
|
-
* Keeps track of the
|
|
416
|
+
* Keeps track of the GC data from the latest summary successfully submitted to and acked from the server.
|
|
398
417
|
*/
|
|
399
|
-
private
|
|
418
|
+
private latestSummaryData: IGCSummaryTrackingData | undefined;
|
|
400
419
|
/**
|
|
401
|
-
* Keeps track of the
|
|
420
|
+
* Keeps track of the GC data from the last summary submitted to the server but not yet acked.
|
|
402
421
|
*/
|
|
403
|
-
private
|
|
404
|
-
// Keeps a list of references (edges in the GC graph) between GC runs. Each entry has a node id and a list of
|
|
405
|
-
// outbound routes from that node.
|
|
406
|
-
private readonly newReferencesSinceLastRun: Map<string, string[]> = new Map();
|
|
422
|
+
private pendingSummaryData: IGCSummaryTrackingData | undefined;
|
|
407
423
|
|
|
408
424
|
// Promise when resolved initializes the base state of the nodes from the base summary state.
|
|
409
425
|
private readonly initializeBaseStateP: Promise<void>;
|
|
@@ -449,6 +465,7 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
449
465
|
runGC: this.shouldRunGC,
|
|
450
466
|
runSweep: this.shouldRunSweep,
|
|
451
467
|
testMode: this.testMode,
|
|
468
|
+
tombstoneMode: this.tombstoneMode,
|
|
452
469
|
sessionExpiry: this.sessionExpiryTimeoutMs,
|
|
453
470
|
sweepTimeout: this.sweepTimeoutMs,
|
|
454
471
|
inactiveTimeout: this.inactiveTimeoutMs,
|
|
@@ -606,6 +623,7 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
606
623
|
|
|
607
624
|
// Whether we are running in test mode. In this mode, unreferenced nodes are immediately deleted.
|
|
608
625
|
this.testMode = this.mc.config.getBoolean(gcTestModeKey) ?? this.gcOptions.runGCInTestMode === true;
|
|
626
|
+
this.tombstoneMode = this.mc.config.getBoolean(testTombstoneKey) ?? false;
|
|
609
627
|
|
|
610
628
|
// The GC state needs to be reset if the base snapshot contains GC tree and GC is disabled or it doesn't
|
|
611
629
|
// contain GC tree and GC is enabled.
|
|
@@ -623,14 +641,20 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
623
641
|
// For newer documents, GC data should be present in the GC tree in the root of the snapshot.
|
|
624
642
|
const gcSnapshotTree = baseSnapshot.trees[gcTreeKey];
|
|
625
643
|
if (gcSnapshotTree !== undefined) {
|
|
626
|
-
const
|
|
644
|
+
const baseGCData = await getGCDataFromSnapshot(
|
|
627
645
|
gcSnapshotTree,
|
|
628
646
|
readAndParseBlob,
|
|
629
647
|
);
|
|
648
|
+
if (baseGCData.tombstones !== undefined && this.tombstoneMode) {
|
|
649
|
+
this.tombstones = baseGCData.tombstones;
|
|
650
|
+
}
|
|
630
651
|
if (this.trackGCState) {
|
|
631
|
-
this.
|
|
652
|
+
this.latestSummaryData = {
|
|
653
|
+
serializedGCState: JSON.stringify(generateSortedGCState(baseGCData.gcState)),
|
|
654
|
+
serializedTombstones: JSON.stringify(baseGCData.tombstones),
|
|
655
|
+
};
|
|
632
656
|
}
|
|
633
|
-
return
|
|
657
|
+
return baseGCData.gcState;
|
|
634
658
|
}
|
|
635
659
|
|
|
636
660
|
// back-compat - Older documents will have the GC blobs in each data store's summary tree. Get them and
|
|
@@ -787,15 +811,19 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
787
811
|
*/
|
|
788
812
|
public setConnectionState(connected: boolean, clientId?: string | undefined): void {
|
|
789
813
|
/**
|
|
790
|
-
* For
|
|
814
|
+
* For all clients, initialize the base state when the container becomes active, i.e., it transitions
|
|
791
815
|
* to "write" mode. This will ensure that the container's own join op is processed and there is a recent
|
|
792
816
|
* reference timestamp that will be used to update the state of unreferenced nodes. Also, all trailing ops which
|
|
793
817
|
* could affect the GC state will have been processed.
|
|
794
818
|
*
|
|
819
|
+
* If GC is up-to-date for the client and the summarizing client, there will be an doubling of both
|
|
820
|
+
* InactiveObject_Loaded and SweepReady_Loaded errors, as there will be one from the sending client and one from
|
|
821
|
+
* the receiving summarizer client.
|
|
822
|
+
*
|
|
795
823
|
* Ideally, this initialization should only be done for summarizer client. However, we are currently rolling out
|
|
796
824
|
* sweep in phases and we want to track when inactive and sweep ready objects are used in any client.
|
|
797
825
|
*/
|
|
798
|
-
if (this.activeConnection() &&
|
|
826
|
+
if (this.activeConnection() && this.shouldRunGC) {
|
|
799
827
|
this.initializeBaseStateP.catch((error) => {});
|
|
800
828
|
}
|
|
801
829
|
}
|
|
@@ -884,6 +912,23 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
884
912
|
// involving access to deleted data.
|
|
885
913
|
if (this.testMode) {
|
|
886
914
|
this.runtime.deleteUnusedRoutes(gcResult.deletedNodeIds);
|
|
915
|
+
} else {
|
|
916
|
+
// If we are running in GC tombstone mode, tombstone objects for unused routes. This enables testing
|
|
917
|
+
// scenarios involving access to "deleted" data without actually deleting the data from summaries.
|
|
918
|
+
// Note: we will not tombstone in test mode
|
|
919
|
+
if (this.tombstoneMode) {
|
|
920
|
+
const tombstoneRoutes: string[] = [];
|
|
921
|
+
// Currently only tombstone datastores
|
|
922
|
+
for (const [key, value] of this.unreferencedNodesState.entries()) {
|
|
923
|
+
if (
|
|
924
|
+
value.state === UnreferencedState.SweepReady &&
|
|
925
|
+
this.runtime.getNodeType(key) === GCNodeType.DataStore
|
|
926
|
+
) {
|
|
927
|
+
tombstoneRoutes.push(key);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
this.runtime.deleteUnusedRoutes(tombstoneRoutes);
|
|
931
|
+
}
|
|
887
932
|
}
|
|
888
933
|
|
|
889
934
|
// Log pending unreferenced events such as a node being used after inactive. This is done after GC runs and
|
|
@@ -916,35 +961,74 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
916
961
|
};
|
|
917
962
|
}
|
|
918
963
|
|
|
919
|
-
const
|
|
964
|
+
const serializedGCState = JSON.stringify(generateSortedGCState(gcState));
|
|
965
|
+
const serializedTombstones = this.tombstones.length > 0 ? JSON.stringify(this.tombstones.sort()) : undefined;
|
|
920
966
|
|
|
921
967
|
/**
|
|
922
|
-
*
|
|
923
|
-
*
|
|
968
|
+
* Incremental summary of GC data - If any of the GC state or tombstone state hasn't changed since the last
|
|
969
|
+
* summary, send summary handles for them. Otherwise, send the data in summary blobs.
|
|
924
970
|
*/
|
|
925
971
|
if (this.trackGCState) {
|
|
926
|
-
this.
|
|
927
|
-
if (
|
|
928
|
-
|
|
929
|
-
this.
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
|
|
972
|
+
this.pendingSummaryData = { serializedGCState, serializedTombstones };
|
|
973
|
+
if (trackState && !fullTree && this.latestSummaryData !== undefined) {
|
|
974
|
+
// If neither GC state or tombstone state changed, send a summary handle for the entire GC data.
|
|
975
|
+
if (this.latestSummaryData.serializedGCState === serializedGCState
|
|
976
|
+
&& this.latestSummaryData.serializedTombstones === serializedTombstones) {
|
|
977
|
+
const stats = mergeStats();
|
|
978
|
+
stats.handleNodeCount++;
|
|
979
|
+
return {
|
|
980
|
+
summary: {
|
|
981
|
+
type: SummaryType.Handle,
|
|
982
|
+
handle: `/${gcTreeKey}`,
|
|
983
|
+
handleType: SummaryType.Tree,
|
|
984
|
+
},
|
|
985
|
+
stats,
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// If either or both of GC state or tombstone state changed, build a GC summary tree.
|
|
990
|
+
return this.buildGCSummaryTree(serializedGCState, serializedTombstones, true /* trackState */);
|
|
943
991
|
}
|
|
944
992
|
}
|
|
993
|
+
// If not tracking GC state, build a GC summary tree without any summary handles.
|
|
994
|
+
return this.buildGCSummaryTree(serializedGCState, serializedTombstones, false /* trackState */);
|
|
995
|
+
}
|
|
945
996
|
|
|
997
|
+
/**
|
|
998
|
+
* Builds the GC summary tree which contains GC state and tombstone state.
|
|
999
|
+
* If trackState is false, both GC state and tombstone state are written as summary blobs.
|
|
1000
|
+
* If trackState is true, summary blob is written for GC state or tombstone state if they changed.
|
|
1001
|
+
* @param serializedGCState - The GC state serialized as string.
|
|
1002
|
+
* @param serializedTombstones - THe tombstone state serialized as string.
|
|
1003
|
+
* @param trackState - Whether we are tracking GC state across summaries.
|
|
1004
|
+
* @returns the GC summary tree.
|
|
1005
|
+
*/
|
|
1006
|
+
private buildGCSummaryTree(
|
|
1007
|
+
serializedGCState: string,
|
|
1008
|
+
serializedTombstones: string | undefined,
|
|
1009
|
+
trackState: boolean,
|
|
1010
|
+
): ISummaryTreeWithStats {
|
|
1011
|
+
const gcStateBlobKey = `${gcBlobPrefix}_root`;
|
|
946
1012
|
const builder = new SummaryTreeBuilder();
|
|
947
|
-
|
|
1013
|
+
|
|
1014
|
+
// If the GC state hasn't changed, write a summary handle, else write a summary blob for it.
|
|
1015
|
+
if (this.latestSummaryData?.serializedGCState === serializedGCState && trackState) {
|
|
1016
|
+
builder.addHandle(gcStateBlobKey, SummaryType.Blob, `/${gcTreeKey}/${gcStateBlobKey}`);
|
|
1017
|
+
} else {
|
|
1018
|
+
builder.addBlob(gcStateBlobKey, serializedGCState);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// If there is no tombstone data, return only the GC state.
|
|
1022
|
+
if (serializedTombstones === undefined) {
|
|
1023
|
+
return builder.getSummaryTree();
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// If the tombstone state hasn't changed, write a summary handle, else write a summary blob for it.
|
|
1027
|
+
if (this.latestSummaryData?.serializedTombstones === serializedTombstones && trackState) {
|
|
1028
|
+
builder.addHandle(gcTombstoneBlobKey, SummaryType.Blob, `/${gcTreeKey}/${gcTombstoneBlobKey}`);
|
|
1029
|
+
} else {
|
|
1030
|
+
builder.addBlob(gcTombstoneBlobKey, serializedTombstones);
|
|
1031
|
+
}
|
|
948
1032
|
return builder.getSummaryTree();
|
|
949
1033
|
}
|
|
950
1034
|
|
|
@@ -987,8 +1071,8 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
987
1071
|
this.latestSummaryGCVersion = this.currentGCVersion;
|
|
988
1072
|
this.initialStateNeedsReset = false;
|
|
989
1073
|
if (this.trackGCState) {
|
|
990
|
-
this.
|
|
991
|
-
this.
|
|
1074
|
+
this.latestSummaryData = this.pendingSummaryData;
|
|
1075
|
+
this.pendingSummaryData = undefined;
|
|
992
1076
|
}
|
|
993
1077
|
return;
|
|
994
1078
|
}
|
|
@@ -1003,15 +1087,18 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
1003
1087
|
|
|
1004
1088
|
const gcSnapshotTree = snapshot.trees[gcTreeKey];
|
|
1005
1089
|
if (gcSnapshotTree !== undefined && this.trackGCState) {
|
|
1006
|
-
const
|
|
1090
|
+
const latestGCData = await getGCDataFromSnapshot(
|
|
1007
1091
|
gcSnapshotTree,
|
|
1008
1092
|
readAndParseBlob,
|
|
1009
1093
|
);
|
|
1010
|
-
this.
|
|
1094
|
+
this.latestSummaryData = {
|
|
1095
|
+
serializedGCState: JSON.stringify(generateSortedGCState(latestGCData.gcState)),
|
|
1096
|
+
serializedTombstones: JSON.stringify(latestGCData.tombstones),
|
|
1097
|
+
};
|
|
1011
1098
|
} else {
|
|
1012
|
-
this.
|
|
1099
|
+
this.latestSummaryData = undefined;
|
|
1013
1100
|
}
|
|
1014
|
-
this.
|
|
1101
|
+
this.pendingSummaryData = undefined;
|
|
1015
1102
|
}
|
|
1016
1103
|
|
|
1017
1104
|
/**
|
|
@@ -1089,6 +1176,7 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
1089
1176
|
currentReferenceTimestampMs: number,
|
|
1090
1177
|
) {
|
|
1091
1178
|
this.previousGCDataFromLastRun = cloneGCData(gcData);
|
|
1179
|
+
this.tombstones = [];
|
|
1092
1180
|
this.newReferencesSinceLastRun.clear();
|
|
1093
1181
|
|
|
1094
1182
|
// Iterate through the referenced nodes and stop tracking if they were unreferenced before.
|
|
@@ -1121,6 +1209,12 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
1121
1209
|
);
|
|
1122
1210
|
} else {
|
|
1123
1211
|
nodeStateTracker.updateTracking(currentReferenceTimestampMs);
|
|
1212
|
+
if (this.tombstoneMode && nodeStateTracker.state === UnreferencedState.SweepReady) {
|
|
1213
|
+
const nodeType = this.runtime.getNodeType(nodeId);
|
|
1214
|
+
if (nodeType === GCNodeType.DataStore || nodeType === GCNodeType.Blob) {
|
|
1215
|
+
this.tombstones.push(nodeId);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1124
1218
|
}
|
|
1125
1219
|
}
|
|
1126
1220
|
}
|
|
@@ -1461,15 +1555,21 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
1461
1555
|
}
|
|
1462
1556
|
|
|
1463
1557
|
/**
|
|
1464
|
-
* Gets the garbage collection
|
|
1465
|
-
* Merge the GC state from all such blobs
|
|
1558
|
+
* Gets the garbage collection data from the given snapshot tree. It contains GC state and tombstone state.
|
|
1559
|
+
* The GC state may be written into multiple blobs. Merge the GC state from all such blobs into one.
|
|
1466
1560
|
*/
|
|
1467
|
-
async function
|
|
1561
|
+
async function getGCDataFromSnapshot(
|
|
1468
1562
|
gcSnapshotTree: ISnapshotTree,
|
|
1469
1563
|
readAndParseBlob: ReadAndParseBlob,
|
|
1470
|
-
)
|
|
1564
|
+
) {
|
|
1471
1565
|
let rootGCState: IGarbageCollectionState = { gcNodes: {} };
|
|
1566
|
+
let tombstones: string[] | undefined;
|
|
1472
1567
|
for (const key of Object.keys(gcSnapshotTree.blobs)) {
|
|
1568
|
+
if (key === gcTombstoneBlobKey) {
|
|
1569
|
+
tombstones = await readAndParseBlob<string[]>(gcSnapshotTree.blobs[key]);
|
|
1570
|
+
continue;
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1473
1573
|
// Skip blobs that do not start with the GC prefix.
|
|
1474
1574
|
if (!key.startsWith(gcBlobPrefix)) {
|
|
1475
1575
|
continue;
|
|
@@ -1484,7 +1584,7 @@ async function getGCStateFromSnapshot(
|
|
|
1484
1584
|
// Merge the GC state of this blob into the root GC state.
|
|
1485
1585
|
rootGCState = concatGarbageCollectionStates(rootGCState, gcState);
|
|
1486
1586
|
}
|
|
1487
|
-
return rootGCState;
|
|
1587
|
+
return { gcState: rootGCState, tombstones };
|
|
1488
1588
|
}
|
|
1489
1589
|
|
|
1490
1590
|
function generateSortedGCState(gcState: IGarbageCollectionState): IGarbageCollectionState {
|
package/src/index.ts
CHANGED
package/src/packageVersion.ts
CHANGED
|
@@ -77,7 +77,7 @@ export class SummarizerClientElection
|
|
|
77
77
|
// Log and elect a new summarizer client.
|
|
78
78
|
const opsSinceLastReport = sequenceNumber - this.lastReportedSeq;
|
|
79
79
|
if (opsSinceLastReport > this.maxOpsSinceLastSummary) {
|
|
80
|
-
this.logger.
|
|
80
|
+
this.logger.sendTelemetryEvent({
|
|
81
81
|
eventName: "ElectedClientNotSummarizing",
|
|
82
82
|
electedClientId,
|
|
83
83
|
lastSummaryAckSeqForClient: this.lastSummaryAckSeqForClient,
|
package/src/summaryGenerator.ts
CHANGED
|
@@ -231,13 +231,14 @@ export class SummaryGenerator {
|
|
|
231
231
|
const category = cancellationToken.cancelled || error?.errorType === DriverErrorType.offlineError ?
|
|
232
232
|
"generic" : "error";
|
|
233
233
|
|
|
234
|
+
const message = getFailMessage(errorCode);
|
|
234
235
|
summarizeEvent.cancel({
|
|
235
236
|
...properties,
|
|
236
237
|
reason: errorCode,
|
|
237
238
|
category,
|
|
238
239
|
retryAfterSeconds,
|
|
239
|
-
}, error);
|
|
240
|
-
resultsBuilder.fail(
|
|
240
|
+
}, error ?? message); // disconnect & summaryAckTimeout do not have proper error.
|
|
241
|
+
resultsBuilder.fail(message, error, nackSummaryResult, retryAfterSeconds);
|
|
241
242
|
};
|
|
242
243
|
|
|
243
244
|
// Wait to generate and send summary
|