@fluidframework/container-runtime 2.3.0-288113 → 2.3.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 +15 -0
- package/api-report/container-runtime.legacy.alpha.api.md +15 -0
- package/container-runtime.test-files.tar +0 -0
- package/dist/channelCollection.d.ts +1 -1
- package/dist/channelCollection.d.ts.map +1 -1
- package/dist/channelCollection.js +1 -16
- package/dist/channelCollection.js.map +1 -1
- package/dist/connectionTelemetry.d.ts +27 -3
- package/dist/connectionTelemetry.d.ts.map +1 -1
- package/dist/connectionTelemetry.js.map +1 -1
- package/dist/containerRuntime.d.ts +68 -13
- package/dist/containerRuntime.d.ts.map +1 -1
- package/dist/containerRuntime.js +262 -180
- package/dist/containerRuntime.js.map +1 -1
- package/dist/deltaManagerProxies.d.ts.map +1 -1
- package/dist/deltaManagerProxies.js +11 -4
- package/dist/deltaManagerProxies.js.map +1 -1
- package/dist/gc/garbageCollection.d.ts.map +1 -1
- package/dist/gc/garbageCollection.js +0 -2
- package/dist/gc/garbageCollection.js.map +1 -1
- package/dist/gc/gcHelpers.d.ts.map +1 -1
- package/dist/gc/gcHelpers.js +0 -8
- package/dist/gc/gcHelpers.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/legacy.d.ts +3 -1
- package/dist/messageTypes.d.ts +0 -9
- package/dist/messageTypes.d.ts.map +1 -1
- package/dist/messageTypes.js.map +1 -1
- package/dist/opLifecycle/batchManager.d.ts +9 -0
- package/dist/opLifecycle/batchManager.d.ts.map +1 -1
- package/dist/opLifecycle/batchManager.js +19 -6
- package/dist/opLifecycle/batchManager.js.map +1 -1
- package/dist/opLifecycle/duplicateBatchDetector.d.ts +32 -0
- package/dist/opLifecycle/duplicateBatchDetector.d.ts.map +1 -0
- package/dist/opLifecycle/duplicateBatchDetector.js +68 -0
- package/dist/opLifecycle/duplicateBatchDetector.js.map +1 -0
- package/dist/opLifecycle/index.d.ts +3 -2
- package/dist/opLifecycle/index.d.ts.map +1 -1
- package/dist/opLifecycle/index.js +4 -1
- package/dist/opLifecycle/index.js.map +1 -1
- package/dist/opLifecycle/opCompressor.d.ts.map +1 -1
- package/dist/opLifecycle/opCompressor.js +0 -4
- package/dist/opLifecycle/opCompressor.js.map +1 -1
- package/dist/opLifecycle/opGroupingManager.d.ts.map +1 -1
- package/dist/opLifecycle/opGroupingManager.js +0 -4
- package/dist/opLifecycle/opGroupingManager.js.map +1 -1
- package/dist/opLifecycle/opSplitter.d.ts.map +1 -1
- package/dist/opLifecycle/opSplitter.js +1 -6
- package/dist/opLifecycle/opSplitter.js.map +1 -1
- package/dist/opLifecycle/outbox.d.ts.map +1 -1
- package/dist/opLifecycle/outbox.js +1 -4
- package/dist/opLifecycle/outbox.js.map +1 -1
- package/dist/opLifecycle/remoteMessageProcessor.d.ts +37 -17
- package/dist/opLifecycle/remoteMessageProcessor.d.ts.map +1 -1
- package/dist/opLifecycle/remoteMessageProcessor.js +47 -37
- package/dist/opLifecycle/remoteMessageProcessor.js.map +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.d.ts.map +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/dist/pendingStateManager.d.ts +27 -17
- package/dist/pendingStateManager.d.ts.map +1 -1
- package/dist/pendingStateManager.js +85 -56
- package/dist/pendingStateManager.js.map +1 -1
- package/dist/scheduleManager.d.ts +2 -4
- package/dist/scheduleManager.d.ts.map +1 -1
- package/dist/scheduleManager.js +6 -37
- package/dist/scheduleManager.js.map +1 -1
- package/dist/summary/summarizerNode/summarizerNodeUtils.d.ts.map +1 -1
- package/dist/summary/summarizerNode/summarizerNodeUtils.js +0 -2
- package/dist/summary/summarizerNode/summarizerNodeUtils.js.map +1 -1
- package/dist/summary/summaryCollection.d.ts.map +1 -1
- package/dist/summary/summaryCollection.js +5 -7
- package/dist/summary/summaryCollection.js.map +1 -1
- package/dist/summary/summaryFormat.d.ts.map +1 -1
- package/dist/summary/summaryFormat.js +1 -4
- package/dist/summary/summaryFormat.js.map +1 -1
- package/lib/channelCollection.d.ts +1 -1
- package/lib/channelCollection.d.ts.map +1 -1
- package/lib/channelCollection.js +1 -16
- package/lib/channelCollection.js.map +1 -1
- package/lib/connectionTelemetry.d.ts +27 -3
- package/lib/connectionTelemetry.d.ts.map +1 -1
- package/lib/connectionTelemetry.js.map +1 -1
- package/lib/containerRuntime.d.ts +68 -13
- package/lib/containerRuntime.d.ts.map +1 -1
- package/lib/containerRuntime.js +262 -181
- package/lib/containerRuntime.js.map +1 -1
- package/lib/deltaManagerProxies.d.ts.map +1 -1
- package/lib/deltaManagerProxies.js +11 -4
- package/lib/deltaManagerProxies.js.map +1 -1
- package/lib/gc/garbageCollection.d.ts.map +1 -1
- package/lib/gc/garbageCollection.js +0 -2
- package/lib/gc/garbageCollection.js.map +1 -1
- package/lib/gc/gcHelpers.d.ts.map +1 -1
- package/lib/gc/gcHelpers.js +0 -8
- package/lib/gc/gcHelpers.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/legacy.d.ts +3 -1
- package/lib/messageTypes.d.ts +0 -9
- package/lib/messageTypes.d.ts.map +1 -1
- package/lib/messageTypes.js.map +1 -1
- package/lib/opLifecycle/batchManager.d.ts +9 -0
- package/lib/opLifecycle/batchManager.d.ts.map +1 -1
- package/lib/opLifecycle/batchManager.js +17 -5
- package/lib/opLifecycle/batchManager.js.map +1 -1
- package/lib/opLifecycle/duplicateBatchDetector.d.ts +32 -0
- package/lib/opLifecycle/duplicateBatchDetector.d.ts.map +1 -0
- package/lib/opLifecycle/duplicateBatchDetector.js +64 -0
- package/lib/opLifecycle/duplicateBatchDetector.js.map +1 -0
- package/lib/opLifecycle/index.d.ts +3 -2
- package/lib/opLifecycle/index.d.ts.map +1 -1
- package/lib/opLifecycle/index.js +2 -1
- package/lib/opLifecycle/index.js.map +1 -1
- package/lib/opLifecycle/opCompressor.d.ts.map +1 -1
- package/lib/opLifecycle/opCompressor.js +0 -4
- package/lib/opLifecycle/opCompressor.js.map +1 -1
- package/lib/opLifecycle/opGroupingManager.d.ts.map +1 -1
- package/lib/opLifecycle/opGroupingManager.js +0 -4
- package/lib/opLifecycle/opGroupingManager.js.map +1 -1
- package/lib/opLifecycle/opSplitter.d.ts.map +1 -1
- package/lib/opLifecycle/opSplitter.js +1 -6
- package/lib/opLifecycle/opSplitter.js.map +1 -1
- package/lib/opLifecycle/outbox.d.ts.map +1 -1
- package/lib/opLifecycle/outbox.js +1 -4
- package/lib/opLifecycle/outbox.js.map +1 -1
- package/lib/opLifecycle/remoteMessageProcessor.d.ts +37 -17
- package/lib/opLifecycle/remoteMessageProcessor.d.ts.map +1 -1
- package/lib/opLifecycle/remoteMessageProcessor.js +47 -37
- package/lib/opLifecycle/remoteMessageProcessor.js.map +1 -1
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.d.ts.map +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/lib/pendingStateManager.d.ts +27 -17
- package/lib/pendingStateManager.d.ts.map +1 -1
- package/lib/pendingStateManager.js +85 -56
- package/lib/pendingStateManager.js.map +1 -1
- package/lib/scheduleManager.d.ts +2 -4
- package/lib/scheduleManager.d.ts.map +1 -1
- package/lib/scheduleManager.js +6 -37
- package/lib/scheduleManager.js.map +1 -1
- package/lib/summary/summarizerNode/summarizerNodeUtils.d.ts.map +1 -1
- package/lib/summary/summarizerNode/summarizerNodeUtils.js +0 -2
- package/lib/summary/summarizerNode/summarizerNodeUtils.js.map +1 -1
- package/lib/summary/summaryCollection.d.ts.map +1 -1
- package/lib/summary/summaryCollection.js +5 -7
- package/lib/summary/summaryCollection.js.map +1 -1
- package/lib/summary/summaryFormat.d.ts.map +1 -1
- package/lib/summary/summaryFormat.js +1 -4
- package/lib/summary/summaryFormat.js.map +1 -1
- package/lib/tsdoc-metadata.json +1 -1
- package/package.json +50 -24
- package/src/channelCollection.ts +7 -21
- package/src/connectionTelemetry.ts +33 -3
- package/src/containerRuntime.ts +382 -233
- package/src/deltaManagerProxies.ts +11 -4
- package/src/gc/garbageCollection.ts +1 -3
- package/src/gc/gcHelpers.ts +4 -12
- package/src/index.ts +2 -0
- package/src/messageTypes.ts +0 -10
- package/src/opLifecycle/batchManager.ts +29 -7
- package/src/opLifecycle/duplicateBatchDetector.ts +78 -0
- package/src/opLifecycle/index.ts +4 -1
- package/src/opLifecycle/opCompressor.ts +2 -6
- package/src/opLifecycle/opGroupingManager.ts +2 -6
- package/src/opLifecycle/opSplitter.ts +2 -6
- package/src/opLifecycle/outbox.ts +1 -3
- package/src/opLifecycle/remoteMessageProcessor.ts +87 -59
- package/src/packageVersion.ts +1 -1
- package/src/pendingStateManager.ts +114 -66
- package/src/scheduleManager.ts +8 -47
- package/src/summary/summarizerNode/summarizerNodeUtils.ts +1 -3
- package/src/summary/summaryCollection.ts +7 -9
- package/src/summary/summaryFormat.ts +1 -3
- package/src/summary/summaryFormats.md +11 -9
- package/tsconfig.json +1 -0
|
@@ -20,7 +20,13 @@ import {
|
|
|
20
20
|
type LocalContainerRuntimeMessage,
|
|
21
21
|
} from "./messageTypes.js";
|
|
22
22
|
import { asBatchMetadata, asEmptyBatchLocalOpMetadata } from "./metadata.js";
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
BatchId,
|
|
25
|
+
BatchMessage,
|
|
26
|
+
getEffectiveBatchId,
|
|
27
|
+
BatchStartInfo,
|
|
28
|
+
InboundMessageResult,
|
|
29
|
+
} from "./opLifecycle/index.js";
|
|
24
30
|
|
|
25
31
|
/**
|
|
26
32
|
* This represents a message that has been submitted and is added to the pending queue when `submit` is called on the
|
|
@@ -37,11 +43,14 @@ export interface IPendingMessage {
|
|
|
37
43
|
sequenceNumber?: number;
|
|
38
44
|
/** Info about the batch this pending message belongs to, for validation and for computing the batchId on reconnect */
|
|
39
45
|
batchInfo: {
|
|
40
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* The Batch's original clientId, from when it was first flushed to be submitted.
|
|
48
|
+
* Or, a random uuid if it was never submitted (and batchStartCsn will be -1)
|
|
49
|
+
*/
|
|
41
50
|
clientId: string;
|
|
42
51
|
/**
|
|
43
52
|
* The Batch's original clientSequenceNumber, from when it was first flushed to be submitted
|
|
44
|
-
*
|
|
53
|
+
* Or, -1 if it was never submitted (and clientId will be a random uuid)
|
|
45
54
|
*/
|
|
46
55
|
batchStartCsn: number;
|
|
47
56
|
/** length of the batch (how many runtime messages here) */
|
|
@@ -125,20 +134,6 @@ function withoutLocalOpMetadata(message: IPendingMessage): IPendingMessage {
|
|
|
125
134
|
};
|
|
126
135
|
}
|
|
127
136
|
|
|
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
|
-
|
|
142
137
|
/**
|
|
143
138
|
* PendingStateManager is responsible for maintaining the messages that have not been sent or have not yet been
|
|
144
139
|
* acknowledged by the server. It also maintains the batch information for both automatically and manually flushed
|
|
@@ -159,9 +154,6 @@ export class PendingStateManager implements IDisposable {
|
|
|
159
154
|
*/
|
|
160
155
|
private savedOps: IPendingMessage[] = [];
|
|
161
156
|
|
|
162
|
-
/** Used to stand in for batchStartCsn for messages that weren't submitted (so no CSN) */
|
|
163
|
-
private negativeCounter: number = -1;
|
|
164
|
-
|
|
165
157
|
private readonly disposeOnce = new Lazy<void>(() => {
|
|
166
158
|
this.initialMessages.clear();
|
|
167
159
|
this.pendingMessages.clear();
|
|
@@ -249,15 +241,19 @@ export class PendingStateManager implements IDisposable {
|
|
|
249
241
|
* or undefined if the batch was not yet sent (e.g. by the time we flushed we lost the connection)
|
|
250
242
|
*/
|
|
251
243
|
public onFlushBatch(batch: BatchMessage[], clientSequenceNumber: number | undefined) {
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
244
|
+
// clientId and batchStartCsn are used for generating the batchId so we can detect container forks
|
|
245
|
+
// where this batch was submitted by two different clients rehydrating from the same local state.
|
|
246
|
+
// In the typical case where the batch was actually sent, use the clientId and clientSequenceNumber.
|
|
247
|
+
// In the case where the batch was not sent, use a random uuid for clientId, and -1 for clientSequenceNumber to indicate this case.
|
|
248
|
+
// This will guarantee uniqueness of the batchId, and is a suitable fallback since clientId/CSN is only needed if the batch was actually sent/sequenced.
|
|
249
|
+
const batchWasSent = clientSequenceNumber !== undefined;
|
|
250
|
+
const [clientId, batchStartCsn] = batchWasSent
|
|
251
|
+
? [this.stateHandler.clientId(), clientSequenceNumber]
|
|
252
|
+
: [uuid(), -1]; // -1 will indicate not a real clientId/CSN pair
|
|
253
|
+
assert(
|
|
254
|
+
clientId !== undefined,
|
|
255
|
+
0xa33 /* clientId (from stateHandler) could only be undefined if we've never connected, but we have a CSN so we know that's not the case */,
|
|
256
|
+
);
|
|
261
257
|
|
|
262
258
|
for (const message of batch) {
|
|
263
259
|
const {
|
|
@@ -326,49 +322,83 @@ export class PendingStateManager implements IDisposable {
|
|
|
326
322
|
}
|
|
327
323
|
|
|
328
324
|
/**
|
|
329
|
-
*
|
|
325
|
+
* Compares the batch ID of the incoming batch with the pending batch ID for this client.
|
|
326
|
+
* They should not match, as that would indicate a forked container.
|
|
327
|
+
* @param remoteBatchStart - BatchStartInfo for an incoming batch *NOT* submitted by this client
|
|
328
|
+
* @returns whether the batch IDs match
|
|
329
|
+
*/
|
|
330
|
+
private remoteBatchMatchesPendingBatch(remoteBatchStart: BatchStartInfo): boolean {
|
|
331
|
+
// We may have no pending changes - if so, no match, no problem.
|
|
332
|
+
const pendingMessage = this.pendingMessages.peekFront();
|
|
333
|
+
if (pendingMessage === undefined) {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// We must compare the effective batch IDs, since one of these ops
|
|
338
|
+
// may have been the original, not resubmitted, so wouldn't have its batch ID stamped yet.
|
|
339
|
+
const pendingBatchId = getEffectiveBatchId(pendingMessage);
|
|
340
|
+
const inboundBatchId = getEffectiveBatchId(remoteBatchStart);
|
|
341
|
+
|
|
342
|
+
return pendingBatchId === inboundBatchId;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Processes an inbound message or batch of messages - May be local or remote.
|
|
330
347
|
*
|
|
331
|
-
* @param
|
|
332
|
-
* @param local - true if we submitted
|
|
333
|
-
* @returns The inbound
|
|
348
|
+
* @param inbound - The inbound message(s) to process, with extra info (e.g. about the start of a batch). Could be local or remote.
|
|
349
|
+
* @param local - true if we submitted these messages and expect corresponding pending messages
|
|
350
|
+
* @returns The inbound messages with localOpMetadata "zipped" in.
|
|
334
351
|
*
|
|
335
|
-
* @
|
|
336
|
-
* - The
|
|
352
|
+
* @throws a DataProcessingError in either of these cases:
|
|
353
|
+
* - The pending message content doesn't match the incoming message content for any message here
|
|
354
|
+
* - The batch IDs *do match* but it's not local (indicates Container forking).
|
|
337
355
|
*/
|
|
338
|
-
public
|
|
339
|
-
|
|
356
|
+
public processInboundMessages(
|
|
357
|
+
inbound: InboundMessageResult,
|
|
340
358
|
local: boolean,
|
|
341
359
|
): {
|
|
342
360
|
message: InboundSequencedContainerRuntimeMessage;
|
|
343
361
|
localOpMetadata?: unknown;
|
|
344
362
|
}[] {
|
|
345
363
|
if (local) {
|
|
346
|
-
return this.
|
|
364
|
+
return this.processPendingLocalMessages(inbound);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// An inbound remote batch should not match the pending batch ID for this client.
|
|
368
|
+
// That would indicate the container forked (two instances trying to submit the same local state)
|
|
369
|
+
if ("batchStart" in inbound && this.remoteBatchMatchesPendingBatch(inbound.batchStart)) {
|
|
370
|
+
throw DataProcessingError.create(
|
|
371
|
+
"Forked Container Error! Matching batchIds but mismatched clientId",
|
|
372
|
+
"PendingStateManager.processInboundMessages",
|
|
373
|
+
inbound.batchStart.keyMessage,
|
|
374
|
+
);
|
|
347
375
|
}
|
|
348
376
|
|
|
349
377
|
// No localOpMetadata for remote messages
|
|
350
|
-
|
|
378
|
+
const messages = inbound.type === "fullBatch" ? inbound.messages : [inbound.nextMessage];
|
|
379
|
+
return messages.map((message) => ({ message }));
|
|
351
380
|
}
|
|
352
381
|
|
|
353
382
|
/**
|
|
354
|
-
* Processes the incoming
|
|
355
|
-
* It verifies that messages are received in the right order and that
|
|
356
|
-
* @param
|
|
357
|
-
* @
|
|
383
|
+
* Processes the incoming message(s) from the server that were submitted by this client.
|
|
384
|
+
* It verifies that messages are received in the right order and that any batch information is correct.
|
|
385
|
+
* @param inbound - The inbound message(s) (originating from this client) to correlate with the pending local state
|
|
386
|
+
* @throws DataProcessingError if the pending message content doesn't match the incoming message content for any message here
|
|
387
|
+
* @returns The inbound messages with localOpMetadata "zipped" in.
|
|
358
388
|
*/
|
|
359
|
-
private
|
|
389
|
+
private processPendingLocalMessages(inbound: InboundMessageResult): {
|
|
360
390
|
message: InboundSequencedContainerRuntimeMessage;
|
|
361
391
|
localOpMetadata: unknown;
|
|
362
392
|
}[] {
|
|
363
|
-
|
|
393
|
+
if ("batchStart" in inbound) {
|
|
394
|
+
this.onLocalBatchBegin(inbound.batchStart, inbound.length);
|
|
395
|
+
}
|
|
364
396
|
|
|
365
397
|
// Empty batch
|
|
366
|
-
if (
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
0x9fb /* Expected sequence number for empty batch */,
|
|
398
|
+
if (inbound.length === 0) {
|
|
399
|
+
const localOpMetadata = this.processNextPendingMessage(
|
|
400
|
+
inbound.batchStart.keyMessage.sequenceNumber,
|
|
370
401
|
);
|
|
371
|
-
const localOpMetadata = this.processNextPendingMessage(batch.emptyBatchSequenceNumber);
|
|
372
402
|
assert(
|
|
373
403
|
asEmptyBatchLocalOpMetadata(localOpMetadata)?.emptyBatch === true,
|
|
374
404
|
0xa20 /* Expected empty batch marker */,
|
|
@@ -376,7 +406,9 @@ export class PendingStateManager implements IDisposable {
|
|
|
376
406
|
return [];
|
|
377
407
|
}
|
|
378
408
|
|
|
379
|
-
|
|
409
|
+
const messages = inbound.type === "fullBatch" ? inbound.messages : [inbound.nextMessage];
|
|
410
|
+
|
|
411
|
+
return messages.map((message) => ({
|
|
380
412
|
message,
|
|
381
413
|
localOpMetadata: this.processNextPendingMessage(message.sequenceNumber, message),
|
|
382
414
|
}));
|
|
@@ -448,7 +480,7 @@ export class PendingStateManager implements IDisposable {
|
|
|
448
480
|
/**
|
|
449
481
|
* Check if the incoming batch matches the batch info for the next pending message.
|
|
450
482
|
*/
|
|
451
|
-
private onLocalBatchBegin(
|
|
483
|
+
private onLocalBatchBegin(batchStart: BatchStartInfo, batchLength?: number) {
|
|
452
484
|
// Get the next message from the pending queue. Verify a message exists.
|
|
453
485
|
const pendingMessage = this.pendingMessages.peekFront();
|
|
454
486
|
assert(
|
|
@@ -456,29 +488,44 @@ export class PendingStateManager implements IDisposable {
|
|
|
456
488
|
0xa21 /* No pending message found as we start processing this remote batch */,
|
|
457
489
|
);
|
|
458
490
|
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
const firstMessage =
|
|
464
|
-
|
|
491
|
+
// If this batch became empty on resubmit, batch.messages will be empty (so firstMessage undefined)
|
|
492
|
+
// and the next pending message should be an empty batch marker.
|
|
493
|
+
// More Info: We must submit empty batches and track them in case a different fork
|
|
494
|
+
// of this container also submitted the same batch (and it may not be empty for that fork).
|
|
495
|
+
const firstMessage = batchStart.keyMessage;
|
|
496
|
+
// -1 length is for back compat, undefined length means we actually don't know it
|
|
497
|
+
const skipLengthCheck =
|
|
498
|
+
pendingMessage.batchInfo.length === -1 || batchLength === undefined;
|
|
499
|
+
const expectedPendingBatchLength =
|
|
500
|
+
batchLength === 0
|
|
501
|
+
? 1 // For an empty batch, expect a singleton array with the empty batch marker
|
|
502
|
+
: batchLength; // Otherwise, the lengths should actually match
|
|
503
|
+
|
|
504
|
+
// Note: We don't need to use getEffectiveBatchId here, just check the explicit stamped batchID
|
|
505
|
+
// That logic is needed only when comparing across potential container forks.
|
|
506
|
+
// Furthermore, we also are comparing the batch IDs constituent data - clientId (it's local) and batchStartCsn.
|
|
507
|
+
const pendingBatchId = asBatchMetadata(pendingMessage.opMetadata)?.batchId;
|
|
508
|
+
const inboundBatchId = batchStart.batchId;
|
|
465
509
|
|
|
466
510
|
// We expect the incoming batch to be of the same length, starting at the same clientSequenceNumber,
|
|
467
|
-
// as the batch we originally submitted.
|
|
511
|
+
// as the batch we originally submitted. The batchIds should match as well, if set (or neither should be set)
|
|
468
512
|
// 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.
|
|
513
|
+
// so we don't throw here, merely log. In a later release this check may replace that one since it's cheaper.
|
|
470
514
|
if (
|
|
471
|
-
pendingMessage.batchInfo.batchStartCsn !==
|
|
472
|
-
(pendingMessage.batchInfo.length
|
|
473
|
-
|
|
515
|
+
pendingMessage.batchInfo.batchStartCsn !== batchStart.batchStartCsn ||
|
|
516
|
+
(!skipLengthCheck && pendingMessage.batchInfo.length !== expectedPendingBatchLength) ||
|
|
517
|
+
pendingBatchId !== inboundBatchId
|
|
474
518
|
) {
|
|
475
519
|
this.logger?.sendErrorEvent({
|
|
476
520
|
eventName: "BatchInfoMismatch",
|
|
477
521
|
details: {
|
|
478
522
|
pendingBatchCsn: pendingMessage.batchInfo.batchStartCsn,
|
|
479
|
-
batchStartCsn:
|
|
523
|
+
batchStartCsn: batchStart.batchStartCsn,
|
|
480
524
|
pendingBatchLength: pendingMessage.batchInfo.length,
|
|
481
|
-
|
|
525
|
+
expectedPendingBatchLength,
|
|
526
|
+
batchLength,
|
|
527
|
+
pendingBatchId,
|
|
528
|
+
inboundBatchId,
|
|
482
529
|
pendingMessageBatchMetadata: asBatchMetadata(pendingMessage.opMetadata)?.batch,
|
|
483
530
|
messageBatchMetadata: asBatchMetadata(firstMessage?.metadata)?.batch,
|
|
484
531
|
},
|
|
@@ -526,8 +573,9 @@ export class PendingStateManager implements IDisposable {
|
|
|
526
573
|
|
|
527
574
|
// The next message starts a batch (possibly single-message), and we'll need its batchId.
|
|
528
575
|
const batchId = getEffectiveBatchId(pendingMessage);
|
|
529
|
-
|
|
576
|
+
|
|
530
577
|
if (asEmptyBatchLocalOpMetadata(pendingMessage.localOpMetadata)?.emptyBatch === true) {
|
|
578
|
+
// Resubmit no messages, with the batchId. Will result in another empty batch marker.
|
|
531
579
|
this.stateHandler.reSubmitBatch([], batchId);
|
|
532
580
|
continue;
|
|
533
581
|
}
|
package/src/scheduleManager.ts
CHANGED
|
@@ -41,8 +41,6 @@ type IRuntimeMessageMetadata =
|
|
|
41
41
|
*/
|
|
42
42
|
export class ScheduleManager {
|
|
43
43
|
private readonly deltaScheduler: DeltaScheduler;
|
|
44
|
-
private batchClientId: string | undefined;
|
|
45
|
-
private hitError = false;
|
|
46
44
|
|
|
47
45
|
constructor(
|
|
48
46
|
private readonly deltaManager: IDeltaManager<ISequencedDocumentMessage, IDocumentMessage>,
|
|
@@ -57,47 +55,14 @@ export class ScheduleManager {
|
|
|
57
55
|
void new ScheduleManagerCore(deltaManager, getClientId, logger);
|
|
58
56
|
}
|
|
59
57
|
|
|
60
|
-
public
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
this.batchClientId === undefined,
|
|
64
|
-
0x2a2 /* "Batch is interrupted by other client op. Should be caught by trackPending()" */,
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
// This could be the beginning of a new batch or an individual message.
|
|
68
|
-
this.emitter.emit("batchBegin", message);
|
|
69
|
-
this.deltaScheduler.batchBegin(message);
|
|
70
|
-
|
|
71
|
-
const batch = (message?.metadata as IRuntimeMessageMetadata)?.batch;
|
|
72
|
-
// TODO: Verify whether this should be able to handle server-generated ops (with null clientId)
|
|
73
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
|
74
|
-
this.batchClientId = batch ? (message.clientId as string) : undefined;
|
|
75
|
-
}
|
|
58
|
+
public batchBegin(message: ISequencedDocumentMessage) {
|
|
59
|
+
this.emitter.emit("batchBegin", message);
|
|
60
|
+
this.deltaScheduler.batchBegin(message);
|
|
76
61
|
}
|
|
77
62
|
|
|
78
|
-
public
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (error) {
|
|
83
|
-
// We assume here that loader will close container and stop processing all future ops.
|
|
84
|
-
// This is implicit dependency. If this flow changes, this code might no longer be correct.
|
|
85
|
-
this.hitError = true;
|
|
86
|
-
this.batchClientId = undefined;
|
|
87
|
-
this.emitter.emit("batchEnd", error, message);
|
|
88
|
-
this.deltaScheduler.batchEnd(message);
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const batch = (message?.metadata as IRuntimeMessageMetadata)?.batch;
|
|
93
|
-
// If no batchClientId has been set then we're in an individual batch. Else, if we get
|
|
94
|
-
// batch end metadata, this is end of the current batch.
|
|
95
|
-
if (this.batchClientId === undefined || batch === false) {
|
|
96
|
-
this.batchClientId = undefined;
|
|
97
|
-
this.emitter.emit("batchEnd", undefined, message);
|
|
98
|
-
this.deltaScheduler.batchEnd(message);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
63
|
+
public batchEnd(error: any | undefined, message: ISequencedDocumentMessage) {
|
|
64
|
+
this.emitter.emit("batchEnd", error, message);
|
|
65
|
+
this.deltaScheduler.batchEnd(message);
|
|
101
66
|
}
|
|
102
67
|
}
|
|
103
68
|
|
|
@@ -124,9 +89,7 @@ class ScheduleManagerCore {
|
|
|
124
89
|
}
|
|
125
90
|
|
|
126
91
|
// First message will have the batch flag set to true if doing a batched send
|
|
127
|
-
|
|
128
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
129
|
-
const firstMessageMetadata = messages[0]!.metadata as IRuntimeMessageMetadata;
|
|
92
|
+
const firstMessageMetadata = messages[0].metadata as IRuntimeMessageMetadata;
|
|
130
93
|
if (!firstMessageMetadata?.batch) {
|
|
131
94
|
return;
|
|
132
95
|
}
|
|
@@ -138,9 +101,7 @@ class ScheduleManagerCore {
|
|
|
138
101
|
}
|
|
139
102
|
|
|
140
103
|
// Set the batch flag to false on the last message to indicate the end of the send batch
|
|
141
|
-
|
|
142
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
143
|
-
const lastMessage = messages[messages.length - 1]!;
|
|
104
|
+
const lastMessage = messages[messages.length - 1];
|
|
144
105
|
// TODO: It's not clear if this shallow clone is required, as opposed to just setting "batch" to false.
|
|
145
106
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
|
146
107
|
lastMessage.metadata = { ...(lastMessage.metadata as any), batch: false };
|
|
@@ -73,9 +73,7 @@ export class EscapedPath {
|
|
|
73
73
|
public static createAndConcat(pathParts: string[]): EscapedPath {
|
|
74
74
|
let ret = EscapedPath.create(pathParts[0] ?? "");
|
|
75
75
|
for (let i = 1; i < pathParts.length; i++) {
|
|
76
|
-
|
|
77
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
78
|
-
ret = ret.concat(EscapedPath.create(pathParts[i]!));
|
|
76
|
+
ret = ret.concat(EscapedPath.create(pathParts[i]));
|
|
79
77
|
}
|
|
80
78
|
return ret;
|
|
81
79
|
}
|
|
@@ -353,12 +353,11 @@ export class SummaryCollection extends TypedEventEmitter<ISummaryCollectionOpEve
|
|
|
353
353
|
}
|
|
354
354
|
|
|
355
355
|
private parseContent(op: ISequencedDocumentMessage) {
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
//
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
// parsing in loader layer!
|
|
356
|
+
// This should become unconditional once (Loader LTS) DeltaManager.processInboundMessage() stops parsing content (ADO #12052)
|
|
357
|
+
// Note: Until that change is made in the loader, this case will never be hit.
|
|
358
|
+
// Then there will be a long time of needing both cases, until LTS catches up to the change.
|
|
359
|
+
// That said, we may instead move to listen for "op" events from ContainerRuntime,
|
|
360
|
+
// and parsing may not be required at all if ContainerRuntime.process() would parse it for all types of ops.
|
|
362
361
|
if (typeof op.contents === "string") {
|
|
363
362
|
op.contents = JSON.parse(op.contents);
|
|
364
363
|
}
|
|
@@ -378,9 +377,8 @@ export class SummaryCollection extends TypedEventEmitter<ISummaryCollectionOpEve
|
|
|
378
377
|
case MessageType.SummaryAck:
|
|
379
378
|
case MessageType.SummaryNack:
|
|
380
379
|
// Old files (prior to PR #10077) may not contain this info
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
op.contents = JSON.parse((op as any).data);
|
|
380
|
+
if (op.data !== undefined) {
|
|
381
|
+
op.contents = JSON.parse(op.data);
|
|
384
382
|
} else {
|
|
385
383
|
this.parseContent(op);
|
|
386
384
|
}
|
|
@@ -281,9 +281,7 @@ export async function getFluidDataStoreAttributes(
|
|
|
281
281
|
): Promise<ReadFluidDataStoreAttributes> {
|
|
282
282
|
const attributes = await readAndParse<ReadFluidDataStoreAttributes>(
|
|
283
283
|
storage,
|
|
284
|
-
|
|
285
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
286
|
-
snapshot.blobs[dataStoreAttributesBlobName]!,
|
|
284
|
+
snapshot.blobs[dataStoreAttributesBlobName],
|
|
287
285
|
);
|
|
288
286
|
// Use the snapshotFormatVersion to determine how the pkg is encoded in the snapshot.
|
|
289
287
|
// For snapshotFormatVersion = "0.1" (1) or above, pkg is jsonified, otherwise it is just a string.
|
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## Table of contents
|
|
4
4
|
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
- [Summary and snapshot formats](#summary-and-snapshot-formats)
|
|
6
|
+
- [Table of contents](#table-of-contents)
|
|
7
|
+
- [Introduction](#introduction)
|
|
8
|
+
- [Summary Format](#summary-format)
|
|
9
|
+
- [Snapshot Format](#snapshot-format)
|
|
10
|
+
- [Summary / Snapshot Tree Visualization](#summary--snapshot-tree-visualization)
|
|
11
|
+
- [Protocol tree](#protocol-tree)
|
|
12
|
+
- [App tree](#app-tree)
|
|
13
|
+
- [Summary tree distinction - Incremental summaries](#summary-tree-distinction---incremental-summaries)
|
|
12
14
|
|
|
13
15
|
## Introduction
|
|
14
16
|
|
|
@@ -211,9 +213,9 @@ flowchart TD
|
|
|
211
213
|
C --> D["handle: '/data store 1'"]:::handle
|
|
212
214
|
C --> E["data store 2"]:::tree
|
|
213
215
|
E --> F[".channels"]:::tree
|
|
214
|
-
F --> G["handle: '/data store 2/DDS 1'"]:::handle
|
|
216
|
+
F --> G["handle: '/.channels/data store 2/.channels/DDS 1'"]:::handle
|
|
215
217
|
F --> H["DDS 2"]:::tree
|
|
216
|
-
H --> I["handle: '/data store 2/DDS 2/sub node'"]:::handle
|
|
218
|
+
H --> I["handle: '/.channels/data store 2/.channels/DDS 2/sub node'"]:::handle
|
|
217
219
|
F --> J["DDS N"]:::tree
|
|
218
220
|
E --> K["other nodes"]:::others
|
|
219
221
|
C --> L["data store N"]:::tree
|