diff --git a/packages/loader/container-loader/src/containerStorageAdapter.ts b/packages/loader/container-loader/src/containerStorageAdapter.ts index 0a114d44e4d7..1c661f4b8942 100644 --- a/packages/loader/container-loader/src/containerStorageAdapter.ts +++ b/packages/loader/container-loader/src/containerStorageAdapter.ts @@ -20,7 +20,7 @@ import { ISnapshotTree, IVersion, } from "@fluidframework/driver-definitions/internal"; -import { UsageError } from "@fluidframework/driver-utils/internal"; +import { isInstanceOfISnapshot, UsageError } from "@fluidframework/driver-utils/internal"; import { ITelemetryLoggerExt } from "@fluidframework/telemetry-utils/internal"; // eslint-disable-next-line import/no-deprecated @@ -31,7 +31,7 @@ import type { ISerializedStateManagerDocumentStorageService, ISnapshotInfo, } from "./serializedStateManager.js"; -import { convertSnapshotInfoToSnapshot, getDocumentAttributes } from "./utils.js"; +import { convertSnapshotInfoToSnapshot } from "./utils.js"; /** * Stringified blobs from a summary/snapshot tree. @@ -175,8 +175,10 @@ export class ContainerStorageAdapter const localSnapshot = this.loadingGroupIdSnapshotsFromPendingState[snapshotFetchOptions.loadingGroupIds[0]]; assert(localSnapshot !== undefined, 0x970 /* Local snapshot must be present */); - const attributes = await getDocumentAttributes(this, localSnapshot.baseSnapshot); - snapshot = convertSnapshotInfoToSnapshot(localSnapshot, attributes.sequenceNumber); + snapshot = convertSnapshotInfoToSnapshot( + localSnapshot, + localSnapshot.snapshotSequenceNumber, + ); } else { if (this._storageService.getSnapshot === undefined) { throw new UsageError( @@ -308,11 +310,19 @@ const redirectTableBlobName = ".redirectTable"; * Get blob contents of a snapshot tree from storage (or, ideally, cache) */ export async function getBlobContentsFromTree( - snapshot: ISnapshotTree, + snapshot: ISnapshot | ISnapshotTree, storage: Pick, ): Promise { const blobs = {}; - await getBlobContentsFromTreeCore(snapshot, blobs, storage); + if (isInstanceOfISnapshot(snapshot)) { + const blobContents = snapshot.blobContents; + for (const [id, content] of blobContents.entries()) { + // ArrayBufferLike will not survive JSON.stringify() + blobs[id] = bufferToString(content, "utf8"); + } + } else { + await getBlobContentsFromTreeCore(snapshot, blobs, storage); + } return blobs; } diff --git a/packages/loader/container-loader/src/serializedStateManager.ts b/packages/loader/container-loader/src/serializedStateManager.ts index 73885fce1661..9e8d92848ac2 100644 --- a/packages/loader/container-loader/src/serializedStateManager.ts +++ b/packages/loader/container-loader/src/serializedStateManager.ts @@ -214,10 +214,7 @@ export class SerializedStateManager { const baseSnapshotTree: ISnapshotTree | undefined = getSnapshotTree(baseSnapshot); // non-interactive clients will not have any pending state we want to save if (this.offlineLoadEnabled) { - const snapshotBlobs = await getBlobContentsFromTree( - baseSnapshotTree, - this.storageAdapter, - ); + const snapshotBlobs = await getBlobContentsFromTree(baseSnapshot, this.storageAdapter); const attributes = await getDocumentAttributes(this.storageAdapter, baseSnapshotTree); this.snapshot = { baseSnapshot: baseSnapshotTree, @@ -459,7 +456,7 @@ export async function getLatestSnapshotInfo( const baseSnapshotTree: ISnapshotTree | undefined = getSnapshotTree(baseSnapshot); const snapshotFetchedTime = Date.now(); - const snapshotBlobs = await getBlobContentsFromTree(baseSnapshotTree, storageAdapter); + const snapshotBlobs = await getBlobContentsFromTree(baseSnapshot, storageAdapter); const attributes: IDocumentAttributes = await getDocumentAttributes( storageAdapter, baseSnapshotTree, diff --git a/packages/test/test-end-to-end-tests/src/test/data-virtualization/groupIdBlobReads.spec.ts b/packages/test/test-end-to-end-tests/src/test/data-virtualization/groupIdBlobReads.spec.ts new file mode 100644 index 000000000000..2067b4a9f192 --- /dev/null +++ b/packages/test/test-end-to-end-tests/src/test/data-virtualization/groupIdBlobReads.spec.ts @@ -0,0 +1,97 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { + describeCompat, + TestDataObjectType, + type ITestDataObject, +} from "@fluid-private/test-version-utils"; +import { type IContainerRuntimeOptions } from "@fluidframework/container-runtime/internal"; +import type { IContainerRuntimeBase } from "@fluidframework/runtime-definitions/internal"; +import { MockLogger } from "@fluidframework/telemetry-utils/internal"; +import { + type ITestObjectProvider, + createSummarizer, + createTestConfigProvider, + summarizeNow, +} from "@fluidframework/test-utils/internal"; + +import { TestSnapshotCache } from "../../testSnapshotCache.js"; + +describeCompat("Odsp Network calls", "NoCompat", (getTestObjectProvider) => { + // Allow us to control summaries + const runtimeOptions: IContainerRuntimeOptions = { + summaryOptions: { + summaryConfigOverrides: { + state: "disabled", + }, + }, + }; + const configProvider = createTestConfigProvider({ + "Fluid.Container.UseLoadingGroupIdForSnapshotFetch2": true, + "Fluid.Container.enableOfflineLoad": true, + }); + + let provider: ITestObjectProvider; + const testSnapshotCache = new TestSnapshotCache(); + + beforeEach("setup", async function () { + provider = getTestObjectProvider({ persistedCache: testSnapshotCache }); + if (provider.driver.type !== "odsp") { + this.skip(); + } + }); + + const loadingGroupId = "loadingGroupId"; + const createDataObjectsWithGroupIds = async ( + mainObject: ITestDataObject, + containerRuntime: IContainerRuntimeBase, + ) => { + const dataStoreA = await containerRuntime.createDataStore( + TestDataObjectType, + loadingGroupId, + ); + const dataStoreB = await containerRuntime.createDataStore( + TestDataObjectType, + loadingGroupId, + ); + + mainObject._root.set("dataObjectA", dataStoreA.entryPoint); + mainObject._root.set("dataObjectB", dataStoreB.entryPoint); + }; + + it("Should not make odsp network calls", async () => { + const container = await provider.makeTestContainer({ + runtimeOptions, + loaderProps: { configProvider }, + }); + const mainObject = (await container.getEntryPoint()) as ITestDataObject; + const containerRuntime = mainObject._context.containerRuntime; + + // Testing all apis for creating a data store with a loadingGroupId + await createDataObjectsWithGroupIds(mainObject, containerRuntime); + const { summarizer } = await createSummarizer(provider, container, { + loaderProps: { configProvider }, + }); + await provider.ensureSynchronized(); + await summarizeNow(summarizer); + + testSnapshotCache.clearCache(); + const logger = new MockLogger(); + await provider.loadTestContainer({ + loaderProps: { configProvider, logger }, + }); + if (provider.driver.type === "odsp") { + logger.assertMatchNone( + [ + { + eventName: "fluid:telemetry:OdspDriver:readDataBlob_end", + }, + ], + "Should not have any odps network calls", + ); + } + }); +});