import { strict as assert } from "assert" ; import { ContainerRuntimeFactoryWithDefaultDataStore , DataObjectFactory } from "@fluidframework/aqueduct" ; import { TelemetryNullLogger } from "@fluidframework/common-utils" ; import { IContainer } from "@fluidframework/container-definitions" ; import { ContainerRuntime , gcTreeKey , 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 { getGCStateFromSummary , loadSummarizer , TestDataObject , submitAndAckSummary } from "./mockSummarizerClient" ; /**
 * Validates that when GC is disabled on a document that had run GC previously, the GC state is removed from summary
 * and all data stores are marked as referenced.
 * This validates scenarios where due to some bug the GC state in summary is incorrect and we need to quickly recover
 * documents. Disabling GC will ensure that we are not deleting / marking things unreferenced incorrectly.
 */ describeFullCompat ( "GC state reset in summaries" , ( getTestObjectProvider ) => { let provider ; const dataObjectFactory = new DataObjectFactory ( "TestDataObject" , TestDataObject , [ ] , [ ] ) ; const defaultRuntimeOptions = { summaryOptions : { disableSummaries : true } } ; const logger = new TelemetryNullLogger ( ) ; const dataStoreAttributesBlobName = ".component" ; let latestUploadedSummary ; let latestSummaryContext ; let latestAckedSummary ; let mainContainer ; const innerRequestHandler = async ( request , runtime ) => runtime . IFluidHandleContext . resolveHandle ( request ) ; /** Creates a new container with the GC enabled / disabled as per gcAllowed param. */ const createContainer = async ( gcAllowed ? ) : Promise < IContainer > => { const runtimeFactory = new ContainerRuntimeFactoryWithDefaultDataStore ( dataObjectFactory , [ [ dataObjectFactory . type , Promise . resolve ( dataObjectFactory ) ] , ] , undefined , [ innerRequestHandler ] , { ... defaultRuntimeOptions , gcOptions : { gcAllowed , writeDataAtRoot : true } } ) ; return provider . createContainer ( runtimeFactory ) ; } ; /** Loads a summarizer client with the given version (if any). Also enables / disables GC as per disableGC param. */ const getNewSummarizer = async ( disableGC , gcAllowed ? , summaryVersion ? ) => { const runtimeFactory = new ContainerRuntimeFactoryWithDefaultDataStore ( dataObjectFactory , [ [ dataObjectFactory . type , Promise . resolve ( dataObjectFactory ) ] , ] , undefined , [ innerRequestHandler ] , { ... defaultRuntimeOptions , gcOptions : { gcAllowed , disableGC , writeDataAtRoot : true } } ) ; return loadSummarizer ( provider , runtimeFactory , mainContainer . deltaManager . lastSequenceNumber , summaryVersion ) ; } ; /**
     * 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 ; } /** Generates a summary and returns the data store channel sub-tree */ async function getSummaryChannelsTree ( summarizerClient ) { const summaryResult = await submitAndAckSummary ( provider , summarizerClient , logger , true ) ; 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" ) ; return ( latestUploadedSummary . tree [ channelsTreeName ] as ISummaryTree ) ?. tree ?? latestUploadedSummary . tree ; } /**
     * Validates that GC ran by asserting that the summary has GC blob.
     * If unreferencedDataStoreId is provided, all node entries for that data store and its children in the GC blob
     * should have unreferenced timestamp. Also, the data store's summary tree should be marked unreferenced.
     * All other nodes should be referenced and should not have unreferenced timestamp.
     */ async function validateGCRan ( summarizerClient , unreferencedDataStoreId ? ) { const channelsTree = await getSummaryChannelsTree ( summarizerClient ) ; assert ( latestUploadedSummary !== undefined , "Did not get a summary" ) ; const gcState = getGCStateFromSummary ( latestUploadedSummary ) ; for ( const [ nodeId , nodeData ] of Object . entries ( gcState . gcNodes ) ) { if ( unreferencedDataStoreId !== undefined && nodeId . startsWith ( `/${ unreferencedDataStoreId }` ) ) { assert ( nodeData . unreferencedTimestampMs !== undefined , `Node ${ nodeId } should have unreferenced timestamp` ) ; } else { assert ( nodeData . unreferencedTimestampMs === undefined , `Node ${ nodeId } shouldn't have unreferenced timestamp` ) ; } } for ( const [ id , summaryObject ] of Object . entries ( channelsTree ) ) { if ( summaryObject . type !== SummaryType . Tree || summaryObject . tree [ dataStoreAttributesBlobName ] === undefined ) { continue ; } if ( id === unreferencedDataStoreId ) { assert ( summaryObject . unreferenced === true , `DataStore ${ id } should be unreferenced` ) ; } else { assert ( summaryObject . unreferenced !== true , `DataStore ${ id } should be referenced` ) ; } } } /**
     * Validates that GC did not run by asserting that the summary does not have GC blob.
     * All data stores should be referenced.
     */ async function validateGCDidNotRun ( summarizerClient ) { const channelsTree = await getSummaryChannelsTree ( summarizerClient ) ; assert ( latestUploadedSummary !== undefined , "Did not get a summary" ) ; const gcSummaryTree = latestUploadedSummary . tree [ gcTreeKey ] ; assert ( gcSummaryTree === undefined , `GC tree should not be present in summary if GC did not run.` ) ; for ( const [ id , summaryObject ] of Object . entries ( channelsTree ) ) { if ( summaryObject . type !== SummaryType . Tree || summaryObject . tree [ dataStoreAttributesBlobName ] === undefined ) { continue ; } assert ( summaryObject . unreferenced !== true , `DataStore ${ id } should be referenced` ) ; } } before ( function ( ) { provider = getTestObjectProvider ( ) ; if ( provider . driver . type !== "local" ) { this . skip ( ) ; } } ) ; beforeEach ( async ( ) => { ( provider as any ) . _documentServiceFactory = wrapDocumentServiceFactory ( provider . documentServiceFactory , uploadSummaryCb ) ; } ) ; afterEach ( ( ) => { latestAckedSummary = undefined ; latestSummaryContext = undefined ; latestUploadedSummary = undefined ; } ) ; it ( "removes GC state and marks all objects as referenced on disabling GC" , async ( ) => { mainContainer = await createContainer ( true ) ; const mainDataStore = await requestFluidObject < TestDataObject > ( mainContainer , "default" ) ; await provider . ensureSynchronized ( ) ; const summarizerClient = await getNewSummarizer ( false ) ; const newDataStore = await dataObjectFactory . createInstance ( mainDataStore . containerRuntime ) ; mainDataStore . _root . set ( "newDataStore" , newDataStore . handle ) ; await validateGCRan ( summarizerClient ) ; mainDataStore . _root . delete ( "newDataStore" ) ; await validateGCRan ( summarizerClient , newDataStore . id ) ; assert ( latestAckedSummary !== undefined , "Summary ack isn't available as expected" ) ; const summarizerClient2 = await getNewSummarizer ( true , undefined , latestAckedSummary . summaryAck . contents . handle ) ; await validateGCDidNotRun ( summarizerClient2 ) ; } ) ; it ( "keeps GC enabled throughout the lifetime of a document" , async ( ) => { mainContainer = await createContainer ( true ) ; const mainDataStore = await requestFluidObject < TestDataObject > ( mainContainer , "default" ) ; await provider . ensureSynchronized ( ) ; const summarizerClient = await getNewSummarizer ( false , false ) ; const newDataStore = await dataObjectFactory . createInstance ( mainDataStore . containerRuntime ) ; mainDataStore . _root . set ( "newDataStore" , newDataStore . handle ) ; await validateGCRan ( summarizerClient ) ; } ) ; it ( "keeps GC disabled throughout the lifetime of a document" , async ( ) => { mainContainer = await createContainer ( false ) ; const mainDataStore = await requestFluidObject < TestDataObject > ( mainContainer , "default" ) ; await provider . ensureSynchronized ( ) ; const summarizerClient = await getNewSummarizer ( true , false ) ; const newDataStore = await dataObjectFactory . createInstance ( mainDataStore . containerRuntime ) ; mainDataStore . _root . set ( "newDataStore" , newDataStore . handle ) ; await validateGCDidNotRun ( summarizerClient ) ; } ) ; } ) ;