diff --git a/packages/loader/container-loader/src/container.ts b/packages/loader/container-loader/src/container.ts index 1f9c4465e1fc..865f14d57a2d 100644 --- a/packages/loader/container-loader/src/container.ts +++ b/packages/loader/container-loader/src/container.ts @@ -744,6 +744,14 @@ export class Container return this._dirtyContainer; } + private get isStorageOnly(): boolean { + let storageOnly = false; + if (this.readOnlyInfo.readonly === true) { + storageOnly = this.readOnlyInfo.storageOnly; + } + return storageOnly; + } + /** * {@inheritDoc @fluidframework/container-definitions#IContainer.entryPoint} */ @@ -1027,6 +1035,7 @@ export class Container this.storageAdapter, offlineLoadEnabled, this, + () => this.isStorageOnly, () => this._deltaManager.connectionManager.shouldJoinWrite(), () => this.supportGetSnapshotApi(), this.mc.config.getNumber("Fluid.Container.snapshotRefreshTimeoutMs"), diff --git a/packages/loader/container-loader/src/serializedStateManager.ts b/packages/loader/container-loader/src/serializedStateManager.ts index 7af0bee31042..4e44470c0efe 100644 --- a/packages/loader/container-loader/src/serializedStateManager.ts +++ b/packages/loader/container-loader/src/serializedStateManager.ts @@ -149,7 +149,7 @@ export class SerializedStateManager { private latestSnapshot: ISnapshotInfo | undefined; private _refreshSnapshotP: Promise | undefined; private readonly lastSavedOpSequenceNumber: number = 0; - private readonly refreshTimer: Timer; + private readonly refreshTimer: Timer | undefined; private readonly snapshotRefreshTimeoutMs: number = 60 * 60 * 24 * 1000; /** @@ -166,6 +166,7 @@ export class SerializedStateManager { private readonly storageAdapter: ISerializedStateManagerDocumentStorageService, private readonly _offlineLoadEnabled: boolean, containerEvent: IEventProvider, + private readonly storageOnly: () => boolean, private readonly containerDirty: () => boolean, private readonly supportGetSnapshotApi: () => boolean, snapshotRefreshTimeoutMs?: number, @@ -176,9 +177,9 @@ export class SerializedStateManager { }); this.snapshotRefreshTimeoutMs = snapshotRefreshTimeoutMs ?? this.snapshotRefreshTimeoutMs; - this.refreshTimer = new Timer(this.snapshotRefreshTimeoutMs, () => - this.tryRefreshSnapshot(), - ); + this.refreshTimer = this.storageOnly() + ? undefined + : new Timer(this.snapshotRefreshTimeoutMs, () => this.tryRefreshSnapshot()); // special case handle. Obtaining the last saved op seq num to avoid // refreshing the snapshot before we have processed it. It could cause // a subsequent stashing to have a newer snapshot than allowed. @@ -244,7 +245,7 @@ export class SerializedStateManager { snapshotBlobs, snapshotSequenceNumber: attributes.sequenceNumber, }; - this.refreshTimer.start(); + this.refreshTimer?.start(); } return { baseSnapshot, version }; } else { @@ -276,7 +277,8 @@ export class SerializedStateManager { if ( this.mc.config.getBoolean("Fluid.Container.enableOfflineSnapshotRefresh") === true && this._refreshSnapshotP === undefined && - this.latestSnapshot === undefined + this.latestSnapshot === undefined && + !this.storageOnly() ) { // Don't block on the refresh snapshot call - it is for the next time we serialize, not booting this incarnation this._refreshSnapshotP = this.refreshLatestSnapshot(this.supportGetSnapshotApi()); @@ -361,14 +363,14 @@ export class SerializedStateManager { stashedSnapshotSequenceNumber: this.snapshot?.snapshotSequenceNumber, }); this.latestSnapshot = undefined; - this.refreshTimer.restart(); + this.refreshTimer?.restart(); } else if (snapshotSequenceNumber <= lastProcessedOpSequenceNumber) { // Snapshot seq num is between the first and last processed op. // Remove the ops that are already part of the snapshot this.processedOps.splice(0, snapshotSequenceNumber - firstProcessedOpSequenceNumber + 1); this.snapshot = this.latestSnapshot; this.latestSnapshot = undefined; - this.refreshTimer.restart(); + this.refreshTimer?.restart(); this.mc.logger.sendTelemetryEvent({ eventName: "SnapshotRefreshed", snapshotSequenceNumber, @@ -410,7 +412,7 @@ export class SerializedStateManager { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access snapshotSequenceNumber: attributes.sequenceNumber as number, }; - this.refreshTimer.start(); + this.refreshTimer?.start(); } } diff --git a/packages/loader/container-loader/src/test/serializedStateManager.spec.ts b/packages/loader/container-loader/src/test/serializedStateManager.spec.ts index aef818dc276c..70b2ed052c1f 100644 --- a/packages/loader/container-loader/src/test/serializedStateManager.spec.ts +++ b/packages/loader/container-loader/src/test/serializedStateManager.spec.ts @@ -193,6 +193,7 @@ describe("serializedStateManager", () => { eventEmitter, () => false, () => false, + () => false, ); await assert.rejects( @@ -219,6 +220,7 @@ describe("serializedStateManager", () => { eventEmitter, () => false, () => false, + () => false, ); // equivalent to attach serializedStateManager.setInitialSnapshot({ @@ -248,6 +250,7 @@ describe("serializedStateManager", () => { eventEmitter, () => false, () => false, + () => false, ); const { baseSnapshot, version } = await serializedStateManager.fetchSnapshot(undefined); assert(baseSnapshot); @@ -271,6 +274,7 @@ describe("serializedStateManager", () => { eventEmitter, () => false, () => false, + () => false, ); const { baseSnapshot, version } = await serializedStateManager.fetchSnapshot(undefined); assert(baseSnapshot); @@ -306,6 +310,7 @@ describe("serializedStateManager", () => { eventEmitter, () => false, () => false, + () => false, ); // eslint-disable-next-line no-void void serializedStateManager.refreshSnapshotP?.then(() => @@ -357,6 +362,7 @@ describe("serializedStateManager", () => { eventEmitter, () => false, () => false, + () => false, ); let seq = 1; let lastProcessedOpSequenceNumber = 20; @@ -420,6 +426,7 @@ describe("serializedStateManager", () => { eventEmitter, () => false, () => false, + () => false, ); const lastProcessedOpSequenceNumber = 20; let seq = 1; @@ -459,6 +466,7 @@ describe("serializedStateManager", () => { storageAdapter, true, eventEmitter, + () => false, isDirtyF, () => false, ); @@ -519,6 +527,7 @@ describe("serializedStateManager", () => { storageAdapter, true, eventEmitter, + () => false, isDirtyF, () => false, ); @@ -571,6 +580,7 @@ describe("serializedStateManager", () => { storageAdapter, true, eventEmitter, + () => false, isDirtyF, () => false, ); @@ -634,6 +644,7 @@ describe("serializedStateManager", () => { storageAdapter, true, eventEmitter, + () => false, () => isDirty, () => false, ); @@ -666,6 +677,7 @@ describe("serializedStateManager", () => { storageAdapter, true, eventEmitter, + () => false, isDirtyF, () => false, ); @@ -737,6 +749,7 @@ describe("serializedStateManager", () => { storageAdapter, true, eventEmitter, + () => false, isDirtyF, () => false, ); @@ -787,6 +800,7 @@ describe("serializedStateManager", () => { storageAdapter, true, eventEmitter, + () => false, () => isDirty, () => false, ); @@ -822,6 +836,7 @@ describe("serializedStateManager", () => { storageAdapter, true, eventEmitter, + () => false, () => isDirty, () => false, ); @@ -873,6 +888,7 @@ describe("serializedStateManager", () => { storageAdapter, true, eventEmitter, + () => false, isDirtyF, () => false, ); @@ -974,6 +990,7 @@ describe("serializedStateManager", () => { storageAdapter, true, eventEmitter, + () => false, isDirtyF, () => false, snapshotRefreshTimeoutMs, @@ -1043,6 +1060,7 @@ describe("serializedStateManager", () => { storageAdapter, true, eventEmitter, + () => false, isDirtyF, () => false, snapshotRefreshTimeoutMs, @@ -1112,6 +1130,7 @@ describe("serializedStateManager", () => { storageAdapter, true, eventEmitter, + () => false, isDirtyF, () => false, snapshotRefreshTimeoutMs, @@ -1176,6 +1195,7 @@ describe("serializedStateManager", () => { storageAdapter, true, eventEmitter, + () => false, isDirtyF, () => false, snapshotRefreshTimeoutMs, @@ -1246,6 +1266,7 @@ describe("serializedStateManager", () => { storageAdapter, true, eventEmitter, + () => false, isDirtyF, () => false, snapshotRefreshTimeoutMs, @@ -1313,6 +1334,7 @@ describe("serializedStateManager", () => { storageAdapter, true, eventEmitter, + () => false, isDirtyF, () => false, snapshotRefreshTimeoutMs, @@ -1363,6 +1385,39 @@ describe("serializedStateManager", () => { lastProcessedOpSequenceNumber, ); }); + + it(`no snapshot refresh on storage only mode. isDirty: ${isDirty}`, async () => { + const storageAdapter = new MockStorageAdapter(); + const saved = false; + const isDirtyF = (): boolean => (saved ? false : isDirty); + const serializedStateManager = new SerializedStateManager( + undefined, + enableOfflineSnapshotRefresh(logger), + storageAdapter, + true, + eventEmitter, + () => true, + isDirtyF, + () => false, + snapshotRefreshTimeoutMs, + ); + + await serializedStateManager.fetchSnapshot(undefined); + const lastProcessedOpSequenceNumber = 20; + let seq = 1; + while (seq <= lastProcessedOpSequenceNumber) { + serializedStateManager.addProcessedOp(generateSavedOp(seq++)); + } + const snapshotSequenceNumber = 11; // latest snapshot will be among processed ops + storageAdapter.uploadSummary(snapshotSequenceNumber); + // snapshot refresh promise is undefined before timeout + const snapshotRefreshP = serializedStateManager.refreshSnapshotP; + assert.strictEqual(snapshotRefreshP, undefined); + clock.tick(snapshotRefreshTimeoutMs); + // now it's a promise + const initialRefreshP = serializedStateManager.refreshSnapshotP; + assert.strictEqual(initialRefreshP, undefined, "no refresh expected"); + }); } }); });