-
Notifications
You must be signed in to change notification settings - Fork 559
(tree) Add versioning to shared tree's summary #25876
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
ab1b845
f93be5d
9af8d3f
962b4e8
ee4f0da
a4a2692
25bd9dc
eac4640
5236774
9f0a8dd
582c27f
5649e17
8311a9d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,34 +4,100 @@ | |
| */ | ||
|
|
||
| import { bufferToString } from "@fluid-internal/client-utils"; | ||
| import { assert } from "@fluidframework/core-utils/internal"; | ||
| import type { IChannelStorageService } from "@fluidframework/datastore-definitions/internal"; | ||
| import type { | ||
| IExperimentalIncrementalSummaryContext, | ||
| ISummaryTreeWithStats, | ||
| ITelemetryContext, | ||
| MinimumVersionForCollab, | ||
| } from "@fluidframework/runtime-definitions/internal"; | ||
| import { createSingleBlobSummary } from "@fluidframework/shared-object-base/internal"; | ||
| import { SummaryTreeBuilder } from "@fluidframework/runtime-utils/internal"; | ||
|
|
||
| import type { DetachedFieldIndex } from "../core/index.js"; | ||
| import type { | ||
| Summarizable, | ||
| SummaryElementParser, | ||
| SummaryElementStringifier, | ||
| } from "../shared-tree-core/index.js"; | ||
| import type { JsonCompatibleReadOnly } from "../util/index.js"; | ||
| import { | ||
| brand, | ||
| readAndParseSnapshotBlob, | ||
| type Brand, | ||
| type JsonCompatibleReadOnly, | ||
| } from "../util/index.js"; | ||
| import { FluidClientVersion } from "../codec/index.js"; | ||
|
|
||
| /** | ||
| * The storage key for the blob in the summary containing schema data | ||
| */ | ||
| const detachedFieldIndexBlobKey = "DetachedFieldIndexBlob"; | ||
|
|
||
| /** | ||
| * The storage key for the blob containing metadata for the detached field index's summary. | ||
| */ | ||
| export const detachedFieldIndexMetadataKey = ".metadata"; | ||
|
|
||
| /** | ||
| * The versions for the detached field index summary. | ||
| */ | ||
| export const DetachedFieldIndexSummaryVersion = { | ||
| /** | ||
| * Version 0 represents summaries before versioning was added. This version is not written. | ||
| * It is only used to avoid undefined checks. | ||
| */ | ||
| v0: 0, | ||
| /** | ||
| * Version 1 adds metadata to the detached field index summary. | ||
| */ | ||
| v1: 1, | ||
| /** | ||
| * The latest version of the detached field index summary. Must be updated when a new version is added. | ||
| */ | ||
| vLatest: 1, | ||
| } as const; | ||
| export type DetachedFieldIndexSummaryVersion = Brand< | ||
| (typeof DetachedFieldIndexSummaryVersion)[keyof typeof DetachedFieldIndexSummaryVersion], | ||
| "DetachedFieldIndexSummaryVersion" | ||
| >; | ||
agarwal-navin marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| /** | ||
| * The type for the metadata in the detached field index's summary. | ||
agarwal-navin marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| * Using type definition instead of interface to make this compatible with JsonCompatible. | ||
| */ | ||
| // eslint-disable-next-line @typescript-eslint/consistent-type-definitions | ||
|
||
| export type DetachedFieldIndexSummaryMetadata = { | ||
| /** The version of the detached field index summary. */ | ||
| readonly version: DetachedFieldIndexSummaryVersion; | ||
| }; | ||
|
|
||
| /** | ||
| * Returns the summary version to use as per the given minimum version for collab. | ||
| */ | ||
| function minVersionToDetachedFieldIndexSummaryVersion( | ||
| version: MinimumVersionForCollab, | ||
| ): DetachedFieldIndexSummaryVersion { | ||
| return version < FluidClientVersion.v2_73 | ||
yann-achard-MS marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| ? brand(DetachedFieldIndexSummaryVersion.v0) | ||
agarwal-navin marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| : brand(DetachedFieldIndexSummaryVersion.v1); | ||
| } | ||
|
|
||
| /** | ||
| * Provides methods for summarizing and loading a tree index. | ||
| */ | ||
| export class DetachedFieldIndexSummarizer implements Summarizable { | ||
| public readonly key = "DetachedFieldIndex"; | ||
|
|
||
| public constructor(private readonly detachedFieldIndex: DetachedFieldIndex) {} | ||
| // The summary version to write in the metadata for the detached field index summary. | ||
| private readonly summaryWriteVersion: DetachedFieldIndexSummaryVersion; | ||
|
|
||
| public constructor( | ||
| private readonly detachedFieldIndex: DetachedFieldIndex, | ||
| minVersionForCollab: MinimumVersionForCollab, | ||
| ) { | ||
| this.summaryWriteVersion = | ||
| minVersionToDetachedFieldIndexSummaryVersion(minVersionForCollab); | ||
| } | ||
|
|
||
| public summarize(props: { | ||
| stringify: SummaryElementStringifier; | ||
|
|
@@ -41,13 +107,35 @@ export class DetachedFieldIndexSummarizer implements Summarizable { | |
| incrementalSummaryContext?: IExperimentalIncrementalSummaryContext; | ||
| }): ISummaryTreeWithStats { | ||
| const data = this.detachedFieldIndex.encode(); | ||
| return createSingleBlobSummary(detachedFieldIndexBlobKey, props.stringify(data)); | ||
| const builder = new SummaryTreeBuilder(); | ||
| builder.addBlob(detachedFieldIndexBlobKey, props.stringify(data)); | ||
|
|
||
| if (this.summaryWriteVersion >= DetachedFieldIndexSummaryVersion.v1) { | ||
| const metadata: DetachedFieldIndexSummaryMetadata = { | ||
| version: this.summaryWriteVersion, | ||
| }; | ||
| builder.addBlob(detachedFieldIndexMetadataKey, JSON.stringify(metadata)); | ||
| } | ||
|
|
||
| return builder.getSummaryTree(); | ||
| } | ||
|
|
||
| public async load( | ||
| services: IChannelStorageService, | ||
| parse: SummaryElementParser, | ||
| ): Promise<void> { | ||
| if (await services.contains(detachedFieldIndexMetadataKey)) { | ||
| const metadata = await readAndParseSnapshotBlob<DetachedFieldIndexSummaryMetadata>( | ||
| detachedFieldIndexMetadataKey, | ||
| services, | ||
| parse, | ||
| ); | ||
| assert( | ||
|
||
| metadata.version === DetachedFieldIndexSummaryVersion.v1, | ||
| "Unsupported detached field index summary", | ||
| ); | ||
| } | ||
|
|
||
| if (await services.contains(detachedFieldIndexBlobKey)) { | ||
| const detachedFieldIndexBuffer = await services.readBlob(detachedFieldIndexBlobKey); | ||
| const treeBufferString = bufferToString(detachedFieldIndexBuffer, "utf8"); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,7 +3,6 @@ | |
| * Licensed under the MIT License. | ||
| */ | ||
|
|
||
| import { bufferToString } from "@fluid-internal/client-utils"; | ||
| import { assert } from "@fluidframework/core-utils/internal"; | ||
| import type { IChannelStorageService } from "@fluidframework/datastore-definitions/internal"; | ||
| import type { IIdCompressor } from "@fluidframework/id-compressor"; | ||
|
|
@@ -32,7 +31,7 @@ import type { | |
| SummaryElementParser, | ||
| SummaryElementStringifier, | ||
| } from "../../shared-tree-core/index.js"; | ||
| import { idAllocatorFromMaxId, type JsonCompatible } from "../../util/index.js"; | ||
| import { idAllocatorFromMaxId, readAndParseSnapshotBlob } from "../../util/index.js"; | ||
| // eslint-disable-next-line import-x/no-internal-modules | ||
| import { chunkFieldSingle, defaultChunkPolicy } from "../chunked-forest/chunkTree.js"; | ||
| import { | ||
|
|
@@ -46,17 +45,16 @@ import { type ForestCodec, makeForestSummarizerCodec } from "./codec.js"; | |
| import { | ||
| ForestIncrementalSummaryBehavior, | ||
| ForestIncrementalSummaryBuilder, | ||
| forestSummaryContentKey, | ||
| } from "./incrementalSummaryBuilder.js"; | ||
| import { TreeCompressionStrategyExtended } from "../treeCompressionUtils.js"; | ||
| import type { IFluidHandle } from "@fluidframework/core-interfaces"; | ||
|
|
||
| /** | ||
| * The key for the tree that contains the overall forest's summary tree. | ||
| * This tree is added by the parent of the forest summarizer. | ||
| * See {@link ForestIncrementalSummaryBuilder} for details on the summary structure. | ||
| */ | ||
| export const forestSummaryKey = "Forest"; | ||
| import { | ||
| forestSummaryContentKey, | ||
| forestSummaryKey, | ||
| forestSummaryMetadataKey, | ||
| ForestSummaryVersion, | ||
| minVersionToForestSummaryVersion, | ||
| type ForestSummaryMetadata, | ||
| } from "./summaryTypes.js"; | ||
|
|
||
| /** | ||
| * Provides methods for summarizing and loading a forest. | ||
|
|
@@ -89,6 +87,7 @@ export class ForestSummarizer implements Summarizable { | |
| (cursor: ITreeCursorSynchronous) => this.forest.chunkField(cursor), | ||
| shouldEncodeIncrementally, | ||
| initialSequenceNumber, | ||
| minVersionToForestSummaryVersion(options.minVersionForCollab), | ||
| ); | ||
| } | ||
|
|
||
|
|
@@ -164,24 +163,30 @@ export class ForestSummarizer implements Summarizable { | |
| 0xc21 /* Forest summary content missing in snapshot */, | ||
| ); | ||
|
|
||
| const readAndParseBlob = async <T extends JsonCompatible<IFluidHandle>>( | ||
| id: string, | ||
| ): Promise<T> => { | ||
| const treeBuffer = await services.readBlob(id); | ||
| const treeBufferString = bufferToString(treeBuffer, "utf8"); | ||
| return parse(treeBufferString) as T; | ||
| }; | ||
| if (await services.contains(forestSummaryMetadataKey)) { | ||
| const metadata = await readAndParseSnapshotBlob<ForestSummaryMetadata>( | ||
| forestSummaryMetadataKey, | ||
| services, | ||
| parse, | ||
| ); | ||
| assert(metadata.version === ForestSummaryVersion.v1, "Unsupported forest summary"); | ||
agarwal-navin marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // Load the incremental summary builder so that it can download any incremental chunks in the | ||
| // snapshot. | ||
| await this.incrementalSummaryBuilder.load(services, readAndParseBlob); | ||
| await this.incrementalSummaryBuilder.load(services, async (id: string) => | ||
|
||
| readAndParseSnapshotBlob(id, services, parse), | ||
| ); | ||
|
|
||
| // TODO: this code is parsing data without an optional validator, this should be defined in a typebox schema as part of the | ||
| // forest summary format. | ||
| const fields = this.codec.decode(await readAndParseBlob(forestSummaryContentKey), { | ||
| ...this.encoderContext, | ||
| incrementalEncoderDecoder: this.incrementalSummaryBuilder, | ||
| }); | ||
| const fields = this.codec.decode( | ||
| await readAndParseSnapshotBlob(forestSummaryContentKey, services, parse), | ||
| { | ||
| ...this.encoderContext, | ||
| incrementalEncoderDecoder: this.incrementalSummaryBuilder, | ||
| }, | ||
| ); | ||
| const allocator = idAllocatorFromMaxId(); | ||
| const fieldChanges: [FieldKey, DeltaFieldChanges][] = []; | ||
| const build: DeltaDetachedNodeBuild[] = []; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| /*! | ||
| * Copyright (c) Microsoft Corporation and contributors. All rights reserved. | ||
| * Licensed under the MIT License. | ||
| */ | ||
|
|
||
| import type { MinimumVersionForCollab } from "@fluidframework/runtime-definitions/internal"; | ||
| import { FluidClientVersion } from "../../codec/index.js"; | ||
| import { brand, type Brand } from "../../util/index.js"; | ||
|
|
||
| /** | ||
| * The key for the tree that contains the overall forest's summary tree. | ||
| * This tree is added by the parent of the forest summarizer. | ||
| * See {@link ForestIncrementalSummaryBuilder} for details on the summary structure. | ||
| */ | ||
| export const forestSummaryKey = "Forest"; | ||
|
|
||
| /** | ||
| * The key for the blob under ForestSummarizer's root. | ||
| * This blob contains the ForestCodec's output. | ||
| * See {@link ForestIncrementalSummaryBuilder} for details on the summary structure. | ||
| */ | ||
| export const forestSummaryContentKey = "ForestTree"; | ||
|
|
||
| /** | ||
| * The storage key for the blob containing metadata for the forest's summary. | ||
| */ | ||
| export const forestSummaryMetadataKey = ".metadata"; | ||
|
|
||
| /** | ||
| * The contents of an incremental chunk is under a summary tree node with its {@link ChunkReferenceId} as the key. | ||
| * The inline portion of the chunk content is encoded with the forest codec is stored in a blob with this key. | ||
| * The rest of the chunk contents is stored in the summary tree under the summary tree node. | ||
| * See the summary format in {@link ForestIncrementalSummaryBuilder} for more details. | ||
| */ | ||
| export const chunkContentsBlobKey = "contents"; | ||
|
|
||
| /** | ||
| * The versions for the forest summary. | ||
| */ | ||
| export const ForestSummaryVersion = { | ||
| /** | ||
| * Version 0 represents summaries before versioning was added. This version is not written. | ||
| * It is only used to avoid undefined checks. | ||
| */ | ||
| v0: 0, | ||
| /** | ||
| * Version 1 adds metadata to the forest summary. | ||
| */ | ||
| v1: 1, | ||
| /** | ||
| * The latest version of the forest summary. Must be updated when a new version is added. | ||
| */ | ||
| vLatest: 1, | ||
| } as const; | ||
| export type ForestSummaryVersion = Brand< | ||
| (typeof ForestSummaryVersion)[keyof typeof ForestSummaryVersion], | ||
| "ForestSummaryVersion" | ||
| >; | ||
|
|
||
| /** | ||
| * The type for the metadata in forest's summary. | ||
| * Using type definition instead of interface to make this compatible with JsonCompatible. | ||
| */ | ||
| // eslint-disable-next-line @typescript-eslint/consistent-type-definitions | ||
| export type ForestSummaryMetadata = { | ||
| /** The version of the forest summary. */ | ||
| readonly version: ForestSummaryVersion; | ||
| }; | ||
|
|
||
| /** | ||
| * Returns the summary version to use as per the given minimum version for collab. | ||
| */ | ||
| export function minVersionToForestSummaryVersion( | ||
| version: MinimumVersionForCollab, | ||
| ): ForestSummaryVersion { | ||
| return version < FluidClientVersion.v2_73 | ||
| ? brand(ForestSummaryVersion.v0) | ||
| : brand(ForestSummaryVersion.v1); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This starting with a "." is interesting.
If this is indented to mean something, maybe establish a pattern that "." keys are part of the top level summary not the summary items or something, it should be documented somewhere central, and this should link to it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason I used "." here is because that's the convention the runtime follows for blobs that are not actual data. For example, the metadata, alias, blobs added by the container runtime. The .attributes blob added by channel context's to a DDS's summary, etc.
However, there is no documentation for this and no real reason to do it. It's just a conventional that is followed. Not sure what a good place to add this would be. Maybe to
ISummaryTree?