import { strict as assert } from "assert" ; import { ContainerRuntimeFactoryWithDefaultDataStore , DataObjectFactory } from "@fluidframework/aqueduct" ; import { TelemetryNullLogger } from "@fluidframework/common-utils" ; import { IContainer , IContainerContext , IRuntime , IRuntimeFactory , } from "@fluidframework/container-definitions" ; import { ContainerRuntime , IAckedSummary , IContainerRuntimeOptions , SummaryCollection , } from "@fluidframework/container-runtime" ; import { ISummaryContext } from "@fluidframework/driver-definitions" ; import { ISummaryTree , SummaryType , } from "@fluidframework/protocol-definitions" ; import { channelsTreeName , IContainerRuntimeBase } from "@fluidframework/runtime-definitions" ; import { requestFluidObject } from "@fluidframework/runtime-utils" ; import { ITestObjectProvider } from "@fluidframework/test-utils" ; import { describeFullCompat } from "@fluidframework/test-version-utils" ; import { IRequest } from "@fluidframework/core-interfaces" ; import { wrapDocumentServiceFactory } from "./gcDriverWrappers" ; import { loadSummarizer , TestDataObject , submitAndAckSummary } from "./mockSummarizerClient" ; /**
 * Runtime factory that increments the current GC version of the container runtime it creates. This is used to simulate
 * scenario where the GC version upgrades and we have to regenerate the GC data and summary.
 */ class ContainerRuntimeFactoryWithGC extends ContainerRuntimeFactoryWithDefaultDataStore { public async instantiateRuntime ( context ) { const runtime = await super . instantiateRuntime ( context ) ; ( runtime as any ) . garbageCollector . currentGCVersion += 1 ; return runtime ; } } /**
 * Validates that when the runtime GC version changes, we re-run GC and summary. Basically, when we update the GC
 * version due to either bugs or changes in the implementation, we re-run GC and regenerate summary based on the
 * new GC code.
 */ describeFullCompat ( "GC version upgrade" , ( getTestObjectProvider ) => { let provider ; const factory = new DataObjectFactory ( "TestDataObject" , TestDataObject , [ ] , [ ] ) ; const runtimeOptions = { summaryOptions : { disableSummaries : true } , gcOptions : { gcAllowed : true } } ; const innerRequestHandler = async ( request , runtime ) => runtime . IFluidHandleContext . resolveHandle ( request ) ; const defaultRuntimeFactory = new ContainerRuntimeFactoryWithDefaultDataStore ( factory , [ [ factory . type , Promise . resolve ( factory ) ] , ] , undefined , [ innerRequestHandler ] , runtimeOptions ) ; const logger = new TelemetryNullLogger ( ) ; let latestUploadedSummary ; let latestSummaryContext ; let latestAckedSummary ; let mainContainer ; let dataStore1Id ; let dataStore2Id ; let dataStore3Id ; const createContainer = async ( runtimeFactory ) : Promise < IContainer > => { return provider . createContainer ( runtimeFactory ) ; } ; const getNewSummarizer = async ( runtimeFactory , summaryVersion ? ) => { return loadSummarizer ( provider , runtimeFactory , mainContainer . deltaManager . lastSequenceNumber , summaryVersion ) ; } ; param containerRuntime param summaryCollection param dataStoresAsHandles async function validateDataStoreSummaryState ( summarizerClient , dataStoresAsHandles ) { const summaryResult = await submitAndAckSummary ( provider , summarizerClient , logger ) ; latestAckedSummary = summaryResult . ackedSummary ; assert ( latestSummaryContext && latestSummaryContext . referenceSequenceNumber >= summaryResult . summarySequenceNumber , `Did not get expected summary. Expected: ${ summaryResult . summarySequenceNumber }. ` + `Actual: ${ latestSummaryContext ?. referenceSequenceNumber }.` ) ; assert ( latestUploadedSummary !== undefined , "Did not get a summary" ) ; const dataStoreTrees = ( latestUploadedSummary . tree [ channelsTreeName ] as ISummaryTree ) ?. tree ?? latestUploadedSummary . tree ; for ( const [ key , value ] of Object . entries ( dataStoreTrees ) ) { if ( dataStoresAsHandles . includes ( key ) ) { assert ( value . type === SummaryType . Handle , `The summary for data store ${ key } should be a handle` ) ; } else { assert ( value . type === SummaryType . Tree , `The summary for data store ${ key } should be a tree` ) ; } } } /**
     * Callback that will be called by the document storage service whenever a summary is uploaded by the client.
     * Update the summary context to include the summary proposal and ack handle as per the latest ack for the
     * document.
     */ function uploadSummaryCb ( summaryTree , context ) { latestUploadedSummary = summaryTree ; latestSummaryContext = context ; const newSummaryContext = { ... context } ; if ( latestAckedSummary !== undefined ) { newSummaryContext . ackHandle = latestAckedSummary . summaryAck . contents . handle ; newSummaryContext . proposalHandle = latestAckedSummary . summaryOp . contents . handle ; } return newSummaryContext ; } beforeEach ( async ( ) => { provider = getTestObjectProvider ( ) ; ( provider as any ) . _documentServiceFactory = wrapDocumentServiceFactory ( provider . documentServiceFactory , uploadSummaryCb ) ; mainContainer = await createContainer ( defaultRuntimeFactory ) ; const dataStore1 = await requestFluidObject < TestDataObject > ( mainContainer , "default" ) ; dataStore1Id = dataStore1 . id ; const dataStore2 = await factory . createInstance ( dataStore1 . containerRuntime ) ; dataStore1 . _root . set ( "dataStore2" , dataStore2 . handle ) ; const dataStore3 = await factory . createInstance ( dataStore1 . containerRuntime ) ; dataStore1 . _root . set ( "dataStore3" , dataStore3 . handle ) ; dataStore2Id = dataStore2 . id ; dataStore3Id = dataStore3 . id ; await provider . ensureSynchronized ( ) ; } ) ; afterEach ( ( ) => { latestSummaryContext = undefined ; latestUploadedSummary = undefined ; latestAckedSummary = undefined ; } ) ; it ( "should regenerate summary and GC data when GC version updates" , async ( ) => { let dataStoresAsHandles = [ ] ; const summarizerClient1 = await getNewSummarizer ( defaultRuntimeFactory ) ; await validateDataStoreSummaryState ( summarizerClient1 , dataStoresAsHandles ) ; dataStoresAsHandles . push ( dataStore1Id , dataStore2Id , dataStore3Id ) ; await validateDataStoreSummaryState ( summarizerClient1 , dataStoresAsHandles ) ; const gcRuntimeFactory = new ContainerRuntimeFactoryWithGC ( factory , [ [ factory . type , Promise . resolve ( factory ) ] , ] , undefined , [ innerRequestHandler ] , runtimeOptions ) ; assert ( latestAckedSummary !== undefined , "Summary ack isn't available as expected" ) ; const summarizerClient2 = await getNewSummarizer ( gcRuntimeFactory , latestAckedSummary . summaryAck . contents . handle ) ; dataStoresAsHandles = [ ] ; await validateDataStoreSummaryState ( summarizerClient2 , dataStoresAsHandles ) ; } ) . timeout ( 20000 ) ; it ( "should regenerate summary and GC data on receiving ack with different GC version" , async ( ) => { const dataStoresAsHandles = [ ] ; const summarizerClient1 = await getNewSummarizer ( defaultRuntimeFactory ) ; await validateDataStoreSummaryState ( summarizerClient1 , dataStoresAsHandles ) ; const gcRuntimeFactory = new ContainerRuntimeFactoryWithGC ( factory , [ [ factory . type , Promise . resolve ( factory ) ] , ] , undefined , [ innerRequestHandler ] , runtimeOptions ) ; assert ( latestAckedSummary !== undefined , "Summary ack isn't available as expected" ) ; const summarizerClient2 = await getNewSummarizer ( gcRuntimeFactory , latestAckedSummary . summaryAck . contents . handle ) ; await validateDataStoreSummaryState ( summarizerClient2 , dataStoresAsHandles ) ; await summarizerClient1 . containerRuntime . refreshLatestSummaryAck ( latestAckedSummary . summaryOp . contents . handle , latestAckedSummary . summaryAck . contents . handle , latestAckedSummary . summaryOp . referenceSequenceNumber , logger ) ; await validateDataStoreSummaryState ( summarizerClient1 , dataStoresAsHandles ) ; } ) . timeout ( 20000 ) ; } ) ;