@fluidframework/container-runtime 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/README.md +2 -2
- package/api-report/container-runtime.legacy.alpha.api.md +4 -3
- package/container-runtime.test-files.tar +0 -0
- package/dist/batchTracker.d.ts.map +1 -1
- package/dist/batchTracker.js.map +1 -1
- package/dist/blobManager/blobManager.d.ts.map +1 -1
- package/dist/blobManager/blobManager.js +9 -0
- package/dist/blobManager/blobManager.js.map +1 -1
- package/dist/channelCollection.d.ts +0 -14
- package/dist/channelCollection.d.ts.map +1 -1
- package/dist/channelCollection.js +2 -12
- package/dist/channelCollection.js.map +1 -1
- package/dist/containerRuntime.d.ts +34 -6
- package/dist/containerRuntime.d.ts.map +1 -1
- package/dist/containerRuntime.js +176 -81
- package/dist/containerRuntime.js.map +1 -1
- package/dist/dataStoreContext.d.ts +9 -18
- package/dist/dataStoreContext.d.ts.map +1 -1
- package/dist/dataStoreContext.js +40 -78
- package/dist/dataStoreContext.js.map +1 -1
- package/dist/gc/garbageCollection.d.ts +0 -6
- package/dist/gc/garbageCollection.d.ts.map +1 -1
- package/dist/gc/garbageCollection.js +23 -66
- package/dist/gc/garbageCollection.js.map +1 -1
- package/dist/gc/gcConfigs.d.ts.map +1 -1
- package/dist/gc/gcConfigs.js +11 -34
- package/dist/gc/gcConfigs.js.map +1 -1
- package/dist/gc/gcDefinitions.d.ts +9 -52
- package/dist/gc/gcDefinitions.d.ts.map +1 -1
- package/dist/gc/gcDefinitions.js +3 -23
- package/dist/gc/gcDefinitions.js.map +1 -1
- package/dist/gc/gcHelpers.d.ts.map +1 -1
- package/dist/gc/gcHelpers.js +2 -6
- package/dist/gc/gcHelpers.js.map +1 -1
- package/dist/gc/gcSummaryStateTracker.d.ts +1 -1
- package/dist/gc/gcSummaryStateTracker.d.ts.map +1 -1
- package/dist/gc/gcSummaryStateTracker.js +4 -8
- package/dist/gc/gcSummaryStateTracker.js.map +1 -1
- package/dist/gc/gcTelemetry.d.ts +1 -9
- package/dist/gc/gcTelemetry.d.ts.map +1 -1
- package/dist/gc/gcTelemetry.js +3 -25
- package/dist/gc/gcTelemetry.js.map +1 -1
- package/dist/gc/index.d.ts +2 -2
- package/dist/gc/index.d.ts.map +1 -1
- package/dist/gc/index.js +2 -7
- package/dist/gc/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -2
- package/dist/index.js.map +1 -1
- package/dist/messageTypes.d.ts +6 -5
- package/dist/messageTypes.d.ts.map +1 -1
- package/dist/messageTypes.js.map +1 -1
- package/dist/metadata.d.ts +9 -1
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +6 -1
- package/dist/metadata.js.map +1 -1
- package/dist/opLifecycle/index.d.ts +1 -1
- package/dist/opLifecycle/index.d.ts.map +1 -1
- package/dist/opLifecycle/index.js.map +1 -1
- package/dist/opLifecycle/opGroupingManager.d.ts +8 -0
- package/dist/opLifecycle/opGroupingManager.d.ts.map +1 -1
- package/dist/opLifecycle/opGroupingManager.js +34 -2
- package/dist/opLifecycle/opGroupingManager.js.map +1 -1
- package/dist/opLifecycle/outbox.d.ts +1 -0
- package/dist/opLifecycle/outbox.d.ts.map +1 -1
- package/dist/opLifecycle/outbox.js +20 -0
- package/dist/opLifecycle/outbox.js.map +1 -1
- package/dist/opLifecycle/remoteMessageProcessor.d.ts +34 -15
- package/dist/opLifecycle/remoteMessageProcessor.d.ts.map +1 -1
- package/dist/opLifecycle/remoteMessageProcessor.js +59 -46
- package/dist/opLifecycle/remoteMessageProcessor.js.map +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/dist/pendingStateManager.d.ts +28 -27
- package/dist/pendingStateManager.d.ts.map +1 -1
- package/dist/pendingStateManager.js +143 -112
- package/dist/pendingStateManager.js.map +1 -1
- package/dist/summary/summarizerNode/summarizerNode.d.ts.map +1 -1
- package/dist/summary/summarizerNode/summarizerNode.js +5 -1
- package/dist/summary/summarizerNode/summarizerNode.js.map +1 -1
- package/dist/summary/summarizerNode/summarizerNodeWithGc.d.ts +3 -4
- package/dist/summary/summarizerNode/summarizerNodeWithGc.d.ts.map +1 -1
- package/dist/summary/summarizerNode/summarizerNodeWithGc.js +16 -15
- package/dist/summary/summarizerNode/summarizerNodeWithGc.js.map +1 -1
- package/lib/batchTracker.d.ts.map +1 -1
- package/lib/batchTracker.js.map +1 -1
- package/lib/blobManager/blobManager.d.ts.map +1 -1
- package/lib/blobManager/blobManager.js +9 -0
- package/lib/blobManager/blobManager.js.map +1 -1
- package/lib/channelCollection.d.ts +0 -14
- package/lib/channelCollection.d.ts.map +1 -1
- package/lib/channelCollection.js +2 -11
- package/lib/channelCollection.js.map +1 -1
- package/lib/containerRuntime.d.ts +34 -6
- package/lib/containerRuntime.d.ts.map +1 -1
- package/lib/containerRuntime.js +176 -81
- package/lib/containerRuntime.js.map +1 -1
- package/lib/dataStoreContext.d.ts +9 -18
- package/lib/dataStoreContext.d.ts.map +1 -1
- package/lib/dataStoreContext.js +27 -65
- package/lib/dataStoreContext.js.map +1 -1
- package/lib/gc/garbageCollection.d.ts +0 -6
- package/lib/gc/garbageCollection.d.ts.map +1 -1
- package/lib/gc/garbageCollection.js +25 -68
- package/lib/gc/garbageCollection.js.map +1 -1
- package/lib/gc/gcConfigs.d.ts.map +1 -1
- package/lib/gc/gcConfigs.js +12 -35
- package/lib/gc/gcConfigs.js.map +1 -1
- package/lib/gc/gcDefinitions.d.ts +9 -52
- package/lib/gc/gcDefinitions.d.ts.map +1 -1
- package/lib/gc/gcDefinitions.js +2 -22
- package/lib/gc/gcDefinitions.js.map +1 -1
- package/lib/gc/gcHelpers.d.ts.map +1 -1
- package/lib/gc/gcHelpers.js +2 -6
- package/lib/gc/gcHelpers.js.map +1 -1
- package/lib/gc/gcSummaryStateTracker.d.ts +1 -1
- package/lib/gc/gcSummaryStateTracker.d.ts.map +1 -1
- package/lib/gc/gcSummaryStateTracker.js +4 -8
- package/lib/gc/gcSummaryStateTracker.js.map +1 -1
- package/lib/gc/gcTelemetry.d.ts +1 -9
- package/lib/gc/gcTelemetry.d.ts.map +1 -1
- package/lib/gc/gcTelemetry.js +3 -24
- package/lib/gc/gcTelemetry.js.map +1 -1
- package/lib/gc/index.d.ts +2 -2
- package/lib/gc/index.d.ts.map +1 -1
- package/lib/gc/index.js +2 -2
- package/lib/gc/index.js.map +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/lib/messageTypes.d.ts +6 -5
- package/lib/messageTypes.d.ts.map +1 -1
- package/lib/messageTypes.js.map +1 -1
- package/lib/metadata.d.ts +9 -1
- package/lib/metadata.d.ts.map +1 -1
- package/lib/metadata.js +4 -0
- package/lib/metadata.js.map +1 -1
- package/lib/opLifecycle/index.d.ts +1 -1
- package/lib/opLifecycle/index.d.ts.map +1 -1
- package/lib/opLifecycle/index.js.map +1 -1
- package/lib/opLifecycle/opGroupingManager.d.ts +8 -0
- package/lib/opLifecycle/opGroupingManager.d.ts.map +1 -1
- package/lib/opLifecycle/opGroupingManager.js +34 -2
- package/lib/opLifecycle/opGroupingManager.js.map +1 -1
- package/lib/opLifecycle/outbox.d.ts +1 -0
- package/lib/opLifecycle/outbox.d.ts.map +1 -1
- package/lib/opLifecycle/outbox.js +20 -0
- package/lib/opLifecycle/outbox.js.map +1 -1
- package/lib/opLifecycle/remoteMessageProcessor.d.ts +34 -15
- package/lib/opLifecycle/remoteMessageProcessor.d.ts.map +1 -1
- package/lib/opLifecycle/remoteMessageProcessor.js +59 -46
- package/lib/opLifecycle/remoteMessageProcessor.js.map +1 -1
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/lib/pendingStateManager.d.ts +28 -27
- package/lib/pendingStateManager.d.ts.map +1 -1
- package/lib/pendingStateManager.js +144 -113
- package/lib/pendingStateManager.js.map +1 -1
- package/lib/summary/summarizerNode/summarizerNode.d.ts.map +1 -1
- package/lib/summary/summarizerNode/summarizerNode.js +5 -1
- package/lib/summary/summarizerNode/summarizerNode.js.map +1 -1
- package/lib/summary/summarizerNode/summarizerNodeWithGc.d.ts +3 -4
- package/lib/summary/summarizerNode/summarizerNodeWithGc.d.ts.map +1 -1
- package/lib/summary/summarizerNode/summarizerNodeWithGc.js +16 -15
- package/lib/summary/summarizerNode/summarizerNodeWithGc.js.map +1 -1
- package/package.json +22 -31
- package/src/batchTracker.ts +4 -2
- package/src/blobManager/blobManager.ts +9 -0
- package/src/channelCollection.ts +2 -11
- package/src/containerRuntime.ts +211 -109
- package/src/dataStoreContext.ts +29 -93
- package/src/gc/garbageCollection.ts +26 -79
- package/src/gc/gcConfigs.ts +12 -45
- package/src/gc/gcDefinitions.ts +10 -55
- package/src/gc/gcHelpers.ts +10 -8
- package/src/gc/gcSummaryStateTracker.ts +6 -9
- package/src/gc/gcTelemetry.ts +3 -38
- package/src/gc/index.ts +2 -6
- package/src/index.ts +0 -1
- package/src/messageTypes.ts +12 -11
- package/src/metadata.ts +16 -2
- package/src/opLifecycle/index.ts +1 -0
- package/src/opLifecycle/opGroupingManager.ts +42 -3
- package/src/opLifecycle/outbox.ts +30 -0
- package/src/opLifecycle/remoteMessageProcessor.ts +98 -62
- package/src/packageVersion.ts +1 -1
- package/src/pendingStateManager.ts +199 -177
- package/src/summary/README.md +31 -28
- package/src/summary/summarizerNode/summarizerNode.ts +6 -1
- package/src/summary/summarizerNode/summarizerNodeWithGc.ts +20 -43
- package/src/summary/summaryFormats.md +25 -22
|
@@ -3,10 +3,8 @@
|
|
|
3
3
|
* Licensed under the MIT License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { ICriticalContainerError } from "@fluidframework/container-definitions";
|
|
7
6
|
import { IDisposable } from "@fluidframework/core-interfaces";
|
|
8
7
|
import { assert, Lazy } from "@fluidframework/core-utils/internal";
|
|
9
|
-
import { ISequencedDocumentMessage } from "@fluidframework/driver-definitions/internal";
|
|
10
8
|
import {
|
|
11
9
|
ITelemetryLoggerExt,
|
|
12
10
|
DataProcessingError,
|
|
@@ -16,10 +14,13 @@ import {
|
|
|
16
14
|
import Deque from "double-ended-queue";
|
|
17
15
|
import { v4 as uuid } from "uuid";
|
|
18
16
|
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
import {
|
|
18
|
+
type InboundContainerRuntimeMessage,
|
|
19
|
+
type InboundSequencedContainerRuntimeMessage,
|
|
20
|
+
type LocalContainerRuntimeMessage,
|
|
21
|
+
} from "./messageTypes.js";
|
|
22
|
+
import { asBatchMetadata, asEmptyBatchLocalOpMetadata } from "./metadata.js";
|
|
23
|
+
import { BatchId, BatchMessage, generateBatchId, InboundBatch } from "./opLifecycle/index.js";
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* This represents a message that has been submitted and is added to the pending queue when `submit` is called on the
|
|
@@ -34,8 +35,8 @@ export interface IPendingMessage {
|
|
|
34
35
|
localOpMetadata: unknown;
|
|
35
36
|
opMetadata: Record<string, unknown> | undefined;
|
|
36
37
|
sequenceNumber?: number;
|
|
37
|
-
/** Info
|
|
38
|
-
|
|
38
|
+
/** Info about the batch this pending message belongs to, for validation and for computing the batchId on reconnect */
|
|
39
|
+
batchInfo: {
|
|
39
40
|
/** The Batch's original clientId, from when it was first flushed to be submitted */
|
|
40
41
|
clientId: string;
|
|
41
42
|
/**
|
|
@@ -43,13 +44,15 @@ export interface IPendingMessage {
|
|
|
43
44
|
* @remarks A negative value means it was not yet submitted when queued here (e.g. disconnected right before flush fired)
|
|
44
45
|
*/
|
|
45
46
|
batchStartCsn: number;
|
|
47
|
+
/** length of the batch (how many runtime messages here) */
|
|
48
|
+
length: number;
|
|
46
49
|
};
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
type Patch<T, U> = U & Omit<T, keyof U>;
|
|
50
53
|
|
|
51
|
-
/** First version of the type (pre-dates
|
|
52
|
-
type IPendingMessageV0 = Patch<IPendingMessage, {
|
|
54
|
+
/** First version of the type (pre-dates batchInfo) */
|
|
55
|
+
type IPendingMessageV0 = Patch<IPendingMessage, { batchInfo?: undefined }>;
|
|
53
56
|
|
|
54
57
|
/**
|
|
55
58
|
* Union of all supported schemas for when applying stashed ops
|
|
@@ -74,33 +77,47 @@ export type PendingMessageResubmitData = Pick<
|
|
|
74
77
|
export interface IRuntimeStateHandler {
|
|
75
78
|
connected(): boolean;
|
|
76
79
|
clientId(): string | undefined;
|
|
77
|
-
close(error?: ICriticalContainerError): void;
|
|
78
80
|
applyStashedOp(content: string): Promise<unknown>;
|
|
79
81
|
reSubmitBatch(batch: PendingMessageResubmitData[], batchId: BatchId): void;
|
|
80
82
|
isActiveConnection: () => boolean;
|
|
81
83
|
isAttached: () => boolean;
|
|
82
84
|
}
|
|
83
85
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
* narrowing.
|
|
89
|
-
*/
|
|
90
|
-
type AnyComboFromUnion<T extends object> = { [P in KeysOfUnion<T>]?: T[P] };
|
|
86
|
+
function isEmptyBatchPendingMessage(message: IPendingMessageFromStash): boolean {
|
|
87
|
+
const content = JSON.parse(message.content);
|
|
88
|
+
return content.type === "groupedBatch" && content.contents?.length === 0;
|
|
89
|
+
}
|
|
91
90
|
|
|
92
|
-
function buildPendingMessageContent(
|
|
93
|
-
// AnyComboFromUnion is needed need to gain access to compatDetails that
|
|
94
|
-
// is only defined for some cases.
|
|
95
|
-
message: AnyComboFromUnion<InboundSequencedContainerRuntimeMessage>,
|
|
96
|
-
): string {
|
|
91
|
+
function buildPendingMessageContent(message: InboundSequencedContainerRuntimeMessage): string {
|
|
97
92
|
// IMPORTANT: Order matters here, this must match the order of the properties used
|
|
98
93
|
// when submitting the message.
|
|
99
|
-
const { type, contents, compatDetails } = message;
|
|
94
|
+
const { type, contents, compatDetails }: InboundContainerRuntimeMessage = message;
|
|
100
95
|
// Any properties that are not defined, won't be emitted by stringify.
|
|
101
96
|
return JSON.stringify({ type, contents, compatDetails });
|
|
102
97
|
}
|
|
103
98
|
|
|
99
|
+
function typesOfKeys<T extends object>(obj: T): Record<keyof T, string> {
|
|
100
|
+
return Object.keys(obj).reduce((acc, key) => {
|
|
101
|
+
acc[key] = typeof obj[key];
|
|
102
|
+
return acc;
|
|
103
|
+
}, {}) as Record<keyof T, string>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function scrubAndStringify(
|
|
107
|
+
message: InboundContainerRuntimeMessage | LocalContainerRuntimeMessage,
|
|
108
|
+
): string {
|
|
109
|
+
// Scrub the whole object in case there are unexpected keys
|
|
110
|
+
const scrubbed: Record<string, unknown> = typesOfKeys(message);
|
|
111
|
+
|
|
112
|
+
// For these known/expected keys, we can either drill in (for contents)
|
|
113
|
+
// or just use the value as-is (since it's not personal info)
|
|
114
|
+
scrubbed.contents = message.contents && typesOfKeys(message.contents);
|
|
115
|
+
scrubbed.compatDetails = message.compatDetails;
|
|
116
|
+
scrubbed.type = message.type;
|
|
117
|
+
|
|
118
|
+
return JSON.stringify(scrubbed);
|
|
119
|
+
}
|
|
120
|
+
|
|
104
121
|
function withoutLocalOpMetadata(message: IPendingMessage): IPendingMessage {
|
|
105
122
|
return {
|
|
106
123
|
...message,
|
|
@@ -108,6 +125,20 @@ function withoutLocalOpMetadata(message: IPendingMessage): IPendingMessage {
|
|
|
108
125
|
};
|
|
109
126
|
}
|
|
110
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Get the effective batch ID for a pending message.
|
|
130
|
+
* If the batch ID is already present in the message's op metadata, return it.
|
|
131
|
+
* Otherwise, generate a new batch ID using the client ID and batch start CSN.
|
|
132
|
+
* @param pendingMessage - The pending message
|
|
133
|
+
* @returns The effective batch ID
|
|
134
|
+
*/
|
|
135
|
+
function getEffectiveBatchId(pendingMessage: IPendingMessage): string {
|
|
136
|
+
return (
|
|
137
|
+
asBatchMetadata(pendingMessage.opMetadata)?.batchId ??
|
|
138
|
+
generateBatchId(pendingMessage.batchInfo.clientId, pendingMessage.batchInfo.batchStartCsn)
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
111
142
|
/**
|
|
112
143
|
* PendingStateManager is responsible for maintaining the messages that have not been sent or have not yet been
|
|
113
144
|
* acknowledged by the server. It also maintains the batch information for both automatically and manually flushed
|
|
@@ -136,13 +167,6 @@ export class PendingStateManager implements IDisposable {
|
|
|
136
167
|
this.pendingMessages.clear();
|
|
137
168
|
});
|
|
138
169
|
|
|
139
|
-
// Indicates whether we are processing a batch.
|
|
140
|
-
private isProcessingBatch: boolean = false;
|
|
141
|
-
|
|
142
|
-
// This stores the first message in the batch that we are processing. This is used to verify that we get
|
|
143
|
-
// the correct batch metadata.
|
|
144
|
-
private pendingBatchBeginMessage: ISequencedDocumentMessage | undefined;
|
|
145
|
-
|
|
146
170
|
/** Used to ensure we don't replay ops on the same connection twice */
|
|
147
171
|
private clientIdFromLastReplay: string | undefined;
|
|
148
172
|
|
|
@@ -248,8 +272,8 @@ export class PendingStateManager implements IDisposable {
|
|
|
248
272
|
content,
|
|
249
273
|
localOpMetadata,
|
|
250
274
|
opMetadata,
|
|
251
|
-
// Note: We only
|
|
252
|
-
|
|
275
|
+
// Note: We only will read this off the first message, but put it on all for simplicity
|
|
276
|
+
batchInfo: { clientId, batchStartCsn, length: batch.length },
|
|
253
277
|
};
|
|
254
278
|
this.pendingMessages.push(pendingMessage);
|
|
255
279
|
}
|
|
@@ -274,7 +298,15 @@ export class PendingStateManager implements IDisposable {
|
|
|
274
298
|
}
|
|
275
299
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
276
300
|
const nextMessage = this.initialMessages.shift()!;
|
|
301
|
+
// Nothing to apply if the message is an empty batch.
|
|
302
|
+
// We still need to track it for resubmission.
|
|
277
303
|
try {
|
|
304
|
+
if (isEmptyBatchPendingMessage(nextMessage)) {
|
|
305
|
+
nextMessage.localOpMetadata = { emptyBatch: true }; // equivalent to applyStashedOp for empty batch
|
|
306
|
+
patchbatchInfo(nextMessage); // Back compat
|
|
307
|
+
this.pendingMessages.push(nextMessage);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
278
310
|
// applyStashedOp will cause the DDS to behave as if it has sent the op but not actually send it
|
|
279
311
|
const localOpMetadata = await this.stateHandler.applyStashedOp(nextMessage.content);
|
|
280
312
|
if (!this.stateHandler.isAttached()) {
|
|
@@ -284,7 +316,7 @@ export class PendingStateManager implements IDisposable {
|
|
|
284
316
|
} else {
|
|
285
317
|
nextMessage.localOpMetadata = localOpMetadata;
|
|
286
318
|
// then we push onto pendingMessages which will cause PendingStateManager to resubmit when we connect
|
|
287
|
-
|
|
319
|
+
patchbatchInfo(nextMessage); // Back compat
|
|
288
320
|
this.pendingMessages.push(nextMessage);
|
|
289
321
|
}
|
|
290
322
|
} catch (error) {
|
|
@@ -294,172 +326,164 @@ export class PendingStateManager implements IDisposable {
|
|
|
294
326
|
}
|
|
295
327
|
|
|
296
328
|
/**
|
|
297
|
-
* Processes
|
|
298
|
-
*
|
|
299
|
-
* @param batch - The batch
|
|
300
|
-
* @param
|
|
329
|
+
* Processes an inbound batch of messages - May be local or remote.
|
|
330
|
+
*
|
|
331
|
+
* @param batch - The inbound batch of messages to process. Could be local or remote.
|
|
332
|
+
* @param local - true if we submitted this batch and expect corresponding pending messages
|
|
333
|
+
* @returns The inbound batch's messages with localOpMetadata "zipped" in.
|
|
334
|
+
*
|
|
335
|
+
* @remarks Closes the container if:
|
|
336
|
+
* - The batchStartCsn doesn't match for local batches
|
|
301
337
|
*/
|
|
302
|
-
public
|
|
303
|
-
batch:
|
|
304
|
-
|
|
338
|
+
public processInboundBatch(
|
|
339
|
+
batch: InboundBatch,
|
|
340
|
+
local: boolean,
|
|
305
341
|
): {
|
|
342
|
+
message: InboundSequencedContainerRuntimeMessage;
|
|
343
|
+
localOpMetadata?: unknown;
|
|
344
|
+
}[] {
|
|
345
|
+
if (local) {
|
|
346
|
+
return this.processPendingLocalBatch(batch);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// No localOpMetadata for remote messages
|
|
350
|
+
return batch.messages.map((message) => ({ message }));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Processes the incoming batch from the server that was submitted by this client.
|
|
355
|
+
* It verifies that messages are received in the right order and that the batch information is correct.
|
|
356
|
+
* @param batch - The inbound batch (originating from this client) to correlate with the pending local state
|
|
357
|
+
* @returns The inbound batch's messages with localOpMetadata "zipped" in.
|
|
358
|
+
*/
|
|
359
|
+
private processPendingLocalBatch(batch: InboundBatch): {
|
|
306
360
|
message: InboundSequencedContainerRuntimeMessage;
|
|
307
361
|
localOpMetadata: unknown;
|
|
308
362
|
}[] {
|
|
309
|
-
|
|
363
|
+
this.onLocalBatchBegin(batch);
|
|
364
|
+
|
|
365
|
+
// Empty batch
|
|
366
|
+
if (batch.messages.length === 0) {
|
|
367
|
+
assert(
|
|
368
|
+
batch.emptyBatchSequenceNumber !== undefined,
|
|
369
|
+
0x9fb /* Expected sequence number for empty batch */,
|
|
370
|
+
);
|
|
371
|
+
const localOpMetadata = this.processNextPendingMessage(batch.emptyBatchSequenceNumber);
|
|
372
|
+
assert(
|
|
373
|
+
asEmptyBatchLocalOpMetadata(localOpMetadata)?.emptyBatch === true,
|
|
374
|
+
0xa20 /* Expected empty batch marker */,
|
|
375
|
+
);
|
|
376
|
+
return [];
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return batch.messages.map((message) => ({
|
|
310
380
|
message,
|
|
311
|
-
localOpMetadata: this.
|
|
381
|
+
localOpMetadata: this.processNextPendingMessage(message.sequenceNumber, message),
|
|
312
382
|
}));
|
|
313
383
|
}
|
|
314
384
|
|
|
315
385
|
/**
|
|
316
|
-
* Processes
|
|
317
|
-
* the
|
|
318
|
-
* @param message - The message
|
|
319
|
-
* @
|
|
320
|
-
*
|
|
386
|
+
* Processes the pending local copy of message that's been ack'd by the server.
|
|
387
|
+
* @param sequenceNumber - The sequenceNumber from the server corresponding to the next pending message.
|
|
388
|
+
* @param message - [optional] The entire incoming message, for comparing contents with the pending message for extra validation.
|
|
389
|
+
* @throws DataProcessingError if the pending message content doesn't match the incoming message content.
|
|
390
|
+
* @returns - The localOpMetadata of the next pending message, to be sent to whoever submitted the original message.
|
|
321
391
|
*/
|
|
322
|
-
private
|
|
323
|
-
|
|
324
|
-
|
|
392
|
+
private processNextPendingMessage(
|
|
393
|
+
sequenceNumber: number,
|
|
394
|
+
message?: InboundSequencedContainerRuntimeMessage,
|
|
325
395
|
): unknown {
|
|
326
|
-
// Get the next message from the pending queue. Verify a message exists.
|
|
327
396
|
const pendingMessage = this.pendingMessages.peekFront();
|
|
328
397
|
assert(
|
|
329
398
|
pendingMessage !== undefined,
|
|
330
399
|
0x169 /* "No pending message found for this remote message" */,
|
|
331
400
|
);
|
|
332
401
|
|
|
333
|
-
|
|
334
|
-
this.maybeProcessBatchBegin(message, batchStartCsn, pendingMessage);
|
|
335
|
-
|
|
336
|
-
pendingMessage.sequenceNumber = message.sequenceNumber;
|
|
402
|
+
pendingMessage.sequenceNumber = sequenceNumber;
|
|
337
403
|
this.savedOps.push(withoutLocalOpMetadata(pendingMessage));
|
|
338
404
|
|
|
339
405
|
this.pendingMessages.shift();
|
|
340
406
|
|
|
341
|
-
|
|
407
|
+
// message is undefined in the Empty Batch case,
|
|
408
|
+
// because we don't have an incoming message to compare and pendingMessage is just a placeholder anyway.
|
|
409
|
+
if (message !== undefined) {
|
|
410
|
+
const messageContent = buildPendingMessageContent(message);
|
|
411
|
+
|
|
412
|
+
// Stringified content should match
|
|
413
|
+
if (pendingMessage.content !== messageContent) {
|
|
414
|
+
const pendingContentObj = JSON.parse(
|
|
415
|
+
pendingMessage.content,
|
|
416
|
+
) as LocalContainerRuntimeMessage;
|
|
417
|
+
const incomingContentObj = JSON.parse(
|
|
418
|
+
messageContent,
|
|
419
|
+
) as InboundContainerRuntimeMessage;
|
|
420
|
+
|
|
421
|
+
const contentsMatch =
|
|
422
|
+
pendingContentObj.contents === incomingContentObj.contents ||
|
|
423
|
+
(pendingContentObj.contents !== undefined &&
|
|
424
|
+
incomingContentObj.contents !== undefined &&
|
|
425
|
+
JSON.stringify(pendingContentObj.contents) ===
|
|
426
|
+
JSON.stringify(incomingContentObj.contents));
|
|
427
|
+
|
|
428
|
+
this.logger.sendErrorEvent({
|
|
429
|
+
eventName: "unexpectedAckReceived",
|
|
430
|
+
details: {
|
|
431
|
+
pendingContentScrubbed: scrubAndStringify(pendingContentObj),
|
|
432
|
+
incomingContentScrubbed: scrubAndStringify(incomingContentObj),
|
|
433
|
+
contentsMatch,
|
|
434
|
+
},
|
|
435
|
+
});
|
|
342
436
|
|
|
343
|
-
|
|
344
|
-
if (pendingMessage.content !== messageContent) {
|
|
345
|
-
this.stateHandler.close(
|
|
346
|
-
DataProcessingError.create(
|
|
437
|
+
throw DataProcessingError.create(
|
|
347
438
|
"pending local message content mismatch",
|
|
348
439
|
"unexpectedAckReceived",
|
|
349
440
|
message,
|
|
350
|
-
|
|
351
|
-
expectedMessageType: JSON.parse(pendingMessage.content).type,
|
|
352
|
-
},
|
|
353
|
-
),
|
|
354
|
-
);
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// Post-processing part - If we are processing a batch then this could be the last message in the batch.
|
|
359
|
-
this.maybeProcessBatchEnd(message);
|
|
360
|
-
|
|
361
|
-
return pendingMessage.localOpMetadata;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/**
|
|
365
|
-
* This message could be the first message in batch. If so, set batch state marking the beginning of a batch.
|
|
366
|
-
* @param message - The message that is being processed.
|
|
367
|
-
* @param batchStartCsn - The clientSequenceNumber of the start of this message's batch (assigned during submit)
|
|
368
|
-
* @param pendingMessage - The corresponding pendingMessage.
|
|
369
|
-
*/
|
|
370
|
-
private maybeProcessBatchBegin(
|
|
371
|
-
message: ISequencedDocumentMessage,
|
|
372
|
-
batchStartCsn: number,
|
|
373
|
-
pendingMessage: IPendingMessage,
|
|
374
|
-
) {
|
|
375
|
-
if (!this.isProcessingBatch) {
|
|
376
|
-
// Expecting the start of a batch (maybe single-message).
|
|
377
|
-
if (pendingMessage.batchIdContext.batchStartCsn !== batchStartCsn) {
|
|
378
|
-
this.logger?.sendErrorEvent({
|
|
379
|
-
eventName: "BatchClientSequenceNumberMismatch",
|
|
380
|
-
details: {
|
|
381
|
-
processingBatch: !!this.pendingBatchBeginMessage,
|
|
382
|
-
pendingBatchCsn: pendingMessage.batchIdContext.batchStartCsn,
|
|
383
|
-
batchStartCsn,
|
|
384
|
-
messageBatchMetadata: (message.metadata as any)?.batch,
|
|
385
|
-
pendingMessageBatchMetadata: (pendingMessage.opMetadata as any)?.batch,
|
|
386
|
-
},
|
|
387
|
-
messageDetails: extractSafePropertiesFromMessage(message),
|
|
388
|
-
});
|
|
441
|
+
);
|
|
389
442
|
}
|
|
390
443
|
}
|
|
391
444
|
|
|
392
|
-
|
|
393
|
-
if ((message.metadata as IBatchMetadata | undefined)?.batch) {
|
|
394
|
-
// We should not already be processing a batch and there should be no pending batch begin message.
|
|
395
|
-
assert(
|
|
396
|
-
!this.isProcessingBatch && this.pendingBatchBeginMessage === undefined,
|
|
397
|
-
0x16b /* "The pending batch state indicates we are already processing a batch" */,
|
|
398
|
-
);
|
|
399
|
-
|
|
400
|
-
// Set the pending batch state indicating we have started processing a batch.
|
|
401
|
-
this.pendingBatchBeginMessage = message;
|
|
402
|
-
this.isProcessingBatch = true;
|
|
403
|
-
}
|
|
445
|
+
return pendingMessage.localOpMetadata;
|
|
404
446
|
}
|
|
405
447
|
|
|
406
448
|
/**
|
|
407
|
-
*
|
|
408
|
-
* @param message - The message that is being processed.
|
|
449
|
+
* Check if the incoming batch matches the batch info for the next pending message.
|
|
409
450
|
*/
|
|
410
|
-
private
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// There should be a pending batch begin message.
|
|
451
|
+
private onLocalBatchBegin(batch: InboundBatch) {
|
|
452
|
+
// Get the next message from the pending queue. Verify a message exists.
|
|
453
|
+
const pendingMessage = this.pendingMessages.peekFront();
|
|
416
454
|
assert(
|
|
417
|
-
|
|
418
|
-
|
|
455
|
+
pendingMessage !== undefined,
|
|
456
|
+
0xa21 /* No pending message found as we start processing this remote batch */,
|
|
419
457
|
);
|
|
420
458
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
clientId: this.stateHandler.clientId(),
|
|
450
|
-
hasBatchStart: batchBeginMetadata === true,
|
|
451
|
-
hasBatchEnd: batchEndMetadata === false,
|
|
452
|
-
messageType: message.type,
|
|
453
|
-
pendingMessagesCount: this.pendingMessagesCount,
|
|
454
|
-
},
|
|
455
|
-
),
|
|
456
|
-
);
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// Clear the pending batch state now that we have processed the entire batch.
|
|
461
|
-
this.pendingBatchBeginMessage = undefined;
|
|
462
|
-
this.isProcessingBatch = false;
|
|
459
|
+
// Note: This could be undefined if this batch became empty on resubmit.
|
|
460
|
+
// In this case the next pending message is an empty batch marker.
|
|
461
|
+
// Empty batches became empty on Resubmit, and submit them and track them in case
|
|
462
|
+
// a different fork of this container also submitted the same batch (and it may not be empty for that fork).
|
|
463
|
+
const firstMessage = batch.messages.length > 0 ? batch.messages[0] : undefined;
|
|
464
|
+
const expectedPendingBatchLength = batch.messages.length === 0 ? 1 : batch.messages.length;
|
|
465
|
+
|
|
466
|
+
// We expect the incoming batch to be of the same length, starting at the same clientSequenceNumber,
|
|
467
|
+
// as the batch we originally submitted.
|
|
468
|
+
// We have another later check to compare the message contents, which we'd expect to fail if this check does,
|
|
469
|
+
// so we don't throw here, merely log. In a later release this check may replace that one.
|
|
470
|
+
if (
|
|
471
|
+
pendingMessage.batchInfo.batchStartCsn !== batch.batchStartCsn ||
|
|
472
|
+
(pendingMessage.batchInfo.length >= 0 && // -1 length is back compat and isn't suitable for this check
|
|
473
|
+
pendingMessage.batchInfo.length !== expectedPendingBatchLength)
|
|
474
|
+
) {
|
|
475
|
+
this.logger?.sendErrorEvent({
|
|
476
|
+
eventName: "BatchInfoMismatch",
|
|
477
|
+
details: {
|
|
478
|
+
pendingBatchCsn: pendingMessage.batchInfo.batchStartCsn,
|
|
479
|
+
batchStartCsn: batch.batchStartCsn,
|
|
480
|
+
pendingBatchLength: pendingMessage.batchInfo.length,
|
|
481
|
+
batchLength: batch.messages.length,
|
|
482
|
+
pendingMessageBatchMetadata: asBatchMetadata(pendingMessage.opMetadata)?.batch,
|
|
483
|
+
messageBatchMetadata: asBatchMetadata(firstMessage?.metadata)?.batch,
|
|
484
|
+
},
|
|
485
|
+
messageDetails: firstMessage && extractSafePropertiesFromMessage(firstMessage),
|
|
486
|
+
});
|
|
463
487
|
}
|
|
464
488
|
}
|
|
465
489
|
|
|
@@ -501,14 +525,12 @@ export class PendingStateManager implements IDisposable {
|
|
|
501
525
|
assert(batchMetadataFlag !== false, 0x41b /* We cannot process batches in chunks */);
|
|
502
526
|
|
|
503
527
|
// The next message starts a batch (possibly single-message), and we'll need its batchId.
|
|
504
|
-
|
|
505
|
-
//
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
pendingMessage.batchIdContext.batchStartCsn,
|
|
511
|
-
);
|
|
528
|
+
const batchId = getEffectiveBatchId(pendingMessage);
|
|
529
|
+
// Resubmit no messages, with the batchId. Will result in another empty batch marker.
|
|
530
|
+
if (asEmptyBatchLocalOpMetadata(pendingMessage.localOpMetadata)?.emptyBatch === true) {
|
|
531
|
+
this.stateHandler.reSubmitBatch([], batchId);
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
512
534
|
|
|
513
535
|
/**
|
|
514
536
|
* We must preserve the distinct batches on resubmit.
|
|
@@ -580,13 +602,13 @@ export class PendingStateManager implements IDisposable {
|
|
|
580
602
|
}
|
|
581
603
|
}
|
|
582
604
|
|
|
583
|
-
/** For back-compat if trying to apply stashed ops that pre-date
|
|
584
|
-
function
|
|
605
|
+
/** For back-compat if trying to apply stashed ops that pre-date batchInfo */
|
|
606
|
+
function patchbatchInfo(
|
|
585
607
|
message: IPendingMessageFromStash,
|
|
586
608
|
): asserts message is IPendingMessage {
|
|
587
|
-
const
|
|
588
|
-
if (
|
|
609
|
+
const batchInfo: IPendingMessageFromStash["batchInfo"] = message.batchInfo;
|
|
610
|
+
if (batchInfo === undefined) {
|
|
589
611
|
// Using uuid guarantees uniqueness, retaining existing behavior
|
|
590
|
-
message.
|
|
612
|
+
message.batchInfo = { clientId: uuid(), batchStartCsn: -1, length: -1 };
|
|
591
613
|
}
|
|
592
614
|
}
|
package/src/summary/README.md
CHANGED
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
- [Summary Lifecycle](#summary-lifecycle)
|
|
10
10
|
- [Single-commit vs two-commit summaries](#single-commit-vs-two-commit-summaries)
|
|
11
11
|
- [Incremental summaries](#incremental-summaries)
|
|
12
|
-
|
|
12
|
+
- [Resiliency](#resiliency)
|
|
13
13
|
- [What does a summary look like?](#what-does-a-summary-look-like)
|
|
14
14
|
|
|
15
15
|
## Introduction
|
|
16
16
|
|
|
17
|
-
This document provides a conceptual overview of summarization
|
|
17
|
+
This document provides a conceptual overview of summarization. It describes what summaries are, how / when they are generated, and what they look like. The goal is for this to be an entry point into summarization for users and developers alike.
|
|
18
18
|
|
|
19
19
|
### Summary vs snapshot
|
|
20
20
|
|
|
@@ -22,23 +22,22 @@ The terms summary and snapshot are sometimes used interchangeably. Both represen
|
|
|
22
22
|
|
|
23
23
|
## Why do we need summaries?
|
|
24
24
|
|
|
25
|
-
A 'summary' captures the state of a container at a point in time. Without it, a client would have to apply every operation in the op log, even if those operations no longer affected the current state (e.g. op 1 inserts
|
|
26
|
-
Instead, when a client opens a collaborative document, it
|
|
25
|
+
A 'summary' captures the state of a container at a point in time so that future clients can start from this point. Without it, a client would have to apply every operation in the op log, even if those operations (hereafter ops) no longer affected the current state (e.g. op 1 inserts 'h' and op 2 deletes 'h'). For large op logs, this would be very expensive for clients to both download from the service and to process them.
|
|
26
|
+
Instead, when a client opens a collaborative document, it downloads the latest snapshot of the container, and simply process new operations from that point forward.
|
|
27
27
|
|
|
28
28
|
## Who generates summaries?
|
|
29
29
|
|
|
30
|
-
Summaries can be generated by any client connected in "write" mode.
|
|
30
|
+
Summaries can be generated by any client connected in "write" mode. They are generated by a separate non-interactive client called the summarizer client. Using a separate client is an optimization - this client doesn't have to take local changes into account which can make the summary process more complicated.
|
|
31
|
+
A summarizer client is like any other client connected to the document except that users cannot interact with this client, and it only works on the state it receives from other clients. It has a brand-new container with its own connection to services.
|
|
32
|
+
All the clients connected to the document participate in a process called "summary client election" to elect a "parent summarizer" client. Typically, it's the oldest "write" client connected to the document. The parent summarizer client spawns a "summarizer" client which is responsible for summarization.
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
- A summarizer client is like any other client connected to the document except that users cannot interact with this client, and it only works on the state it receives from other clients. It has a brand-new container with its own connection to services.
|
|
34
|
-
|
|
35
|
-
> Note that if the summarizer client closes, the "summary client election" process will choose a new one, if applicable. The default "summary client election" algorithm is to select the oldest "write" client as the parent summarizer client which in turn will create the summarizer client.
|
|
34
|
+
Note: If the summarizer client closes, the "summary client election" process will choose a new one, if there are eligible clients.
|
|
36
35
|
|
|
37
36
|
## When are summaries generated?
|
|
38
37
|
|
|
39
|
-
The summarizer client periodically generates summary based on heuristics calculated based on configurations such as the number of user or system
|
|
38
|
+
The summarizer client periodically generates summary based on heuristics calculated based on configurations such as the number of user or system operations received, the amount of time a client has been idle (hasn't received any ops), the maximum time since last summary, maximum number of ops since last summary, etc. The heuristic configurations are defined by an `ISummaryConfigurationHeuristics` interface defined in [containerRuntime.ts in the container-runtime package][container-runtime].
|
|
40
39
|
|
|
41
|
-
The summarizer client uses a default set of configurations defined by
|
|
40
|
+
The summarizer client uses a default set of configurations defined by `DefaultSummaryConfiguration` in [containerRuntime.ts in the container-runtime package][container-runtime]. These can be overridden by providing a new set of configurations as part of container runtime options during creation.
|
|
42
41
|
|
|
43
42
|
## How are summaries generated?
|
|
44
43
|
|
|
@@ -47,35 +46,39 @@ When summarization process is triggered, every object in the container's object
|
|
|
47
46
|
### Summary Lifecycle
|
|
48
47
|
|
|
49
48
|
The lifecycle of a summary starts when a "parent summarizer" client is elected.
|
|
49
|
+
|
|
50
50
|
- The parent summarizer spawns a non-interactive summarizer client.
|
|
51
51
|
- The summarizer client periodically starts a summary as per heuristics. A summary happens at a particular sequence number called the "summary sequence number" or reference sequence number for the summary.
|
|
52
|
-
- The container runtime (
|
|
53
|
-
- The runtime uploads the summary tree to the Fluid storage service which returns a handle (unique id) to the summary if the upload is successful. Otherwise, it returns a failure.
|
|
52
|
+
- The container runtime (hereafter runtime) generates a summary tree (described in the ["What does a summary look like?"](#what-does-a-summary-look-like) section below).
|
|
53
|
+
- The runtime uploads the summary tree to the Fluid storage service which returns a handle (unique id) to the summary if the upload is successful. Otherwise, it returns a failure. The runtime also includes the handle of the last successful summary. If this information is incorrect, the service will reject this summary. This is done to ensure that [incremental summaries](#incremental-summaries) are correct.
|
|
54
54
|
- The runtime submits a "summarize" op to the Fluid ordering service containing the uploaded summary handle and the summary sequence number.
|
|
55
|
-
- The ordering service stamps it with a sequence number (like any other op) and broadcasts the summarize op.
|
|
56
|
-
-
|
|
57
|
-
- If the summary is accepted, it sends a "summary ack" with the summary sequence number and a summary handle.
|
|
58
|
-
- If the summary is rejected, it sends a "summary nack" with the details of the summary op
|
|
59
|
-
- The runtime
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
By default, Fluid uses "two-commit summaries" mode where the two commits refer to the storage committing the summary twice and returning two different handles for it - One when the summary is uploaded and second, on responding to the summary op via a summary ack. In this mode, when the server receives the summary op, it augments the corresponding summary with a "protocol" blob hence generating a new commit and new handle for this summary which it returns in the summary ack.
|
|
64
|
-
|
|
65
|
-
Fluid is switching to "single-commit summary" mode where the client adds the "protocol" blob when uploading the summary. Thus, the server doesn't need to augment the summary and the summary ack is no longer required. As soon as the summary is uploaded (first commit), the summary process is complete. The "summarize" op then is just a way to indicate that a summary happened, and it has details of the summary
|
|
55
|
+
- The ordering service stamps it with a sequence number (like any other op) and broadcasts the summarize op. This creates a record in the op log that a summary was submitted and it lets other clients know about it. Non-summarizer clients don't do anything with the summary op. The summarizer client that submitted it processes it and waits for a summary ack / nack. Future summarizer clients also process them and validates that a corresponding summary ack / nack is received.
|
|
56
|
+
- The ordering service then responds to the summarize op:
|
|
57
|
+
- If the summary is accepted, it sends a "summary ack" with the summary sequence number and a summary handle.
|
|
58
|
+
- If the summary is rejected, it sends a "summary nack" with the details of the summary op.
|
|
59
|
+
- The runtime processes the summary ack or nack completes the summary process as success or failure accordingly.
|
|
60
|
+
- If the summary is successful, the handle in the ack becomes the last successful summary's handle which is used when upload summaries as described earlier.
|
|
61
|
+
- If the summary failed, the summarizer client closes and the summary election process starts to elect a new one.
|
|
62
|
+
- The runtime has a timeout called "maxAckWaitTime" and if the summary op, ack or nack is not received within this time, it will fail this summary.
|
|
66
63
|
|
|
67
64
|
### Incremental summaries
|
|
68
65
|
|
|
69
|
-
Summaries are incremental, i.e., if an object (or node) did not change since the last summary, it doesn't have to re-summarize its entire contents. Fluid supports the concept of a summary handle defined in [
|
|
66
|
+
Summaries are incremental, i.e., if an object (or node) did not change since the last summary, it doesn't have to re-summarize its entire contents. Fluid supports the concept of a summary handle defined in [summary.ts in the protocol-definitions package][summary-protocol]. A handle is a path to a subtree in a snapshot and it allows objects to reference a subtree in the previous snapshot, which is essentially an instruction to storage to find that subtree and populate into new summary.
|
|
67
|
+
|
|
68
|
+
So, say that a data store or DDS did not change since the last summary, it doesn't have to go through the whole summary process described above. It can instead return an ISummaryHandle with path to its subtree in the last successful summary. The same applies to other types of content like a single content blob within an object's summary tree.
|
|
70
69
|
|
|
71
|
-
|
|
70
|
+
For incremental summary, objects diff their content against the last summary to determine whether to send a summary handle. So, it's crucial that the last summary information be correct or else the summary will be incorrect. So, during upload, the last summary's handle is also sent and the service will validate that it's correct.
|
|
72
71
|
|
|
73
72
|
### Resiliency
|
|
74
73
|
|
|
75
|
-
The summarization process is designed to be resilient -
|
|
74
|
+
The summarization process is designed to be resilient - A document will eventually summarize and make progress even if there are intermittent failures or disruptions. Some examples of steps taken to achieve this:
|
|
75
|
+
|
|
76
76
|
- Last summary - Usually, if the "parent summarizer" client disconnects or shuts down, the "summarizer" client also shuts down and the summarizer election process begins. However, if there a certain number of un-summarized ops, the summarizer client will perform a "last summary" even if the parent shuts down. This is done to make progress in scenarios where new summarizer clients are closed quickly because the parent summarizer keeps disconnecting repeatedly.
|
|
77
77
|
- Retries - The summarizer has a retry mechanism which can identify certain types of intermittent failures either in the client or in the server. It will retry the summary attempt for these failures a certain number of times. This helps in cases where there are intermittent failures such as throttling errors from the server which goes away after waiting for a while.
|
|
78
78
|
|
|
79
79
|
## What does a summary look like?
|
|
80
80
|
|
|
81
|
-
The format of
|
|
81
|
+
The format of summaries (and snapshots) is described in [summary and snapshot formats](./summaryFormats.md).
|
|
82
|
+
|
|
83
|
+
[container-runtime]: ../../src/containerRuntime.ts
|
|
84
|
+
[summary-protocol]: /common/lib/protocol-definitions/src/summary.ts
|