Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export interface ContainerRuntimeOptions {
readonly chunkSizeInBytes: number;
readonly compressionOptions: ICompressionRuntimeOptions;
readonly createBlobPayloadPending: true | undefined;
readonly disableSchemaUpgrade: boolean;
// @deprecated
readonly enableGroupedBatching: boolean;
readonly enableRuntimeIdCompressor: IdCompressorMode;
Expand Down
6 changes: 5 additions & 1 deletion packages/runtime/container-runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,11 @@
"typescript": "~5.4.5"
},
"typeValidation": {
"broken": {},
"broken": {
"Interface_ContainerRuntimeOptions": {
"forwardCompat": false
}
},
"entrypoint": "legacy"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export type RuntimeOptionsAffectingDocSchema = Omit<
| "maxBatchSizeInBytes"
| "loadSequenceNumberVerification"
| "summaryOptions"
| "disableSchemaUpgrade"
>;

/**
Expand Down
11 changes: 11 additions & 0 deletions packages/runtime/container-runtime/src/containerRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,13 @@ export interface ContainerRuntimeOptions {
* When enabled (`true`), createBlob will return a handle before the blob upload completes.
*/
readonly createBlobPayloadPending: true | undefined;

/**
* When this property is set to true, the runtime will never send DocumentSchemaChange ops
* and will throw an error if any incoming DocumentSchemaChange ops are received.
* This effectively freezes the document schema at whatever state it was in when the document was created.
*/
readonly disableSchemaUpgrade: boolean;
}

