/*! * Copyright (c) Microsoft Corporation and contributors. All rights reserved. * Licensed under the MIT License. */ import type { IContainerContext } from "@fluidframework/container-definitions/internal"; import { readAndParse } from "@fluidframework/driver-utils/internal"; import type { ISummaryTreeWithStats } from "@fluidframework/runtime-definitions/internal"; import { SummaryTreeBuilder } from "@fluidframework/runtime-utils/internal"; import type { ITelemetryLoggerExt } from "@fluidframework/telemetry-utils/internal"; /** * Information from a snapshot needed to load BlobManager * @internal */ export interface IBlobManagerLoadInfo { ids?: string[]; redirectTable?: [string, string][]; } export const redirectTableBlobName = ".redirectTable"; /** * @internal */ export const blobsTreeName = ".blobs"; /** * Reads blobs needed to load BlobManager from storage. * */ export const loadBlobManagerLoadInfo = async ( context: Pick, ): Promise => loadV1(context); const loadV1 = async ( context: Pick, ): Promise => { const blobsTree = context.baseSnapshot?.trees[blobsTreeName]; if (!blobsTree) { return {}; } let redirectTableEntries: [string, string][] = []; const tableId: string | undefined = blobsTree.blobs[redirectTableBlobName]; if (tableId) { redirectTableEntries = await readAndParse(context.storage, tableId); } const ids = Object.entries(blobsTree.blobs) .filter(([k, _]) => k !== redirectTableBlobName) .map(([_, v]) => v); return { ids, redirectTable: redirectTableEntries }; }; export const toRedirectTable = ( blobManagerLoadInfo: IBlobManagerLoadInfo, logger: ITelemetryLoggerExt, ): Map => { logger.sendTelemetryEvent({ eventName: "AttachmentBlobsLoaded", count: blobManagerLoadInfo.ids?.length ?? 0, redirectTable: blobManagerLoadInfo.redirectTable?.length, }); const redirectTable = new Map(blobManagerLoadInfo.redirectTable); if (blobManagerLoadInfo.ids !== undefined) { for (const storageId of blobManagerLoadInfo.ids) { // Older versions of the runtime used the storage ID directly in the handle, // rather than routing through the redirectTable. To support old handles that // were created in this way but unify handling through the redirectTable, we // add identity mappings to the redirect table at load. These identity entries // will be excluded during summarization. redirectTable.set(storageId, storageId); } } return redirectTable; }; export const summarizeBlobManagerState = ( redirectTable: Map, ): ISummaryTreeWithStats => summarizeV1(redirectTable); const summarizeV1 = (redirectTable: Map): ISummaryTreeWithStats => { const builder = new SummaryTreeBuilder(); const storageIds = getStorageIds(redirectTable); for (const storageId of storageIds) { // The Attachment is inspectable by storage, which lets it detect that the blob is referenced // and therefore should not be GC'd. builder.addAttachment(storageId); } // Exclude identity mappings from the redirectTable summary. Note that // the storageIds of the identity mappings are still included in the Attachments // above, so we expect these identity mappings will be recreated at load // time in toRedirectTable even if there is no non-identity mapping in // the redirectTable. const nonIdentityRedirectTableEntries = [...redirectTable.entries()].filter( ([localId, storageId]) => localId !== storageId, ); if (nonIdentityRedirectTableEntries.length > 0) { builder.addBlob(redirectTableBlobName, JSON.stringify(nonIdentityRedirectTableEntries)); } return builder.getSummaryTree(); }; export const getStorageIds = (redirectTable: Map): Set => { return new Set(redirectTable.values()); };