@fluidframework/container-loader 2.0.0-dev.1.4.5.105745 → 2.0.0-dev.2.2.0.111723
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/.eslintrc.js +21 -8
- package/README.md +21 -11
- package/dist/audience.d.ts +0 -4
- package/dist/audience.d.ts.map +1 -1
- package/dist/audience.js +6 -11
- package/dist/audience.js.map +1 -1
- package/dist/connectionManager.d.ts.map +1 -1
- package/dist/connectionManager.js +46 -7
- package/dist/connectionManager.js.map +1 -1
- package/dist/connectionStateHandler.d.ts +20 -11
- package/dist/connectionStateHandler.d.ts.map +1 -1
- package/dist/connectionStateHandler.js +65 -36
- package/dist/connectionStateHandler.js.map +1 -1
- package/dist/container.d.ts +0 -2
- package/dist/container.d.ts.map +1 -1
- package/dist/container.js +28 -31
- package/dist/container.js.map +1 -1
- package/dist/containerContext.d.ts +1 -1
- package/dist/containerContext.d.ts.map +1 -1
- package/dist/containerContext.js +2 -2
- package/dist/containerContext.js.map +1 -1
- package/dist/containerStorageAdapter.js +1 -1
- package/dist/containerStorageAdapter.js.map +1 -1
- package/dist/deltaManager.d.ts +1 -3
- package/dist/deltaManager.d.ts.map +1 -1
- package/dist/deltaManager.js +37 -13
- package/dist/deltaManager.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/protocol.d.ts +8 -3
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +34 -8
- package/dist/protocol.js.map +1 -1
- package/dist/retriableDocumentStorageService.d.ts.map +1 -1
- package/dist/retriableDocumentStorageService.js +7 -3
- package/dist/retriableDocumentStorageService.js.map +1 -1
- package/lib/audience.d.ts +0 -4
- package/lib/audience.d.ts.map +1 -1
- package/lib/audience.js +6 -11
- package/lib/audience.js.map +1 -1
- package/lib/connectionManager.d.ts.map +1 -1
- package/lib/connectionManager.js +46 -7
- package/lib/connectionManager.js.map +1 -1
- package/lib/connectionStateHandler.d.ts +20 -11
- package/lib/connectionStateHandler.d.ts.map +1 -1
- package/lib/connectionStateHandler.js +65 -36
- package/lib/connectionStateHandler.js.map +1 -1
- package/lib/container.d.ts +0 -2
- package/lib/container.d.ts.map +1 -1
- package/lib/container.js +28 -31
- package/lib/container.js.map +1 -1
- package/lib/containerContext.d.ts +1 -1
- package/lib/containerContext.d.ts.map +1 -1
- package/lib/containerContext.js +2 -2
- package/lib/containerContext.js.map +1 -1
- package/lib/containerStorageAdapter.js +1 -1
- package/lib/containerStorageAdapter.js.map +1 -1
- package/lib/deltaManager.d.ts +1 -3
- package/lib/deltaManager.d.ts.map +1 -1
- package/lib/deltaManager.js +40 -16
- package/lib/deltaManager.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/protocol.d.ts +8 -3
- package/lib/protocol.d.ts.map +1 -1
- package/lib/protocol.js +33 -7
- package/lib/protocol.js.map +1 -1
- package/lib/retriableDocumentStorageService.d.ts.map +1 -1
- package/lib/retriableDocumentStorageService.js +7 -3
- package/lib/retriableDocumentStorageService.js.map +1 -1
- package/package.json +27 -29
- package/prettier.config.cjs +8 -0
- package/src/audience.ts +6 -12
- package/src/connectionManager.ts +52 -10
- package/src/connectionStateHandler.ts +87 -39
- package/src/container.ts +33 -33
- package/src/containerContext.ts +2 -2
- package/src/containerStorageAdapter.ts +1 -1
- package/src/deltaManager.ts +58 -36
- package/src/packageVersion.ts +1 -1
- package/src/protocol.ts +31 -8
- package/src/retriableDocumentStorageService.ts +7 -3
package/src/container.ts
CHANGED
|
@@ -64,7 +64,6 @@ import {
|
|
|
64
64
|
ISequencedClient,
|
|
65
65
|
ISequencedDocumentMessage,
|
|
66
66
|
ISequencedProposal,
|
|
67
|
-
ISignalClient,
|
|
68
67
|
ISignalMessage,
|
|
69
68
|
ISnapshotTree,
|
|
70
69
|
ISummaryContent,
|
|
@@ -412,7 +411,6 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
|
|
|
412
411
|
private readonly clientDetailsOverride: IClientDetails | undefined;
|
|
413
412
|
private readonly _deltaManager: DeltaManager<ConnectionManager>;
|
|
414
413
|
private service: IDocumentService | undefined;
|
|
415
|
-
private _initialClients: ISignalClient[] | undefined;
|
|
416
414
|
|
|
417
415
|
private _context: ContainerContext | undefined;
|
|
418
416
|
private get context() {
|
|
@@ -619,7 +617,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
|
|
|
619
617
|
this.mc = loggerToMonitoringContext(ChildLogger.create(this.subLogger, "Container"));
|
|
620
618
|
|
|
621
619
|
const summarizeProtocolTree =
|
|
622
|
-
this.mc.config.getBoolean("Fluid.Container.
|
|
620
|
+
this.mc.config.getBoolean("Fluid.Container.summarizeProtocolTree2")
|
|
623
621
|
?? this.loader.services.options.summarizeProtocolTree;
|
|
624
622
|
|
|
625
623
|
this.options = {
|
|
@@ -639,19 +637,37 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
|
|
|
639
637
|
}
|
|
640
638
|
this.logConnectionStateChangeTelemetry(value, oldState, reason);
|
|
641
639
|
if (this._lifecycleState === "loaded") {
|
|
642
|
-
this.propagateConnectionState(
|
|
640
|
+
this.propagateConnectionState(
|
|
641
|
+
false /* initial transition */,
|
|
642
|
+
value === ConnectionState.Disconnected ? reason : undefined /* disconnectedReason */,
|
|
643
|
+
);
|
|
643
644
|
}
|
|
644
645
|
},
|
|
645
646
|
shouldClientJoinWrite: () => this._deltaManager.connectionManager.shouldJoinWrite(),
|
|
646
647
|
maxClientLeaveWaitTime: this.loader.services.options.maxClientLeaveWaitTime,
|
|
647
648
|
logConnectionIssue: (eventName: string, details?: ITelemetryProperties) => {
|
|
649
|
+
const mode = this.connectionMode;
|
|
648
650
|
// We get here when socket does not receive any ops on "write" connection, including
|
|
649
651
|
// its own join op. Attempt recovery option.
|
|
650
652
|
this._deltaManager.logConnectionIssue({
|
|
651
653
|
eventName,
|
|
654
|
+
mode,
|
|
652
655
|
duration: performance.now() - this.connectionTransitionTimes[ConnectionState.CatchingUp],
|
|
653
656
|
...(details === undefined ? {} : { details: JSON.stringify(details) }),
|
|
654
657
|
});
|
|
658
|
+
|
|
659
|
+
// If this is "write" connection, it took too long to receive join op. But in most cases that's due
|
|
660
|
+
// to very slow op fetches and we will eventually get there.
|
|
661
|
+
// For "read" connections, we get here due to self join signal not arriving on time. We will need to
|
|
662
|
+
// better understand when and why it may happen.
|
|
663
|
+
// For now, attempt to recover by reconnecting. In future, maybe we can query relay service for
|
|
664
|
+
// current state of audience.
|
|
665
|
+
// Other possible recovery path - move to connected state (i.e. ConnectionStateHandler.joinOpTimer
|
|
666
|
+
// to call this.applyForConnectedState("addMemberEvent") for "read" connections)
|
|
667
|
+
if (mode === "read") {
|
|
668
|
+
this.disconnect();
|
|
669
|
+
this.connect();
|
|
670
|
+
}
|
|
655
671
|
},
|
|
656
672
|
},
|
|
657
673
|
this.deltaManager,
|
|
@@ -1383,11 +1399,8 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
|
|
|
1383
1399
|
attributes,
|
|
1384
1400
|
quorumSnapshot,
|
|
1385
1401
|
(key, value) => this.submitMessage(MessageType.Propose, JSON.stringify({ key, value })),
|
|
1386
|
-
this._initialClients ?? [],
|
|
1387
1402
|
);
|
|
1388
1403
|
|
|
1389
|
-
this._initialClients = undefined;
|
|
1390
|
-
|
|
1391
1404
|
const protocolLogger = ChildLogger.create(this.subLogger, "ProtocolHandler");
|
|
1392
1405
|
|
|
1393
1406
|
protocol.quorum.on("error", (error) => {
|
|
@@ -1512,22 +1525,8 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
|
|
|
1512
1525
|
deltaManager.inboundSignal.pause();
|
|
1513
1526
|
|
|
1514
1527
|
deltaManager.on("connect", (details: IConnectionDetails, _opsBehind?: number) => {
|
|
1515
|
-
|
|
1516
|
-
// Store the initial clients so that they can be submitted to the
|
|
1517
|
-
// protocol handler when it is created.
|
|
1518
|
-
this._initialClients = details.initialClients;
|
|
1519
|
-
} else {
|
|
1520
|
-
// When reconnecting, the protocol handler is already created,
|
|
1521
|
-
// so we can update the audience right now.
|
|
1522
|
-
this._protocolHandler.audience.clear();
|
|
1523
|
-
|
|
1524
|
-
for (const priorClient of details.initialClients ?? []) {
|
|
1525
|
-
this._protocolHandler.audience.addMember(priorClient.clientId, priorClient.client);
|
|
1526
|
-
}
|
|
1527
|
-
}
|
|
1528
|
-
|
|
1528
|
+
assert(this.connectionMode === details.mode, "mismatch");
|
|
1529
1529
|
this.connectionStateHandler.receivedConnectEvent(
|
|
1530
|
-
this.connectionMode,
|
|
1531
1530
|
details,
|
|
1532
1531
|
);
|
|
1533
1532
|
});
|
|
@@ -1542,7 +1541,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
|
|
|
1542
1541
|
// Some "warning" events come from outside the container and are logged
|
|
1543
1542
|
// elsewhere (e.g. summarizing container). We shouldn't log these here.
|
|
1544
1543
|
if (warn.logged !== true) {
|
|
1545
|
-
this.
|
|
1544
|
+
this.mc.logger.sendTelemetryEvent({ eventName: "ContainerWarning" }, warn);
|
|
1546
1545
|
}
|
|
1547
1546
|
this.emit("warning", warn);
|
|
1548
1547
|
});
|
|
@@ -1629,7 +1628,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
|
|
|
1629
1628
|
}
|
|
1630
1629
|
}
|
|
1631
1630
|
|
|
1632
|
-
private propagateConnectionState(initialTransition: boolean) {
|
|
1631
|
+
private propagateConnectionState(initialTransition: boolean, disconnectedReason?: string) {
|
|
1633
1632
|
// When container loaded, we want to propagate initial connection state.
|
|
1634
1633
|
// After that, we communicate only transitions to Connected & Disconnected states, skipping all other states.
|
|
1635
1634
|
// This can be changed in the future, for example we likely should add "CatchingUp" event on Container.
|
|
@@ -1652,7 +1651,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
|
|
|
1652
1651
|
|
|
1653
1652
|
this.setContextConnectedState(state, this._deltaManager.connectionManager.readOnlyInfo.readonly ?? false);
|
|
1654
1653
|
this.protocolHandler.setConnectionState(state, this.clientId);
|
|
1655
|
-
raiseConnectedEvent(this.mc.logger, this, state, this.clientId);
|
|
1654
|
+
raiseConnectedEvent(this.mc.logger, this, state, this.clientId, disconnectedReason);
|
|
1656
1655
|
|
|
1657
1656
|
if (logOpsOnReconnect) {
|
|
1658
1657
|
this.mc.logger.sendTelemetryEvent(
|
|
@@ -1687,7 +1686,8 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
|
|
|
1687
1686
|
MessageType.Operation,
|
|
1688
1687
|
message.contents,
|
|
1689
1688
|
true, // batch
|
|
1690
|
-
message.metadata
|
|
1689
|
+
message.metadata,
|
|
1690
|
+
message.compression);
|
|
1691
1691
|
}
|
|
1692
1692
|
this._deltaManager.flush();
|
|
1693
1693
|
return clientSequenceNumber;
|
|
@@ -1706,7 +1706,11 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
|
|
|
1706
1706
|
return this.submitMessage(MessageType.Summarize, JSON.stringify(summary), false /* batch */);
|
|
1707
1707
|
}
|
|
1708
1708
|
|
|
1709
|
-
private submitMessage(type: MessageType,
|
|
1709
|
+
private submitMessage(type: MessageType,
|
|
1710
|
+
contents?: string,
|
|
1711
|
+
batch?: boolean,
|
|
1712
|
+
metadata?: any,
|
|
1713
|
+
compression?: string): number {
|
|
1710
1714
|
if (this.connectionState !== ConnectionState.Connected) {
|
|
1711
1715
|
this.mc.logger.sendErrorEvent({ eventName: "SubmitMessageWithNoConnection", type });
|
|
1712
1716
|
return -1;
|
|
@@ -1714,7 +1718,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
|
|
|
1714
1718
|
|
|
1715
1719
|
this.messageCountAfterDisconnection += 1;
|
|
1716
1720
|
this.collabWindowTracker?.stopSequenceNumberUpdate();
|
|
1717
|
-
return this._deltaManager.submit(type, contents, batch, metadata);
|
|
1721
|
+
return this._deltaManager.submit(type, contents, batch, metadata, compression);
|
|
1718
1722
|
}
|
|
1719
1723
|
|
|
1720
1724
|
private processRemoteMessage(message: ISequencedDocumentMessage) {
|
|
@@ -1724,7 +1728,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
|
|
|
1724
1728
|
const result = this.protocolHandler.processMessage(message, local);
|
|
1725
1729
|
|
|
1726
1730
|
// Forward messages to the loaded runtime for processing
|
|
1727
|
-
this.context.process(message, local
|
|
1731
|
+
this.context.process(message, local);
|
|
1728
1732
|
|
|
1729
1733
|
// Inactive (not in quorum or not writers) clients don't take part in the minimum sequence number calculation.
|
|
1730
1734
|
if (this.activeConnection()) {
|
|
@@ -1846,10 +1850,6 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
|
|
|
1846
1850
|
this.emit(dirty ? dirtyContainerEvent : savedContainerEvent);
|
|
1847
1851
|
}
|
|
1848
1852
|
|
|
1849
|
-
private logContainerError(warning: ContainerWarning) {
|
|
1850
|
-
this.mc.logger.sendErrorEvent({ eventName: "ContainerWarning" }, warning);
|
|
1851
|
-
}
|
|
1852
|
-
|
|
1853
1853
|
/**
|
|
1854
1854
|
* Set the connected state of the ContainerContext
|
|
1855
1855
|
* This controls the "connected" state of the ContainerRuntime as well
|
package/src/containerContext.ts
CHANGED
|
@@ -243,8 +243,8 @@ export class ContainerContext implements IContainerContext {
|
|
|
243
243
|
runtime.setConnectionState(connected, clientId);
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
-
public process(message: ISequencedDocumentMessage, local: boolean
|
|
247
|
-
this.runtime.process(message, local
|
|
246
|
+
public process(message: ISequencedDocumentMessage, local: boolean) {
|
|
247
|
+
this.runtime.process(message, local);
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
public processSignal(message: ISignalMessage, local: boolean) {
|
|
@@ -176,7 +176,7 @@ class BlobOnlyStorage implements IDocumentStorageService {
|
|
|
176
176
|
// some browsers may not populate stack unless exception is thrown
|
|
177
177
|
throw new Error("BlobOnlyStorage not implemented method used");
|
|
178
178
|
} catch (err) {
|
|
179
|
-
this.logger.
|
|
179
|
+
this.logger.sendTelemetryEvent({ eventName: "BlobOnlyStorageWrongCall" }, err);
|
|
180
180
|
throw err;
|
|
181
181
|
}
|
|
182
182
|
}
|
package/src/deltaManager.ts
CHANGED
|
@@ -25,8 +25,6 @@ import {
|
|
|
25
25
|
normalizeError,
|
|
26
26
|
logIfFalse,
|
|
27
27
|
safeRaiseEvent,
|
|
28
|
-
MonitoringContext,
|
|
29
|
-
loggerToMonitoringContext,
|
|
30
28
|
} from "@fluidframework/telemetry-utils";
|
|
31
29
|
import {
|
|
32
30
|
IDocumentDeltaStorageService,
|
|
@@ -42,20 +40,20 @@ import {
|
|
|
42
40
|
} from "@fluidframework/protocol-definitions";
|
|
43
41
|
import {
|
|
44
42
|
NonRetryableError,
|
|
45
|
-
|
|
43
|
+
isRuntimeMessage,
|
|
44
|
+
MessageType2,
|
|
46
45
|
} from "@fluidframework/driver-utils";
|
|
47
46
|
import {
|
|
48
47
|
ThrottlingWarning,
|
|
49
48
|
DataCorruptionError,
|
|
50
49
|
extractSafePropertiesFromMessage,
|
|
51
50
|
DataProcessingError,
|
|
52
|
-
UsageError,
|
|
53
51
|
} from "@fluidframework/container-utils";
|
|
54
52
|
import { DeltaQueue } from "./deltaQueue";
|
|
55
53
|
import {
|
|
56
54
|
IConnectionManagerFactoryArgs,
|
|
57
55
|
IConnectionManager,
|
|
58
|
-
|
|
56
|
+
} from "./contracts";
|
|
59
57
|
|
|
60
58
|
export interface IConnectionArgs {
|
|
61
59
|
mode?: ConnectionMode;
|
|
@@ -72,6 +70,25 @@ export interface IDeltaManagerInternalEvents extends IDeltaManagerEvents {
|
|
|
72
70
|
(event: "closed", listener: (error?: ICriticalContainerError) => void);
|
|
73
71
|
}
|
|
74
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Determines if message was sent by client, not service
|
|
75
|
+
*/
|
|
76
|
+
function isClientMessage(message: ISequencedDocumentMessage | IDocumentMessage): boolean {
|
|
77
|
+
if (isRuntimeMessage(message)) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
switch (message.type) {
|
|
81
|
+
case MessageType.Propose:
|
|
82
|
+
case MessageType.Reject:
|
|
83
|
+
case MessageType.NoOp:
|
|
84
|
+
case MessageType2.Accept:
|
|
85
|
+
case MessageType.Summarize:
|
|
86
|
+
return true;
|
|
87
|
+
default:
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
75
92
|
/**
|
|
76
93
|
* Manages the flow of both inbound and outbound messages. This class ensures that shared objects receive delta
|
|
77
94
|
* messages in order regardless of possible network conditions or timings causing out of order delivery.
|
|
@@ -92,15 +109,9 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
|
|
|
92
109
|
private pending: ISequencedDocumentMessage[] = [];
|
|
93
110
|
private fetchReason: string | undefined;
|
|
94
111
|
|
|
95
|
-
private readonly mc: MonitoringContext;
|
|
96
|
-
|
|
97
112
|
// A boolean used to assert that ops are not being sent while processing another op.
|
|
98
113
|
private currentlyProcessingOps: boolean = false;
|
|
99
114
|
|
|
100
|
-
// Feature gate that closes a container when sending an op if the container is
|
|
101
|
-
// concurrently processing another op
|
|
102
|
-
private readonly preventConcurrentOpSend: boolean = true;
|
|
103
|
-
|
|
104
115
|
// The minimum sequence number and last sequence number received from the server
|
|
105
116
|
private minSequenceNumber: number = 0;
|
|
106
117
|
|
|
@@ -185,7 +196,7 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
|
|
|
185
196
|
* Tells if current connection has checkpoint information.
|
|
186
197
|
* I.e. we know how far behind the client was at the time of establishing connection
|
|
187
198
|
*/
|
|
188
|
-
|
|
199
|
+
public get hasCheckpointSequenceNumber() {
|
|
189
200
|
// Valid to be called only if we have active connection.
|
|
190
201
|
assert(this.connectionManager.connected, 0x0df /* "Missing active connection" */);
|
|
191
202
|
return this._checkpointSequenceNumber !== undefined;
|
|
@@ -199,15 +210,13 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
|
|
|
199
210
|
public get readOnlyInfo() { return this.connectionManager.readOnlyInfo; }
|
|
200
211
|
public get clientDetails() { return this.connectionManager.clientDetails; }
|
|
201
212
|
|
|
202
|
-
public submit(type: MessageType, contents?: string, batch = false, metadata?: any) {
|
|
203
|
-
if (this.currentlyProcessingOps && this.preventConcurrentOpSend) {
|
|
204
|
-
this.close(new UsageError("Making changes to data model is disallowed while processing ops."));
|
|
205
|
-
}
|
|
213
|
+
public submit(type: MessageType, contents?: string, batch = false, metadata?: any, compression?: string) {
|
|
206
214
|
const messagePartial: Omit<IDocumentMessage, "clientSequenceNumber"> = {
|
|
207
215
|
contents,
|
|
208
216
|
metadata,
|
|
209
217
|
referenceSequenceNumber: this.lastProcessedSequenceNumber,
|
|
210
218
|
type,
|
|
219
|
+
compression,
|
|
211
220
|
};
|
|
212
221
|
|
|
213
222
|
if (!batch) {
|
|
@@ -218,6 +227,8 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
|
|
|
218
227
|
return -1;
|
|
219
228
|
}
|
|
220
229
|
|
|
230
|
+
assert(isClientMessage(message), 0x419 /* client sends non-client message */);
|
|
231
|
+
|
|
221
232
|
if (contents !== undefined) {
|
|
222
233
|
this.opsSize += contents.length;
|
|
223
234
|
}
|
|
@@ -321,8 +332,6 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
|
|
|
321
332
|
};
|
|
322
333
|
|
|
323
334
|
this.connectionManager = createConnectionManager(props);
|
|
324
|
-
this.mc = loggerToMonitoringContext(logger);
|
|
325
|
-
this.preventConcurrentOpSend = this.mc.config.getBoolean("Fluid.Container.ConcurrentOpSend") === true;
|
|
326
335
|
this._inbound = new DeltaQueue<ISequencedDocumentMessage>(
|
|
327
336
|
(op) => {
|
|
328
337
|
this.processInboundMessage(op);
|
|
@@ -390,7 +399,7 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
|
|
|
390
399
|
if (checkpointSequenceNumber > this.lastQueuedSequenceNumber) {
|
|
391
400
|
this.fetchMissingDeltas("AfterConnection");
|
|
392
401
|
}
|
|
393
|
-
|
|
402
|
+
// we do not know the gap, and we will not learn about it if socket is quite - have to ask.
|
|
394
403
|
} else if (connection.mode === "read") {
|
|
395
404
|
this.fetchMissingDeltas("AfterReadConnection");
|
|
396
405
|
}
|
|
@@ -708,9 +717,9 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
|
|
|
708
717
|
// Report if we found some issues
|
|
709
718
|
if (duplicate !== 0 || gap !== 0 && !allowGaps || initialGap > 0 && this.fetchReason === undefined) {
|
|
710
719
|
eventName = "enqueueMessages";
|
|
711
|
-
|
|
720
|
+
// Also report if we are fetching ops, and same range comes in, thus making this fetch obsolete.
|
|
712
721
|
} else if (this.fetchReason !== undefined && this.fetchReason !== reason &&
|
|
713
|
-
|
|
722
|
+
(from <= this.lastQueuedSequenceNumber + 1 && last > this.lastQueuedSequenceNumber)) {
|
|
714
723
|
eventName = "enqueueMessagesExtraFetch";
|
|
715
724
|
}
|
|
716
725
|
|
|
@@ -795,13 +804,16 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
|
|
|
795
804
|
this.currentlyProcessingOps = true;
|
|
796
805
|
this.lastProcessedMessage = message;
|
|
797
806
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
)
|
|
807
|
+
const isString = typeof message.clientId === "string";
|
|
808
|
+
assert(message.clientId === null || isString, 0x41a /* undefined or string */);
|
|
809
|
+
// All client messages are coming from some client, and should have clientId,
|
|
810
|
+
// and non-client message should not have clientId. But, there are two exceptions:
|
|
811
|
+
// 1. (Legacy) We can see message.type === "attach" or "chunkedOp" for legacy files before RTM
|
|
812
|
+
// 2. Non-immediate noops (contents: null) can be sent by service without clientId
|
|
813
|
+
if (!isString && isClientMessage(message) && message.type !== MessageType.NoOp) {
|
|
814
|
+
throw new DataCorruptionError("Mismatch in clientId",
|
|
815
|
+
{ ...extractSafePropertiesFromMessage(message), messageType: message.type });
|
|
816
|
+
}
|
|
805
817
|
|
|
806
818
|
// TODO Remove after SPO picks up the latest build.
|
|
807
819
|
if (
|
|
@@ -822,6 +834,16 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
|
|
|
822
834
|
clientId: this.connectionManager.clientId,
|
|
823
835
|
});
|
|
824
836
|
}
|
|
837
|
+
|
|
838
|
+
// Client ops: MSN has to be lower than sequence #, as client can continue to send ops with same
|
|
839
|
+
// reference sequence number as this op.
|
|
840
|
+
// System ops (when no clients are connected) are the only ops where equation is possible.
|
|
841
|
+
const diff = message.sequenceNumber - message.minimumSequenceNumber;
|
|
842
|
+
if (diff < 0 || diff === 0 && message.clientId !== null) {
|
|
843
|
+
throw new DataCorruptionError("MSN has to be lower than sequence #",
|
|
844
|
+
extractSafePropertiesFromMessage(message));
|
|
845
|
+
}
|
|
846
|
+
|
|
825
847
|
this.minSequenceNumber = message.minimumSequenceNumber;
|
|
826
848
|
|
|
827
849
|
if (message.sequenceNumber !== this.lastProcessedSequenceNumber + 1) {
|
|
@@ -858,15 +880,15 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
|
|
|
858
880
|
/**
|
|
859
881
|
* Retrieves the missing deltas between the given sequence numbers
|
|
860
882
|
*/
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
883
|
+
private fetchMissingDeltas(reasonArg: string, to?: number) {
|
|
884
|
+
this.fetchMissingDeltasCore(reasonArg, false /* cacheOnly */, to).catch((error) => {
|
|
885
|
+
this.logger.sendErrorEvent({ eventName: "fetchMissingDeltasException" }, error);
|
|
886
|
+
});
|
|
887
|
+
}
|
|
866
888
|
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
889
|
+
/**
|
|
890
|
+
* Retrieves the missing deltas between the given sequence numbers
|
|
891
|
+
*/
|
|
870
892
|
private async fetchMissingDeltasCore(
|
|
871
893
|
reason: string,
|
|
872
894
|
cacheOnly: boolean,
|
package/src/packageVersion.ts
CHANGED
package/src/protocol.ts
CHANGED
|
@@ -20,6 +20,13 @@ import {
|
|
|
20
20
|
} from "@fluidframework/protocol-definitions";
|
|
21
21
|
import { canBeCoalescedByService } from "@fluidframework/driver-utils";
|
|
22
22
|
|
|
23
|
+
// ADO: #1986: Start using enum from protocol-base.
|
|
24
|
+
export enum SignalType {
|
|
25
|
+
ClientJoin = "join", // same value as MessageType.ClientJoin,
|
|
26
|
+
ClientLeave = "leave", // same value as MessageType.ClientLeave,
|
|
27
|
+
Clear = "clear", // used only by client for synthetic signals
|
|
28
|
+
}
|
|
29
|
+
|
|
23
30
|
/**
|
|
24
31
|
* Function to be used for creating a protocol handler.
|
|
25
32
|
*/
|
|
@@ -27,7 +34,6 @@ export type ProtocolHandlerBuilder = (
|
|
|
27
34
|
attributes: IDocumentAttributes,
|
|
28
35
|
snapshot: IQuorumSnapshot,
|
|
29
36
|
sendProposal: (key: string, value: any) => number,
|
|
30
|
-
initialClients: ISignalClient[],
|
|
31
37
|
) => IProtocolHandler;
|
|
32
38
|
|
|
33
39
|
export interface IProtocolHandler extends IBaseProtocolHandler {
|
|
@@ -40,7 +46,6 @@ export class ProtocolHandler extends ProtocolOpHandler implements IProtocolHandl
|
|
|
40
46
|
attributes: IDocumentAttributes,
|
|
41
47
|
quorumSnapshot: IQuorumSnapshot,
|
|
42
48
|
sendProposal: (key: string, value: any) => number,
|
|
43
|
-
initialClients: ISignalClient[],
|
|
44
49
|
readonly audience: IAudienceOwner,
|
|
45
50
|
) {
|
|
46
51
|
super(
|
|
@@ -53,8 +58,11 @@ export class ProtocolHandler extends ProtocolOpHandler implements IProtocolHandl
|
|
|
53
58
|
sendProposal,
|
|
54
59
|
);
|
|
55
60
|
|
|
56
|
-
for
|
|
57
|
-
|
|
61
|
+
// Join / leave signals are ignored for "write" clients in favor of join / leave ops
|
|
62
|
+
this.quorum.on("addMember", (clientId, details) => audience.addMember(clientId, details.client));
|
|
63
|
+
this.quorum.on("removeMember", (clientId) => audience.removeMember(clientId));
|
|
64
|
+
for (const [clientId, details] of this.quorum.getMembers()) {
|
|
65
|
+
this.audience.addMember(clientId, details.client);
|
|
58
66
|
}
|
|
59
67
|
}
|
|
60
68
|
|
|
@@ -81,14 +89,29 @@ export class ProtocolHandler extends ProtocolOpHandler implements IProtocolHandl
|
|
|
81
89
|
public processSignal(message: ISignalMessage) {
|
|
82
90
|
const innerContent = message.content as { content: any; type: string; };
|
|
83
91
|
switch (innerContent.type) {
|
|
84
|
-
case
|
|
92
|
+
case SignalType.Clear: {
|
|
93
|
+
const members = this.audience.getMembers();
|
|
94
|
+
for (const [clientId, client] of members) {
|
|
95
|
+
if (client.mode === "read") {
|
|
96
|
+
this.audience.removeMember(clientId);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
case SignalType.ClientJoin: {
|
|
85
102
|
const newClient = innerContent.content as ISignalClient;
|
|
86
|
-
|
|
103
|
+
// Ignore write clients - quorum will control such clients.
|
|
104
|
+
if (newClient.client.mode === "read") {
|
|
105
|
+
this.audience.addMember(newClient.clientId, newClient.client);
|
|
106
|
+
}
|
|
87
107
|
break;
|
|
88
108
|
}
|
|
89
|
-
case
|
|
109
|
+
case SignalType.ClientLeave: {
|
|
90
110
|
const leftClientId = innerContent.content as string;
|
|
91
|
-
|
|
111
|
+
// Ignore write clients - quorum will control such clients.
|
|
112
|
+
if (this.audience.getMember(leftClientId)?.mode === "read") {
|
|
113
|
+
this.audience.removeMember(leftClientId);
|
|
114
|
+
}
|
|
92
115
|
break;
|
|
93
116
|
}
|
|
94
117
|
default: break;
|
|
@@ -104,12 +104,16 @@ export class RetriableDocumentStorageService implements IDocumentStorageService,
|
|
|
104
104
|
);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
private checkStorageDisposed() {
|
|
107
|
+
private checkStorageDisposed(callName: string, error: unknown) {
|
|
108
108
|
if (this._disposed) {
|
|
109
|
+
this.logger.sendTelemetryEvent({
|
|
110
|
+
eventName: `${callName}_abortedStorageDisposed`,
|
|
111
|
+
fetchCallName: callName, // fetchCallName matches logs in runWithRetry.ts
|
|
112
|
+
}, error);
|
|
109
113
|
// pre-0.58 error message: storageServiceDisposedCannotRetry
|
|
110
114
|
throw new GenericError("Storage Service is disposed. Cannot retry", { canRetry: false });
|
|
111
115
|
}
|
|
112
|
-
return
|
|
116
|
+
return;
|
|
113
117
|
}
|
|
114
118
|
|
|
115
119
|
private async runWithRetry<T>(api: () => Promise<T>, callName: string): Promise<T> {
|
|
@@ -118,7 +122,7 @@ export class RetriableDocumentStorageService implements IDocumentStorageService,
|
|
|
118
122
|
callName,
|
|
119
123
|
this.logger,
|
|
120
124
|
{
|
|
121
|
-
onRetry: () => this.checkStorageDisposed(),
|
|
125
|
+
onRetry: (_delayInMs: number, error: unknown) => this.checkStorageDisposed(callName, error),
|
|
122
126
|
},
|
|
123
127
|
);
|
|
124
128
|
}
|