@fluidframework/container-runtime 2.0.0-internal.1.1.1 → 2.0.0-internal.1.2.0.93071
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/dist/batchManager.d.ts +32 -0
- package/dist/batchManager.d.ts.map +1 -0
- package/dist/batchManager.js +71 -0
- package/dist/batchManager.js.map +1 -0
- package/dist/containerRuntime.d.ts +44 -17
- package/dist/containerRuntime.d.ts.map +1 -1
- package/dist/containerRuntime.js +197 -95
- package/dist/containerRuntime.js.map +1 -1
- package/dist/dataStoreContext.d.ts +4 -4
- package/dist/dataStoreContext.js +5 -5
- package/dist/dataStoreContext.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 +0 -11
- package/dist/pendingStateManager.d.ts.map +1 -1
- package/dist/pendingStateManager.js +6 -43
- package/dist/pendingStateManager.js.map +1 -1
- package/dist/runningSummarizer.js +1 -1
- package/dist/runningSummarizer.js.map +1 -1
- package/dist/scheduleManager.js +1 -1
- package/dist/scheduleManager.js.map +1 -1
- package/dist/summarizerTypes.d.ts +3 -3
- package/dist/summarizerTypes.js +1 -1
- package/dist/summarizerTypes.js.map +1 -1
- package/dist/summaryCollection.d.ts +1 -0
- package/dist/summaryCollection.d.ts.map +1 -1
- package/dist/summaryCollection.js +32 -13
- package/dist/summaryCollection.js.map +1 -1
- package/lib/batchManager.d.ts +32 -0
- package/lib/batchManager.d.ts.map +1 -0
- package/lib/batchManager.js +67 -0
- package/lib/batchManager.js.map +1 -0
- package/lib/containerRuntime.d.ts +44 -17
- package/lib/containerRuntime.d.ts.map +1 -1
- package/lib/containerRuntime.js +200 -98
- package/lib/containerRuntime.js.map +1 -1
- package/lib/dataStoreContext.d.ts +4 -4
- package/lib/dataStoreContext.js +5 -5
- package/lib/dataStoreContext.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 +0 -11
- package/lib/pendingStateManager.d.ts.map +1 -1
- package/lib/pendingStateManager.js +6 -43
- package/lib/pendingStateManager.js.map +1 -1
- package/lib/runningSummarizer.js +1 -1
- package/lib/runningSummarizer.js.map +1 -1
- package/lib/scheduleManager.js +2 -2
- package/lib/scheduleManager.js.map +1 -1
- package/lib/summarizerTypes.d.ts +3 -3
- package/lib/summarizerTypes.js +1 -1
- package/lib/summarizerTypes.js.map +1 -1
- package/lib/summaryCollection.d.ts +1 -0
- package/lib/summaryCollection.d.ts.map +1 -1
- package/lib/summaryCollection.js +32 -13
- package/lib/summaryCollection.js.map +1 -1
- package/package.json +17 -17
- package/src/batchManager.ts +88 -0
- package/src/containerRuntime.ts +273 -156
- package/src/dataStoreContext.ts +7 -7
- package/src/packageVersion.ts +1 -1
- package/src/pendingStateManager.ts +6 -56
- package/src/runningSummarizer.ts +1 -1
- package/src/scheduleManager.ts +2 -2
- package/src/summarizerTypes.ts +3 -3
- package/src/summaryCollection.ts +33 -16
package/src/containerRuntime.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
ILoaderOptions,
|
|
24
24
|
LoaderHeader,
|
|
25
25
|
ISnapshotTreeWithBlobContents,
|
|
26
|
+
IBatchMessage,
|
|
26
27
|
} from "@fluidframework/container-definitions";
|
|
27
28
|
import {
|
|
28
29
|
IContainerRuntime,
|
|
@@ -41,6 +42,7 @@ import {
|
|
|
41
42
|
TaggedLoggerAdapter,
|
|
42
43
|
MonitoringContext,
|
|
43
44
|
loggerToMonitoringContext,
|
|
45
|
+
wrapError,
|
|
44
46
|
} from "@fluidframework/telemetry-utils";
|
|
45
47
|
import {
|
|
46
48
|
DriverHeader,
|
|
@@ -48,7 +50,7 @@ import {
|
|
|
48
50
|
IDocumentStorageService,
|
|
49
51
|
ISummaryContext,
|
|
50
52
|
} from "@fluidframework/driver-definitions";
|
|
51
|
-
import { readAndParse
|
|
53
|
+
import { readAndParse } from "@fluidframework/driver-utils";
|
|
52
54
|
import {
|
|
53
55
|
DataCorruptionError,
|
|
54
56
|
DataProcessingError,
|
|
@@ -113,7 +115,11 @@ import {
|
|
|
113
115
|
ReportOpPerfTelemetry,
|
|
114
116
|
IPerfSignalReport,
|
|
115
117
|
} from "./connectionTelemetry";
|
|
116
|
-
import {
|
|
118
|
+
import {
|
|
119
|
+
IPendingLocalState,
|
|
120
|
+
PendingStateManager,
|
|
121
|
+
} from "./pendingStateManager";
|
|
122
|
+
import { BatchManager, BatchMessage } from "./batchManager";
|
|
117
123
|
import { pkgVersion } from "./packageVersion";
|
|
118
124
|
import { BlobManager, IBlobManagerLoadInfo, IPendingBlobs } from "./blobManager";
|
|
119
125
|
import { DataStores, getSummaryForDatastores } from "./dataStores";
|
|
@@ -195,6 +201,7 @@ export interface ContainerRuntimeMessage {
|
|
|
195
201
|
contents: any;
|
|
196
202
|
type: ContainerMessageType;
|
|
197
203
|
}
|
|
204
|
+
|
|
198
205
|
export interface ISummaryBaseConfiguration {
|
|
199
206
|
/**
|
|
200
207
|
* Delay before first attempt to spawn summarizing container.
|
|
@@ -223,7 +230,8 @@ export interface ISummaryBaseConfiguration {
|
|
|
223
230
|
export interface ISummaryConfigurationHeuristics extends ISummaryBaseConfiguration {
|
|
224
231
|
state: "enabled";
|
|
225
232
|
/**
|
|
226
|
-
* @deprecated
|
|
233
|
+
* @deprecated Please move all implementations to {@link ISummaryConfigurationHeuristics.minIdleTime} and
|
|
234
|
+
* {@link ISummaryConfigurationHeuristics.maxIdleTime} instead.
|
|
227
235
|
*/
|
|
228
236
|
idleTime: number;
|
|
229
237
|
/**
|
|
@@ -368,33 +376,45 @@ export interface ISummaryRuntimeOptions {
|
|
|
368
376
|
summaryConfigOverrides?: ISummaryConfiguration;
|
|
369
377
|
|
|
370
378
|
/**
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
|
|
379
|
+
* Delay before first attempt to spawn summarizing container.
|
|
380
|
+
*
|
|
381
|
+
* @deprecated Use {@link ISummaryRuntimeOptions.summaryConfigOverrides}'s
|
|
382
|
+
* {@link ISummaryBaseConfiguration.initialSummarizerDelayMs} instead.
|
|
383
|
+
*/
|
|
374
384
|
initialSummarizerDelayMs?: number;
|
|
375
385
|
|
|
376
386
|
/**
|
|
377
|
-
* @deprecated - use `summaryConfigOverrides.disableSummaries` instead.
|
|
378
387
|
* Flag that disables summaries if it is set to true.
|
|
388
|
+
*
|
|
389
|
+
* @deprecated Use {@link ISummaryRuntimeOptions.summaryConfigOverrides}'s
|
|
390
|
+
* {@link ISummaryConfigurationDisableSummarizer.state} instead.
|
|
379
391
|
*/
|
|
380
392
|
disableSummaries?: boolean;
|
|
381
393
|
|
|
382
394
|
/**
|
|
383
|
-
* @
|
|
384
|
-
*
|
|
395
|
+
* @defaultValue 7000 operations (ops)
|
|
396
|
+
*
|
|
397
|
+
* @deprecated Use {@link ISummaryRuntimeOptions.summaryConfigOverrides}'s
|
|
398
|
+
* {@link ISummaryBaseConfiguration.maxOpsSinceLastSummary} instead.
|
|
385
399
|
*/
|
|
386
400
|
maxOpsSinceLastSummary?: number;
|
|
387
401
|
|
|
388
402
|
/**
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
403
|
+
* Flag that will enable changing elected summarizer client after maxOpsSinceLastSummary.
|
|
404
|
+
*
|
|
405
|
+
* @defaultValue `false` (disabled) and must be explicitly set to true to enable.
|
|
406
|
+
*
|
|
407
|
+
* @deprecated Use {@link ISummaryRuntimeOptions.summaryConfigOverrides}'s
|
|
408
|
+
* {@link ISummaryBaseConfiguration.summarizerClientElection} instead.
|
|
409
|
+
*/
|
|
393
410
|
summarizerClientElection?: boolean;
|
|
394
411
|
|
|
395
412
|
/**
|
|
396
|
-
*
|
|
397
|
-
*
|
|
413
|
+
* Options that control the running summarizer behavior.
|
|
414
|
+
*
|
|
415
|
+
* @deprecated Use {@link ISummaryRuntimeOptions.summaryConfigOverrides}'s
|
|
416
|
+
* `{@link ISummaryConfiguration.state} = "DisableHeuristics"` instead.
|
|
417
|
+
* */
|
|
398
418
|
summarizerOptions?: Readonly<Partial<ISummarizerOptions>>;
|
|
399
419
|
}
|
|
400
420
|
|
|
@@ -493,13 +513,11 @@ interface IPendingRuntimeState {
|
|
|
493
513
|
|
|
494
514
|
const maxConsecutiveReconnectsKey = "Fluid.ContainerRuntime.MaxConsecutiveReconnects";
|
|
495
515
|
|
|
496
|
-
// By default, we should reject any op larger than 768KB,
|
|
497
|
-
// in order to account for some extra overhead from serialization
|
|
498
|
-
// to not reach the 1MB limits in socket.io and Kafka.
|
|
499
|
-
const defaultMaxOpSizeInBytes = 768000;
|
|
500
|
-
|
|
501
516
|
const defaultFlushMode = FlushMode.TurnBased;
|
|
502
517
|
|
|
518
|
+
/**
|
|
519
|
+
* @deprecated - use ContainerRuntimeMessage instead
|
|
520
|
+
*/
|
|
503
521
|
export enum RuntimeMessage {
|
|
504
522
|
FluidDataStoreOp = "component",
|
|
505
523
|
Attach = "attach",
|
|
@@ -510,6 +528,9 @@ export enum RuntimeMessage {
|
|
|
510
528
|
Operation = "op",
|
|
511
529
|
}
|
|
512
530
|
|
|
531
|
+
/**
|
|
532
|
+
* @deprecated - please use version in driver-utils
|
|
533
|
+
*/
|
|
513
534
|
export function isRuntimeMessage(message: ISequencedDocumentMessage): boolean {
|
|
514
535
|
if ((Object.values(RuntimeMessage) as string[]).includes(message.type)) {
|
|
515
536
|
return true;
|
|
@@ -517,6 +538,12 @@ export function isRuntimeMessage(message: ISequencedDocumentMessage): boolean {
|
|
|
517
538
|
return false;
|
|
518
539
|
}
|
|
519
540
|
|
|
541
|
+
/**
|
|
542
|
+
* Unpacks runtime messages
|
|
543
|
+
* @internal - no promises RE back-compat - this is internal API.
|
|
544
|
+
* @param message - message (as it observed in storage / service)
|
|
545
|
+
* @returns unpacked runtime message
|
|
546
|
+
*/
|
|
520
547
|
export function unpackRuntimeMessage(message: ISequencedDocumentMessage) {
|
|
521
548
|
if (message.type === MessageType.Operation) {
|
|
522
549
|
// legacy op format?
|
|
@@ -529,13 +556,14 @@ export function unpackRuntimeMessage(message: ISequencedDocumentMessage) {
|
|
|
529
556
|
message.type = innerContents.type;
|
|
530
557
|
message.contents = innerContents.contents;
|
|
531
558
|
}
|
|
532
|
-
|
|
559
|
+
return true;
|
|
533
560
|
} else {
|
|
534
561
|
// Legacy format, but it's already "unpacked",
|
|
535
562
|
// i.e. message.type is actually ContainerMessageType.
|
|
563
|
+
// Or it's non-runtime message.
|
|
536
564
|
// Nothing to do in such case.
|
|
565
|
+
return false;
|
|
537
566
|
}
|
|
538
|
-
return message;
|
|
539
567
|
}
|
|
540
568
|
|
|
541
569
|
/**
|
|
@@ -774,7 +802,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
774
802
|
|
|
775
803
|
private _orderSequentiallyCalls: number = 0;
|
|
776
804
|
private _flushMode: FlushMode;
|
|
777
|
-
private needsFlush = false;
|
|
778
805
|
private flushTrigger = false;
|
|
779
806
|
|
|
780
807
|
private _connected: boolean;
|
|
@@ -823,6 +850,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
823
850
|
private readonly scheduleManager: ScheduleManager;
|
|
824
851
|
private readonly blobManager: BlobManager;
|
|
825
852
|
private readonly pendingStateManager: PendingStateManager;
|
|
853
|
+
private readonly batchManager = new BatchManager();
|
|
826
854
|
private readonly garbageCollector: IGarbageCollector;
|
|
827
855
|
|
|
828
856
|
// Local copy of incomplete received chunks.
|
|
@@ -1058,7 +1086,6 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1058
1086
|
flush: this.flush.bind(this),
|
|
1059
1087
|
flushMode: () => this.flushMode,
|
|
1060
1088
|
reSubmit: this.reSubmit.bind(this),
|
|
1061
|
-
rollback: this.rollback.bind(this),
|
|
1062
1089
|
setFlushMode: (mode) => this.setFlushMode(mode),
|
|
1063
1090
|
},
|
|
1064
1091
|
this._flushMode,
|
|
@@ -1427,7 +1454,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1427
1454
|
return true;
|
|
1428
1455
|
}
|
|
1429
1456
|
|
|
1430
|
-
if (!this.
|
|
1457
|
+
if (!this.hasPendingMessages()) {
|
|
1431
1458
|
// If there are no pending messages, we can always reconnect
|
|
1432
1459
|
this.resetReconnectCount();
|
|
1433
1460
|
return true;
|
|
@@ -1546,6 +1573,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1546
1573
|
this._perfSignalData.signalsLost = 0;
|
|
1547
1574
|
this._perfSignalData.signalTimestamp = 0;
|
|
1548
1575
|
this._perfSignalData.trackingSignalSequenceNumber = undefined;
|
|
1576
|
+
} else {
|
|
1577
|
+
assert(this.attachState === AttachState.Attached,
|
|
1578
|
+
"Connection is possible only if container exists in storage");
|
|
1549
1579
|
}
|
|
1550
1580
|
|
|
1551
1581
|
// Fail while disconnected
|
|
@@ -1554,9 +1584,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1554
1584
|
|
|
1555
1585
|
if (!this.shouldContinueReconnecting()) {
|
|
1556
1586
|
this.closeFn(
|
|
1557
|
-
// pre-0.58 error message: MaxReconnectsWithNoProgress
|
|
1558
1587
|
DataProcessingError.create(
|
|
1559
|
-
|
|
1588
|
+
// eslint-disable-next-line max-len
|
|
1589
|
+
"Runtime detected too many reconnects with no progress syncing local ops. Batch of ops is likely too large (over 1Mb)",
|
|
1560
1590
|
"setConnectionState",
|
|
1561
1591
|
undefined,
|
|
1562
1592
|
{
|
|
@@ -1580,49 +1610,50 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1580
1610
|
public process(messageArg: ISequencedDocumentMessage, local: boolean) {
|
|
1581
1611
|
this.verifyNotClosed();
|
|
1582
1612
|
|
|
1583
|
-
// If it's not message for runtime, bail out right away.
|
|
1584
|
-
if (!isUnpackedRuntimeMessage(messageArg)) {
|
|
1585
|
-
return;
|
|
1586
|
-
}
|
|
1587
|
-
|
|
1588
|
-
if (this.mc.config.getBoolean("enableOfflineLoad") ?? this.runtimeOptions.enableOfflineLoad) {
|
|
1589
|
-
this.savedOps.push(messageArg);
|
|
1590
|
-
}
|
|
1591
|
-
|
|
1592
1613
|
// Do shallow copy of message, as methods below will modify it.
|
|
1593
1614
|
// There might be multiple container instances receiving same message
|
|
1594
1615
|
// We do not need to make deep copy, as each layer will just replace message.content itself,
|
|
1595
1616
|
// but would not modify contents details
|
|
1596
1617
|
let message = { ...messageArg };
|
|
1597
1618
|
|
|
1619
|
+
// back-compat: ADO #1385: eventually should become unconditional, but only for runtime messages!
|
|
1620
|
+
// System message may have no contents, or in some cases (mostly for back-compat) they may have actual objects.
|
|
1621
|
+
// Old ops may contain empty string (I assume noops).
|
|
1622
|
+
if (typeof message.contents === "string" && message.contents !== "") {
|
|
1623
|
+
message.contents = JSON.parse(message.contents);
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
// Caveat: This will return false for runtime message in very old format, that are used in snapshot tests
|
|
1627
|
+
// This format was not shipped to production workflows.
|
|
1628
|
+
const runtimeMessage = unpackRuntimeMessage(message);
|
|
1629
|
+
|
|
1630
|
+
if (this.mc.config.getBoolean("enableOfflineLoad") ?? this.runtimeOptions.enableOfflineLoad) {
|
|
1631
|
+
this.savedOps.push(messageArg);
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1598
1634
|
// Surround the actual processing of the operation with messages to the schedule manager indicating
|
|
1599
1635
|
// the beginning and end. This allows it to emit appropriate events and/or pause the processing of new
|
|
1600
1636
|
// messages once a batch has been fully processed.
|
|
1601
1637
|
this.scheduleManager.beforeOpProcessing(message);
|
|
1602
1638
|
|
|
1603
1639
|
try {
|
|
1604
|
-
message = unpackRuntimeMessage(message);
|
|
1605
|
-
|
|
1606
1640
|
// Chunk processing must come first given that we will transform the message to the unchunked version
|
|
1607
1641
|
// once all pieces are available
|
|
1608
1642
|
message = this.processRemoteChunkedMessage(message);
|
|
1609
1643
|
|
|
1610
1644
|
let localOpMetadata: unknown;
|
|
1611
|
-
if (local) {
|
|
1612
|
-
|
|
1613
|
-
// Do not process local chunked ops until all pieces are available.
|
|
1614
|
-
if (message.type !== ContainerMessageType.ChunkedOp) {
|
|
1615
|
-
localOpMetadata = this.pendingStateManager.processPendingLocalMessage(message);
|
|
1616
|
-
}
|
|
1645
|
+
if (local && runtimeMessage) {
|
|
1646
|
+
localOpMetadata = this.pendingStateManager.processPendingLocalMessage(message);
|
|
1617
1647
|
}
|
|
1618
1648
|
|
|
1619
1649
|
// If there are no more pending messages after processing a local message,
|
|
1620
1650
|
// the document is no longer dirty.
|
|
1621
|
-
if (!this.
|
|
1651
|
+
if (!this.hasPendingMessages()) {
|
|
1622
1652
|
this.updateDocumentDirtyState(false);
|
|
1623
1653
|
}
|
|
1624
1654
|
|
|
1625
|
-
|
|
1655
|
+
const type = message.type as ContainerMessageType;
|
|
1656
|
+
switch (type) {
|
|
1626
1657
|
case ContainerMessageType.Attach:
|
|
1627
1658
|
this.dataStores.processAttachMessage(message, local);
|
|
1628
1659
|
break;
|
|
@@ -1635,10 +1666,18 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1635
1666
|
case ContainerMessageType.BlobAttach:
|
|
1636
1667
|
this.blobManager.processBlobAttachOp(message, local);
|
|
1637
1668
|
break;
|
|
1669
|
+
case ContainerMessageType.ChunkedOp:
|
|
1670
|
+
case ContainerMessageType.Rejoin:
|
|
1671
|
+
break;
|
|
1638
1672
|
default:
|
|
1673
|
+
assert(!runtimeMessage, "Runtime message of unknown type");
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
// For back-compat, notify only about runtime messages for now.
|
|
1677
|
+
if (runtimeMessage) {
|
|
1678
|
+
this.emit("op", message, runtimeMessage);
|
|
1639
1679
|
}
|
|
1640
1680
|
|
|
1641
|
-
this.emit("op", message);
|
|
1642
1681
|
this.scheduleManager.afterOpProcessing(undefined, message);
|
|
1643
1682
|
|
|
1644
1683
|
if (local) {
|
|
@@ -1752,30 +1791,76 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1752
1791
|
assert(this._orderSequentiallyCalls === 0,
|
|
1753
1792
|
0x24c /* "Cannot call `flush()` from `orderSequentially`'s callback" */);
|
|
1754
1793
|
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
}
|
|
1794
|
+
const batch = this.batchManager.popBatch();
|
|
1795
|
+
this.flushBatch(batch);
|
|
1758
1796
|
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
// not connected, `this.needsFlush` will be false but the PendingStateManager might have pending messages and
|
|
1762
|
-
// hence needs to track this.
|
|
1763
|
-
this.pendingStateManager.onFlush();
|
|
1797
|
+
assert(this.batchManager.empty, "reentrancy");
|
|
1798
|
+
}
|
|
1764
1799
|
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1800
|
+
protected flushBatch(batch: BatchMessage[]): void {
|
|
1801
|
+
const length = batch.length;
|
|
1802
|
+
|
|
1803
|
+
if (length > 1) {
|
|
1804
|
+
batch[0].metadata = { ...batch[0].metadata, batch: true };
|
|
1805
|
+
batch[length - 1].metadata = { ...batch[length - 1].metadata, batch: false };
|
|
1806
|
+
|
|
1807
|
+
// This assert fires for the following reason (there might be more cases like that):
|
|
1808
|
+
// AgentScheduler will send ops in response to ConsensusRegisterCollection's "atomicChanged" event handler,
|
|
1809
|
+
// i.e. in the middle of op processing!
|
|
1810
|
+
// Sending ops while processing ops is not good idea - it's not defined when
|
|
1811
|
+
// referenceSequenceNumber changes in op processing sequence (at the beginning or end of op processing),
|
|
1812
|
+
// If we send ops in response to processing multiple ops, then we for sure hit this assert!
|
|
1813
|
+
// Tracked via ADO #1834
|
|
1814
|
+
// assert(batch[0].referenceSequenceNumber === batch[length - 1].referenceSequenceNumber,
|
|
1815
|
+
// "Batch should be generated synchronously, without processing ops in the middle!");
|
|
1768
1816
|
}
|
|
1769
1817
|
|
|
1770
|
-
|
|
1818
|
+
let clientSequenceNumber: number = -1;
|
|
1771
1819
|
|
|
1772
1820
|
// Did we disconnect in the middle of turn-based batch?
|
|
1773
1821
|
// If so, do nothing, as pending state manager will resubmit it correctly on reconnect.
|
|
1774
|
-
if (
|
|
1775
|
-
|
|
1822
|
+
if (this.canSendOps()) {
|
|
1823
|
+
if (this.context.submitBatchFn !== undefined) {
|
|
1824
|
+
const batchToSend: IBatchMessage[] = [];
|
|
1825
|
+
for (const message of batch) {
|
|
1826
|
+
batchToSend.push({ contents: message.contents, metadata: message.metadata });
|
|
1827
|
+
}
|
|
1828
|
+
// returns clientSequenceNumber of last message in a batch
|
|
1829
|
+
clientSequenceNumber = this.context.submitBatchFn(batchToSend);
|
|
1830
|
+
} else {
|
|
1831
|
+
// Legacy path - supporting old loader versions. Can be removed only when LTS moves above
|
|
1832
|
+
// version that has support for batches (submitBatchFn)
|
|
1833
|
+
for (const message of batch) {
|
|
1834
|
+
clientSequenceNumber = this.context.submitFn(
|
|
1835
|
+
MessageType.Operation,
|
|
1836
|
+
message.deserializedContent,
|
|
1837
|
+
true, // batch
|
|
1838
|
+
message.metadata);
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
this.deltaSender.flush();
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
// Convert from clientSequenceNumber of last message in the batch to clientSequenceNumber of first message.
|
|
1845
|
+
clientSequenceNumber -= batch.length - 1;
|
|
1846
|
+
assert(clientSequenceNumber >= 0, "clientSequenceNumber can't be negative");
|
|
1776
1847
|
}
|
|
1777
1848
|
|
|
1778
|
-
|
|
1849
|
+
// Let the PendingStateManager know that a message was submitted.
|
|
1850
|
+
// In future, need to shift toward keeping batch as a whole!
|
|
1851
|
+
for (const message of batch) {
|
|
1852
|
+
this.pendingStateManager.onSubmitMessage(
|
|
1853
|
+
message.deserializedContent.type,
|
|
1854
|
+
clientSequenceNumber,
|
|
1855
|
+
message.referenceSequenceNumber,
|
|
1856
|
+
message.deserializedContent.contents,
|
|
1857
|
+
message.localOpMetadata,
|
|
1858
|
+
message.metadata,
|
|
1859
|
+
);
|
|
1860
|
+
clientSequenceNumber++;
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
this.pendingStateManager.onFlush();
|
|
1779
1864
|
}
|
|
1780
1865
|
|
|
1781
1866
|
public orderSequentially(callback: () => void): void {
|
|
@@ -1801,9 +1886,9 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1801
1886
|
}
|
|
1802
1887
|
|
|
1803
1888
|
private trackOrderSequentiallyCalls(callback: () => void): void {
|
|
1804
|
-
let checkpoint: { rollback: () => void; } | undefined;
|
|
1889
|
+
let checkpoint: { rollback: (action: (message: BatchMessage) => void) => void; } | undefined;
|
|
1805
1890
|
if (this.mc.config.getBoolean("Fluid.ContainerRuntime.EnableRollback")) {
|
|
1806
|
-
checkpoint = this.
|
|
1891
|
+
checkpoint = this.batchManager.checkpoint();
|
|
1807
1892
|
}
|
|
1808
1893
|
|
|
1809
1894
|
try {
|
|
@@ -1812,7 +1897,22 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1812
1897
|
} catch (error) {
|
|
1813
1898
|
if (checkpoint) {
|
|
1814
1899
|
// This will throw and close the container if rollback fails
|
|
1815
|
-
|
|
1900
|
+
try {
|
|
1901
|
+
checkpoint.rollback((message: BatchMessage) =>
|
|
1902
|
+
this.rollback(
|
|
1903
|
+
message.deserializedContent.type,
|
|
1904
|
+
message.deserializedContent.contents,
|
|
1905
|
+
message.localOpMetadata));
|
|
1906
|
+
} catch (err) {
|
|
1907
|
+
const error2 = wrapError(err, (message) => {
|
|
1908
|
+
return DataProcessingError.create(
|
|
1909
|
+
`RollbackError: ${message}`,
|
|
1910
|
+
"checkpointRollback",
|
|
1911
|
+
undefined) as DataProcessingError;
|
|
1912
|
+
});
|
|
1913
|
+
this.closeFn(error2);
|
|
1914
|
+
throw error2;
|
|
1915
|
+
}
|
|
1816
1916
|
} else {
|
|
1817
1917
|
// pre-0.58 error message: orderSequentiallyCallbackException
|
|
1818
1918
|
this.closeFn(new GenericError("orderSequentially callback exception", error));
|
|
@@ -1948,7 +2048,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
1948
2048
|
this.emit("attached");
|
|
1949
2049
|
}
|
|
1950
2050
|
|
|
1951
|
-
if (attachState === AttachState.Attached && !this.
|
|
2051
|
+
if (attachState === AttachState.Attached && !this.hasPendingMessages()) {
|
|
1952
2052
|
this.updateDocumentDirtyState(false);
|
|
1953
2053
|
}
|
|
1954
2054
|
this.dataStores.setAttachState(attachState);
|
|
@@ -2216,6 +2316,8 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2216
2316
|
},
|
|
2217
2317
|
);
|
|
2218
2318
|
|
|
2319
|
+
assert(this.batchManager.empty, "Can't trigger summary in the middle of a batch");
|
|
2320
|
+
|
|
2219
2321
|
let latestSnapshotVersionId: string | undefined;
|
|
2220
2322
|
if (refreshLatestAck) {
|
|
2221
2323
|
const latestSnapshotInfo = await this.refreshLatestSummaryAckFromServer(
|
|
@@ -2413,7 +2515,7 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2413
2515
|
|
|
2414
2516
|
let clientSequenceNumber: number;
|
|
2415
2517
|
try {
|
|
2416
|
-
clientSequenceNumber = this.
|
|
2518
|
+
clientSequenceNumber = this.submitSummaryMessage(summaryMessage);
|
|
2417
2519
|
} catch (error) {
|
|
2418
2520
|
return { stage: "upload", ...uploadData, error };
|
|
2419
2521
|
}
|
|
@@ -2472,7 +2574,19 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2472
2574
|
}
|
|
2473
2575
|
}
|
|
2474
2576
|
|
|
2577
|
+
private hasPendingMessages() {
|
|
2578
|
+
return this.pendingStateManager.hasPendingMessages() || !this.batchManager.empty;
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2475
2581
|
private updateDocumentDirtyState(dirty: boolean) {
|
|
2582
|
+
if (this.attachState !== AttachState.Attached) {
|
|
2583
|
+
assert(dirty, "Non-attached container is dirty");
|
|
2584
|
+
} else {
|
|
2585
|
+
// Other way is not true = see this.isContainerMessageDirtyable()
|
|
2586
|
+
assert(!dirty || this.hasPendingMessages(),
|
|
2587
|
+
"if doc is dirty, there has to be pending ops");
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2476
2590
|
if (this.dirtyContainer === dirty) {
|
|
2477
2591
|
return;
|
|
2478
2592
|
}
|
|
@@ -2511,31 +2625,58 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2511
2625
|
|
|
2512
2626
|
private submit(
|
|
2513
2627
|
type: ContainerMessageType,
|
|
2514
|
-
|
|
2628
|
+
contents: any,
|
|
2515
2629
|
localOpMetadata: unknown = undefined,
|
|
2516
|
-
|
|
2630
|
+
metadata: Record<string, unknown> | undefined = undefined,
|
|
2517
2631
|
): void {
|
|
2518
2632
|
this.verifyNotClosed();
|
|
2519
2633
|
|
|
2520
2634
|
// There should be no ops in detached container state!
|
|
2521
2635
|
assert(this.attachState !== AttachState.Detached, 0x132 /* "sending ops in detached container" */);
|
|
2522
2636
|
|
|
2523
|
-
|
|
2524
|
-
|
|
2637
|
+
const deserializedContent: ContainerRuntimeMessage = { type, contents };
|
|
2638
|
+
const serializedContent = JSON.stringify(deserializedContent);
|
|
2525
2639
|
|
|
2526
|
-
if (this.
|
|
2527
|
-
|
|
2640
|
+
if (this.deltaManager.readOnlyInfo.readonly) {
|
|
2641
|
+
this.logger.sendErrorEvent({ eventName: "SubmitOpInReadonly" });
|
|
2642
|
+
}
|
|
2528
2643
|
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2644
|
+
const message: BatchMessage = {
|
|
2645
|
+
contents: serializedContent,
|
|
2646
|
+
deserializedContent,
|
|
2647
|
+
metadata,
|
|
2648
|
+
localOpMetadata,
|
|
2649
|
+
referenceSequenceNumber: this.deltaManager.lastSequenceNumber,
|
|
2650
|
+
};
|
|
2536
2651
|
|
|
2537
|
-
|
|
2538
|
-
|
|
2652
|
+
try {
|
|
2653
|
+
// If this is attach message for new data store, and we are in a batch, send this op out of order
|
|
2654
|
+
// Is it safe:
|
|
2655
|
+
// Yes, this should be safe reordering. Newly created data stores are not visible through API surface.
|
|
2656
|
+
// They become visible only when aliased, or handle to some sub-element of newly created datastore
|
|
2657
|
+
// is stored in some DDS, i.e. only after some other op.
|
|
2658
|
+
// Why:
|
|
2659
|
+
// Attach ops are large, and expensive to process. Plus there are scenarios where a lot of new data
|
|
2660
|
+
// stores are created, causing issues like relay service throttling (too many ops) and catastrophic
|
|
2661
|
+
// failure (batch is too large). Pushing them earlier and outside of main batch should alleviate
|
|
2662
|
+
// these issues.
|
|
2663
|
+
// Cons:
|
|
2664
|
+
// With large batches, relay service may throttle clients. Clients may disconnect while throttled.
|
|
2665
|
+
// This change creates new possibility of a lot of newly created data stores never being referenced
|
|
2666
|
+
// because client died before it had a change to submit the rest of the ops. This will create more
|
|
2667
|
+
// garbage that needs to be collected leveraging GC (Garbage Collection) feature.
|
|
2668
|
+
// Please note that this does not change file format, so it can be disabled in the future if this
|
|
2669
|
+
// optimization no longer makes sense (for example, batch compression may make it less appealing).
|
|
2670
|
+
if (this._flushMode === FlushMode.TurnBased && type === ContainerMessageType.Attach &&
|
|
2671
|
+
this.mc.config.getBoolean("Fluid.ContainerRuntime.disableAttachOpReorder") !== true) {
|
|
2672
|
+
this.flushBatch([message]);
|
|
2673
|
+
} else {
|
|
2674
|
+
this.batchManager.push(message);
|
|
2675
|
+
if (this._flushMode !== FlushMode.TurnBased) {
|
|
2676
|
+
this.flush();
|
|
2677
|
+
} else if (!this.flushTrigger) {
|
|
2678
|
+
this.flushTrigger = true;
|
|
2679
|
+
// Queue a microtask to detect the end of the turn and force a flush.
|
|
2539
2680
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
2540
2681
|
Promise.resolve().then(() => {
|
|
2541
2682
|
this.flushTrigger = false;
|
|
@@ -2543,72 +2684,32 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2543
2684
|
});
|
|
2544
2685
|
}
|
|
2545
2686
|
}
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
content, this._flushMode === FlushMode.TurnBased /* batch */, opMetadataInternal);
|
|
2550
|
-
} else {
|
|
2551
|
-
// If the content length is larger than the client configured message size
|
|
2552
|
-
// instead of splitting the content, we will fail by explicitly closing the container
|
|
2553
|
-
this.closeFn(new GenericError(
|
|
2554
|
-
"OpTooLarge",
|
|
2555
|
-
/* error */ undefined,
|
|
2556
|
-
{
|
|
2557
|
-
length: serializedContent.length,
|
|
2558
|
-
limit: defaultMaxOpSizeInBytes,
|
|
2559
|
-
}));
|
|
2560
|
-
clientSequenceNumber = -1;
|
|
2561
|
-
}
|
|
2687
|
+
} catch (error) {
|
|
2688
|
+
this.closeFn(error as GenericError);
|
|
2689
|
+
throw error;
|
|
2562
2690
|
}
|
|
2563
2691
|
|
|
2564
|
-
|
|
2565
|
-
this.pendingStateManager.onSubmitMessage(
|
|
2566
|
-
type,
|
|
2567
|
-
clientSequenceNumber,
|
|
2568
|
-
this.deltaManager.lastSequenceNumber,
|
|
2569
|
-
content,
|
|
2570
|
-
localOpMetadata,
|
|
2571
|
-
opMetadataInternal,
|
|
2572
|
-
);
|
|
2573
|
-
if (this.isContainerMessageDirtyable(type, content)) {
|
|
2692
|
+
if (this.isContainerMessageDirtyable(type, contents)) {
|
|
2574
2693
|
this.updateDocumentDirtyState(true);
|
|
2575
2694
|
}
|
|
2576
2695
|
}
|
|
2577
2696
|
|
|
2578
|
-
private
|
|
2579
|
-
type: MessageType,
|
|
2580
|
-
contents: any) {
|
|
2697
|
+
private submitSummaryMessage(contents: ISummaryContent) {
|
|
2581
2698
|
this.verifyNotClosed();
|
|
2582
2699
|
assert(this.connected, 0x133 /* "Container disconnected when trying to submit system message" */);
|
|
2583
2700
|
|
|
2584
2701
|
// System message should not be sent in the middle of the batch.
|
|
2585
|
-
|
|
2586
|
-
// That might be not what caller hopes to get, but we can look deeper if telemetry tells us it's a problem.
|
|
2587
|
-
const middleOfBatch = this.flushMode === FlushMode.TurnBased && this.needsFlush;
|
|
2588
|
-
if (middleOfBatch) {
|
|
2589
|
-
this.mc.logger.sendErrorEvent({ eventName: "submitSystemMessageError", type });
|
|
2590
|
-
}
|
|
2591
|
-
|
|
2592
|
-
return this.context.submitFn(
|
|
2593
|
-
type,
|
|
2594
|
-
contents,
|
|
2595
|
-
middleOfBatch);
|
|
2596
|
-
}
|
|
2702
|
+
assert(this.batchManager.empty, "System op in the middle of a batch");
|
|
2597
2703
|
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
return this.context.submitFn(
|
|
2608
|
-
MessageType.Operation,
|
|
2609
|
-
payload,
|
|
2610
|
-
batch,
|
|
2611
|
-
appData);
|
|
2704
|
+
// back-compat: ADO #1385: Make this call unconditional in the future
|
|
2705
|
+
if (this.context.submitSummaryFn !== undefined) {
|
|
2706
|
+
return this.context.submitSummaryFn(contents);
|
|
2707
|
+
} else {
|
|
2708
|
+
return this.context.submitFn(
|
|
2709
|
+
MessageType.Summarize,
|
|
2710
|
+
contents,
|
|
2711
|
+
false); // batch
|
|
2712
|
+
}
|
|
2612
2713
|
}
|
|
2613
2714
|
|
|
2614
2715
|
/**
|
|
@@ -2680,25 +2781,29 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2680
2781
|
summaryLogger: ITelemetryLogger,
|
|
2681
2782
|
) {
|
|
2682
2783
|
const readAndParseBlob = async <T>(id: string) => readAndParse<T>(this.storage, id);
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2784
|
+
// The call to fetch the snapshot is very expensive and not always needed.
|
|
2785
|
+
// It should only be done by the summarizerNode, if required.
|
|
2786
|
+
const snapshotTreeFetcher = async () => {
|
|
2787
|
+
const fetchResult = await this.fetchSnapshotFromStorage(
|
|
2788
|
+
ackHandle,
|
|
2789
|
+
summaryLogger,
|
|
2790
|
+
{
|
|
2791
|
+
eventName: "RefreshLatestSummaryGetSnapshot",
|
|
2792
|
+
ackHandle,
|
|
2793
|
+
summaryRefSeq,
|
|
2794
|
+
fetchLatest: false,
|
|
2795
|
+
});
|
|
2796
|
+
return fetchResult.snapshotTree;
|
|
2797
|
+
};
|
|
2798
|
+
const result = await this.summarizerNode.refreshLatestSummary(
|
|
2799
|
+
proposalHandle,
|
|
2800
|
+
summaryRefSeq,
|
|
2801
|
+
snapshotTreeFetcher,
|
|
2802
|
+
readAndParseBlob,
|
|
2803
|
+
summaryLogger,
|
|
2804
|
+
);
|
|
2805
|
+
|
|
2806
|
+
// Notify the garbage collector so it can update its latest summary state.
|
|
2702
2807
|
await this.garbageCollector.latestSummaryStateRefreshed(result, readAndParseBlob);
|
|
2703
2808
|
}
|
|
2704
2809
|
|
|
@@ -2785,6 +2890,11 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2785
2890
|
throw new UsageError("can't get state when offline load disabled");
|
|
2786
2891
|
}
|
|
2787
2892
|
|
|
2893
|
+
// Flush pending batch.
|
|
2894
|
+
// getPendingLocalState() is only exposed through Container.closeAndGetPendingLocalState(), so it's safe
|
|
2895
|
+
// to close current batch.
|
|
2896
|
+
this.flush();
|
|
2897
|
+
|
|
2788
2898
|
const previousPendingState = this.context.pendingLocalState as IPendingRuntimeState | undefined;
|
|
2789
2899
|
if (previousPendingState) {
|
|
2790
2900
|
return {
|
|
@@ -2874,6 +2984,13 @@ export class ContainerRuntime extends TypedEventEmitter<IContainerRuntimeEvents>
|
|
|
2874
2984
|
// we may not have seen every sequence number (because of system ops) so apply everything once we
|
|
2875
2985
|
// don't have any more saved ops
|
|
2876
2986
|
await this.pendingStateManager.applyStashedOpsAt();
|
|
2987
|
+
|
|
2988
|
+
// If it's not the case, we should take it into account when calculating dirty state.
|
|
2989
|
+
assert(this.context.attachState === AttachState.Attached,
|
|
2990
|
+
"this function is called for attached containers only");
|
|
2991
|
+
if (!this.hasPendingMessages()) {
|
|
2992
|
+
this.updateDocumentDirtyState(false);
|
|
2993
|
+
}
|
|
2877
2994
|
}
|
|
2878
2995
|
|
|
2879
2996
|
private validateSummaryHeuristicConfiguration(configuration: ISummaryConfigurationHeuristics) {
|