diff --git a/packages/runtime/container-runtime/src/dataStoreContext.ts b/packages/runtime/container-runtime/src/dataStoreContext.ts index 3a3a3450e1e1..475736b99d66 100644 --- a/packages/runtime/container-runtime/src/dataStoreContext.ts +++ b/packages/runtime/container-runtime/src/dataStoreContext.ts @@ -621,7 +621,7 @@ export abstract class FluidDataStoreContext extends TypedEventEmitter; - protected abstract getInitialGCSummaryDetails(): Promise; + public abstract getInitialGCSummaryDetails(): Promise; public reSubmit(contents: any, localOpMetadata: unknown) { assert(!!this.channel, "Channel must exist when resubmitting ops"); @@ -768,7 +768,7 @@ export class RemotedFluidDataStoreContext extends FluidDataStoreContext { return this.initialSnapshotDetailsP; } - protected async getInitialGCSummaryDetails(): Promise { + public async getInitialGCSummaryDetails(): Promise { return this.gcDetailsInInitialSummaryP; } @@ -861,7 +861,7 @@ export class LocalFluidDataStoreContextBase extends FluidDataStoreContext { }; } - protected async getInitialGCSummaryDetails(): Promise { + public async getInitialGCSummaryDetails(): Promise { // Local data store does not have initial summary. return {}; } diff --git a/packages/runtime/datastore/src/channelContext.ts b/packages/runtime/datastore/src/channelContext.ts index e7f983a0f224..08342c20c266 100644 --- a/packages/runtime/datastore/src/channelContext.ts +++ b/packages/runtime/datastore/src/channelContext.ts @@ -7,11 +7,9 @@ import { IChannel } from "@fluidframework/datastore-definitions"; import { IDocumentStorageService } from "@fluidframework/driver-definitions"; import { ISequencedDocumentMessage, ISnapshotTree } from "@fluidframework/protocol-definitions"; import { - gcBlobKey, IChannelSummarizeResult, IContextSummarizeResult, IGarbageCollectionData, - IGarbageCollectionSummaryDetails, } from "@fluidframework/runtime-definitions"; import { addBlobToSummary } from "@fluidframework/runtime-utils"; import { ChannelDeltaConnection } from "./channelDeltaConnection"; @@ -76,13 +74,5 @@ export function summarizeChannel( // Add the channel attributes to the returned result. addBlobToSummary(summarizeResult, attributesBlobKey, JSON.stringify(channel.attributes)); - - // Add GC details to the summary. - const gcDetails: IGarbageCollectionSummaryDetails = { - usedRoutes: [""], - gcData: summarizeResult.gcData, - }; - addBlobToSummary(summarizeResult, gcBlobKey, JSON.stringify(gcDetails)); - return summarizeResult; } diff --git a/packages/runtime/datastore/src/dataStoreRuntime.ts b/packages/runtime/datastore/src/dataStoreRuntime.ts index 28c3f7c6634b..b766d8752b5a 100644 --- a/packages/runtime/datastore/src/dataStoreRuntime.ts +++ b/packages/runtime/datastore/src/dataStoreRuntime.ts @@ -22,6 +22,7 @@ import { import { assert, Deferred, + LazyPromise, TypedEventEmitter, unreachableCase, } from "@fluidframework/common-utils"; @@ -48,6 +49,7 @@ import { IFluidDataStoreContext, IFluidDataStoreChannel, IGarbageCollectionData, + IGarbageCollectionSummaryDetails, IInboundSignalMessage, ISummaryTreeWithStats, } from "@fluidframework/runtime-definitions"; @@ -66,7 +68,13 @@ import { IChannelFactory, IChannelAttributes, } from "@fluidframework/datastore-definitions"; -import { GCDataBuilder, getChildNodesUsedRoutes } from "@fluidframework/garbage-collector"; +import { + cloneGCData, + GCDataBuilder, + getChildNodesGCData, + getChildNodesUsedRoutes, + removeRouteFromAllNodes, +} from "@fluidframework/garbage-collector"; import { v4 as uuid } from "uuid"; import { IChannelContext, summarizeChannel } from "./channelContext"; import { LocalChannelContext } from "./localChannelContext"; @@ -183,6 +191,14 @@ IFluidDataStoreChannel, IFluidDataStoreRuntime, IFluidHandleContext { private readonly audience: IAudience; public readonly logger: ITelemetryLogger; + // A map of child channel context ids to the context's used routes in the initial summary of this data store. This + // is used to initialize the context with data from the previous summary. + private readonly initialChannelUsedRoutesP: LazyPromise>; + + // A map of child channel context ids to the channel context's GC data in the initial summary of this data store. + // This is used to initialize the context with data from the previous summary. + private readonly initialChannelGCDataP: LazyPromise>; + public constructor( private readonly dataStoreContext: IFluidDataStoreContext, private readonly sharedObjectRegistry: ISharedObjectRegistry, @@ -200,6 +216,35 @@ IFluidDataStoreChannel, IFluidDataStoreRuntime, IFluidHandleContext { const tree = dataStoreContext.baseSnapshot; + this.initialChannelUsedRoutesP = new LazyPromise(async () => { + // back-compat: 0.35.0. getInitialGCSummaryDetails is added to IFluidDataStoreContext in 0.35.0. Remove + // undefined check when N > 0.36.0. + const gcDetailsInInitialSummary = await this.dataStoreContext.getInitialGCSummaryDetails?.(); + if (gcDetailsInInitialSummary?.usedRoutes !== undefined) { + // Remove the route to this data store, if it exists. + const usedRoutes = gcDetailsInInitialSummary.usedRoutes.filter( + (id: string) => { return id !== "/" && id !== ""; }, + ); + return getChildNodesUsedRoutes(usedRoutes); + } + return new Map(); + }); + + this.initialChannelGCDataP = new LazyPromise(async () => { + // back-compat: 0.35.0. getInitialGCSummaryDetails is added to IFluidDataStoreContext in 0.35.0. Remove + // undefined check when N > 0.36.0. + const gcDetailsInInitialSummary = await this.dataStoreContext.getInitialGCSummaryDetails?.(); + if (gcDetailsInInitialSummary?.gcData !== undefined) { + const gcData = cloneGCData(gcDetailsInInitialSummary.gcData); + // Remove GC node for this data store, if any. + delete gcData.gcNodes["/"]; + // Remove the back route to this data store that was added when generating each child's GC nodes. + removeRouteFromAllNodes(gcData.gcNodes, this.absolutePath); + return getChildNodesGCData(gcData); + } + return new Map(); + }); + // Must always receive the data store type inside of the attributes if (tree?.trees !== undefined) { Object.keys(tree.trees).forEach((path) => { @@ -246,7 +291,8 @@ IFluidDataStoreChannel, IFluidDataStoreRuntime, IFluidHandleContext { this.dataStoreContext.getCreateChildSummarizerNodeFn( path, { type: CreateSummarizerNodeSource.FromSummary }, - )); + ), + async () => this.getChannelInitialGCDetails(path)); } const deferred = new Deferred(); deferred.resolve(channelContext); @@ -512,6 +558,7 @@ IFluidDataStoreChannel, IFluidDataStoreRuntime, IFluidHandleContext { snapshot: attachMessage.snapshot, }, ), + async () => this.getChannelInitialGCDetails(id), attachMessage.type); this.contexts.set(id, remoteChannelContext); @@ -638,6 +685,30 @@ IFluidDataStoreChannel, IFluidDataStoreRuntime, IFluidHandleContext { } } + /** + * Returns the GC details in initial summary for the channel with the given id. The initial summary of the data + * store contains the GC details of all the child channel contexts that were created before the summary was taken. + * We find the GC details belonging to the given channel context and return it. + * @param channelId - The id of the channel context that is asked for the initial GC details. + * @returns the requested channel's GC details in the initial summary. + */ + private async getChannelInitialGCDetails(channelId: string): Promise { + const channelInitialUsedRoutes = await this.initialChannelUsedRoutesP; + const channelInitialGCData = await this.initialChannelGCDataP; + + let channelUsedRoutes = channelInitialUsedRoutes.get(channelId); + // Currently, channel context's are always considered used. So, it there is no used route for it, we still + // need to mark it as used. Add self-route (empty string) to the channel context's used routes. + if (channelUsedRoutes === undefined || channelUsedRoutes.length === 0) { + channelUsedRoutes = [""]; + } + + return { + usedRoutes: channelUsedRoutes, + gcData: channelInitialGCData.get(channelId), + }; + } + /** * Returns a summary at the current sequence number. * @param fullTree - true to bypass optimizations and force a full summary tree diff --git a/packages/runtime/datastore/src/remoteChannelContext.ts b/packages/runtime/datastore/src/remoteChannelContext.ts index 64a9ec969485..fc58bea96f52 100644 --- a/packages/runtime/datastore/src/remoteChannelContext.ts +++ b/packages/runtime/datastore/src/remoteChannelContext.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -import { assert, LazyPromise } from "@fluidframework/common-utils"; +import { assert } from "@fluidframework/common-utils"; import { CreateContainerError, DataCorruptionError } from "@fluidframework/container-utils"; import { IChannel, @@ -19,7 +19,6 @@ import { } from "@fluidframework/protocol-definitions"; import { CreateChildSummarizerNodeFn, - gcBlobKey, IContextSummarizeResult, IFluidDataStoreContext, IGarbageCollectionData, @@ -49,17 +48,6 @@ export class RemoteChannelContext implements IChannelContext { }; private readonly summarizerNode: ISummarizerNodeWithGC; - /** - * This loads the GC details from the base snapshot of this context. - */ - private readonly gcDetailsInInitialSummaryP = new LazyPromise(async () => { - if (await this.services.objectStorage.contains(gcBlobKey)) { - return readAndParse(this.services.objectStorage, gcBlobKey); - } else { - return {}; - } - }); - constructor( private readonly runtime: IFluidDataStoreRuntime, private readonly dataStoreContext: IFluidDataStoreContext, @@ -71,6 +59,7 @@ export class RemoteChannelContext implements IChannelContext { private readonly registry: ISharedObjectRegistry, extraBlobs: Map | undefined, createSummarizerNode: CreateChildSummarizerNodeFn, + gcDetailsInInitialSummary: () => Promise, private readonly attachMessageType?: string, ) { this.services = createServiceEndpoints( @@ -88,7 +77,7 @@ export class RemoteChannelContext implements IChannelContext { this.summarizerNode = createSummarizerNode( thisSummarizeInternal, async () => this.getGCDataInternal(), - async () => this.gcDetailsInInitialSummaryP, + async () => gcDetailsInInitialSummary(), ); } diff --git a/packages/runtime/garbage-collector/src/utils.ts b/packages/runtime/garbage-collector/src/utils.ts index c464d6c2bad0..bd128e466637 100644 --- a/packages/runtime/garbage-collector/src/utils.ts +++ b/packages/runtime/garbage-collector/src/utils.ts @@ -22,25 +22,69 @@ export function cloneGCData(gcData: IGarbageCollectionData): IGarbageCollectionD } /** - * Helper function that generates the used routes of the children from a given node's used routes. + * Helper function that generates the used routes of children from a given node's used routes. * @param usedRoutes - The used routes of a node. * @returns A map of used routes of each children of the the given node. */ export function getChildNodesUsedRoutes(usedRoutes: string[]) { - const usedNodesRoutes: Map = new Map(); + const childUsedRoutesMap: Map = new Map(); for (const route of usedRoutes) { assert(route.startsWith("/"), "Used route should always be an absolute route"); const childId = route.split("/")[1]; const childUsedRoute = route.slice(childId.length + 1); - const childUsedRoutes = usedNodesRoutes.get(childId); + const childUsedRoutes = childUsedRoutesMap.get(childId); if (childUsedRoutes !== undefined) { childUsedRoutes.push(childUsedRoute); } else { - usedNodesRoutes.set(childId, [ childUsedRoute ]); + childUsedRoutesMap.set(childId, [ childUsedRoute ]); + } + } + return childUsedRoutesMap; +} + +/** + * Helper function that generates the GC data of children from a given node's GC data. + * @param gcData - The GC data of a node. + * @returns A map of GC data of each children of the the given node. + */ +export function getChildNodesGCData(gcData: IGarbageCollectionData) { + const childGCDataMap: Map = new Map(); + for (const [id, outboundRoutes] of Object.entries(gcData.gcNodes)) { + assert(id.startsWith("/"), "id should always be an absolute route"); + const childId = id.split("/")[1]; + let childGCNodeId = id.slice(childId.length + 1); + // GC node id always begins with "/". Handle the special case where the id in parent's GC nodes is of the + // for `/root`. This would make `childId=root` and `childGCNodeId=""`. + if (childGCNodeId === "") { + childGCNodeId = "/"; + } + + // Create a copy of the outbound routes array in the parents GC data. + const childOutboundRoutes = Array.from(outboundRoutes); + + let childGCData = childGCDataMap.get(childId); + if (childGCData === undefined) { + childGCData = { gcNodes: {} }; + } + childGCData.gcNodes[childGCNodeId] = childOutboundRoutes; + childGCDataMap.set(childId, childGCData); + } + return childGCDataMap; +} + +/** + * Removes the given route from the outbound routes of all the given GC nodes. + * @param gcNodes - The nodes from which the route is to be removed. + * @param outboundRoute - The route to be removed. + */ +export function removeRouteFromAllNodes(gcNodes: { [ id: string ]: string[] }, outboundRoute: string) { + for (const outboundRoutes of Object.values(gcNodes)) { + const index = outboundRoutes.indexOf(outboundRoute); + if (index > -1) { + outboundRoutes.splice(index, 1); } } - return usedNodesRoutes; } export class GCDataBuilder implements IGarbageCollectionData { diff --git a/packages/runtime/runtime-definitions/src/dataStoreContext.ts b/packages/runtime/runtime-definitions/src/dataStoreContext.ts index 1f59a52af141..a8c98151c8fb 100644 --- a/packages/runtime/runtime-definitions/src/dataStoreContext.ts +++ b/packages/runtime/runtime-definitions/src/dataStoreContext.ts @@ -347,6 +347,12 @@ IEventProvider, Partial>; + + /** + * Returns the GC details in the initial summary of this data store. This is used to initialize the data store + * and its children with the GC details from the previous summary. + */ + getInitialGCSummaryDetails(): Promise; } export interface IFluidDataStoreContextDetached extends IFluidDataStoreContext { diff --git a/packages/test/snapshots/content b/packages/test/snapshots/content index ef3d5404c446..9a93e64bfef2 160000 --- a/packages/test/snapshots/content +++ b/packages/test/snapshots/content @@ -1 +1 @@ -Subproject commit ef3d5404c44658053718fd43c22fbd015460f476 +Subproject commit 9a93e64bfef267fc06f29362f4f8599a50ae72dc