/**
Expand Down Expand Up @@ -961,6 +968,7 @@ export class ContainerRuntime
loadSequenceNumberVerification: "close",
maxBatchSizeInBytes: defaultMaxBatchSizeInBytes,
chunkSizeInBytes: defaultChunkSizeInBytes,
disableSchemaUpgrade: false,
};

const defaultConfigs = {
Expand All @@ -986,6 +994,7 @@ export class ContainerRuntime
? disabledCompressionConfig
: defaultConfigs.compressionOptions,
createBlobPayloadPending = defaultConfigs.createBlobPayloadPending,
disableSchemaUpgrade = false,
}: IContainerRuntimeOptionsInternal = runtimeOptions;

// If explicitSchemaControl is off, ensure that options which require explicitSchemaControl are not enabled.
Expand Down Expand Up @@ -1184,6 +1193,7 @@ export class ContainerRuntime
},
{ minVersionForCollab },
logger,
disableSchemaUpgrade,
);

// If the minVersionForCollab for this client is greater than the existing one, we should use that one going forward.
Expand Down Expand Up @@ -1214,6 +1224,7 @@ export class ContainerRuntime
enableGroupedBatching,
explicitSchemaControl,
createBlobPayloadPending,
disableSchemaUpgrade,
};

validateMinimumVersionForCollab(updatedMinVersionForCollab);
Expand Down
27 changes: 25 additions & 2 deletions packages/runtime/container-runtime/src/summary/documentSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,7 @@ function arrayToProp(arr: string[]): string[] | undefined {
*
* Users of this class need to use DocumentsSchemaController.sessionSchema to determine what features can be used.
*
* There are two modes this class can operate:
* There are three modes this class can operate:
* 1) Legacy mode. In such mode it does not issue any ops to change document schema. Any changes happen implicitly,
* right away, and new features are available right away
* 2) Non-legacy mode. In such mode any changes to schema require an op roundtrip. This class will manage such transitions.
Expand All @@ -523,6 +523,9 @@ function arrayToProp(arr: string[]): string[] | undefined {
* then eventually all documents that are modified will have that feature reflected in their schema. It could require
* multiple reloads / new sessions to get there (depends on if code reacts to schema changes right away, or only consults
* schema on document load).
* 3) Schema upgrade disabled mode (disableSchemaUpgrade = true). In this mode the controller will never send DocumentSchemaChange ops
* and will throw an error if any incoming schema change ops are received. The document schema is effectively frozen at whatever
* state it was in when the document was created.
*
* How schemas are changed (in non-legacy mode):
* If a client needs to change a schema, it will attempt to do so as part of normal ops sending process.
Expand Down Expand Up @@ -569,6 +572,7 @@ export class DocumentsSchemaController {
* @param onSchemaChange - callback that is called whenever schema is changed (not called on creation / load, only when processing document schema change ops)
* @param info - Informational properties of the document that are not subject to strict schema enforcement
* @param logger - telemetry logger from the runtime
* @param disableSchemaUpgrade - when true, the controller will never send or accept DocumentSchemaChange ops
*/
constructor(
existing: boolean,
Expand All @@ -578,6 +582,7 @@ export class DocumentsSchemaController {
private readonly onSchemaChange: (schema: IDocumentSchemaCurrent) => void,
info: IDocumentSchemaInfo,
logger: ITelemetryLoggerExt,
private readonly disableSchemaUpgrade: boolean,
) {
// For simplicity, let's only support new schema features for explicit schema control mode
assert(
Expand Down Expand Up @@ -704,9 +709,12 @@ export class DocumentsSchemaController {
* Called by Container runtime whenever it is about to send some op.
* It gives opportunity for controller to issue its own ops - we do not want to send ops if there are no local changes in document.
* Please consider note above constructor about race conditions - current design is to generate op only once in a session lifetime.
* @returns Optional message to send.
* @returns Optional message to send. Always returns undefined when disableSchemaUpgrade is true.
*/
public maybeGenerateSchemaMessage(): IDocumentSchemaChangeMessageOutgoing | undefined {
if (this.disableSchemaUpgrade) {
return undefined;
}
if (this.futureSchema !== undefined && !this.opPending) {
this.opPending = true;
assert(
Expand Down Expand Up @@ -739,6 +747,7 @@ export class DocumentsSchemaController {
/**
* Process document schema change messages
* Called by ContainerRuntime whenever it sees document schema messages.
* When disableSchemaUpgrade is true, an error is thrown if any incoming schema change ops are received.
* @param contents - contents of the messages
* @param local - whether op is local
* @param sequenceNumber - sequence number of the op
Expand All @@ -749,6 +758,20 @@ export class DocumentsSchemaController {
local: boolean,
sequenceNumber: number,
): boolean {
if (this.disableSchemaUpgrade) {
assert(
!local,
"local schema change messages should never be generated when disableSchemaUpgrade is enabled",
);
// Clients with disableSchemaUpgrade enabled should never generate schema change messages, but they
// may receive them from misconfigured clients. In such case, throw on any incoming schema change ops
// to prevent unexpected schema upgrades.
throw DataProcessingError.create(
"DocSchema: Received schema change op while disableSchemaUpgrade is enabled",
"processDocumentSchemaMessages",
undefined,
);
}
for (const content of contents) {
this.validateSeqNumber(content.refSeq, this.documentSchema.refSeq, "content.refSeq");
this.validateSeqNumber(this.documentSchema.refSeq, sequenceNumber, "refSeq");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1800,6 +1800,7 @@ describe("Runtime", () => {
enableGroupedBatching: true, // Redundant, but makes the JSON.stringify yield the same result as the logs
explicitSchemaControl: false,
createBlobPayloadPending: undefined,
disableSchemaUpgrade: false,
} as const satisfies ContainerRuntimeOptionsInternal;
const mergedRuntimeOptions = { ...defaultRuntimeOptions, ...runtimeOptions } as const;

Expand Down Expand Up @@ -3758,6 +3759,7 @@ describe("Runtime", () => {
enableRuntimeIdCompressor: undefined,
enableGroupedBatching: true,
explicitSchemaControl: false,
disableSchemaUpgrade: false,
};

logger.assertMatchAny([
Expand Down Expand Up @@ -3817,6 +3819,7 @@ describe("Runtime", () => {
enableRuntimeIdCompressor: undefined,
enableGroupedBatching: false,
explicitSchemaControl: false,
disableSchemaUpgrade: false,
};

logger.assertMatchAny([
Expand Down Expand Up @@ -3855,6 +3858,7 @@ describe("Runtime", () => {
enableRuntimeIdCompressor: undefined,
enableGroupedBatching: true,
explicitSchemaControl: false,
disableSchemaUpgrade: false,
};

logger.assertMatchAny([
Expand Down Expand Up @@ -3893,6 +3897,7 @@ describe("Runtime", () => {
enableRuntimeIdCompressor: undefined,
enableGroupedBatching: true,
explicitSchemaControl: true,
disableSchemaUpgrade: false,
};

logger.assertMatchAny([
Expand Down Expand Up @@ -3930,6 +3935,7 @@ describe("Runtime", () => {
enableRuntimeIdCompressor: undefined,
enableGroupedBatching: true,
explicitSchemaControl: true,
disableSchemaUpgrade: false,
};

logger.assertMatchAny([
Expand Down Expand Up @@ -3975,6 +3981,7 @@ describe("Runtime", () => {
enableRuntimeIdCompressor: "on",
enableGroupedBatching: false,
explicitSchemaControl: false,
disableSchemaUpgrade: false,
};

logger.assertMatchAny([
Expand Down Expand Up @@ -4006,6 +4013,7 @@ describe("Runtime", () => {
enableGroupedBatching: undefined,
compressionOptions: undefined,
explicitSchemaControl: undefined,
disableSchemaUpgrade: undefined,
createBlobPayloadPending: undefined,
},
},
Expand Down Expand Up @@ -4034,6 +4042,7 @@ describe("Runtime", () => {
enableRuntimeIdCompressor: undefined, // idCompressor is undefined, since that represents a logical state (off)
enableGroupedBatching: true,
explicitSchemaControl: false,
disableSchemaUpgrade: false,
};

logger.assertMatchAny([
Expand Down Expand Up @@ -4074,6 +4083,7 @@ describe("Runtime", () => {
enableRuntimeIdCompressor: undefined,
enableGroupedBatching: true,
explicitSchemaControl: true,
disableSchemaUpgrade: false,
};

logger.assertMatchAny([
Expand Down
Loading
Loading