@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fluidframework/container-loader",
|
|
3
|
-
"version": "2.0.0-dev.
|
|
3
|
+
"version": "2.0.0-dev.2.2.0.111723",
|
|
4
4
|
"description": "Fluid container loader",
|
|
5
5
|
"homepage": "https://fluidframework.com",
|
|
6
6
|
"repository": {
|
|
@@ -28,17 +28,19 @@
|
|
|
28
28
|
"clean": "rimraf dist lib *.tsbuildinfo *.build.log",
|
|
29
29
|
"eslint": "eslint --format stylish src",
|
|
30
30
|
"eslint:fix": "eslint --format stylish src --fix --fix-type problem,suggestion,layout",
|
|
31
|
+
"format": "npm run prettier:fix",
|
|
31
32
|
"lint": "npm run eslint",
|
|
32
33
|
"lint:fix": "npm run eslint:fix",
|
|
34
|
+
"prettier": "prettier --check . --ignore-path ../../../.prettierignore",
|
|
35
|
+
"prettier:fix": "prettier --write . --ignore-path ../../../.prettierignore",
|
|
33
36
|
"test": "npm run test:mocha",
|
|
34
37
|
"test:coverage": "nyc npm test -- --reporter xunit --reporter-option output=nyc/junit-report.xml",
|
|
35
38
|
"test:mocha": "mocha --ignore 'dist/test/types/*' --recursive dist/test -r node_modules/@fluidframework/mocha-test-setup --unhandled-rejections=strict",
|
|
36
39
|
"test:mocha:verbose": "cross-env FLUID_TEST_VERBOSE=1 npm run test:mocha",
|
|
37
40
|
"tsc": "tsc",
|
|
38
41
|
"tsc:watch": "tsc --watch",
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"typetests:gen": "fluid-type-validator -g -d ."
|
|
42
|
+
"typetests:gen": "flub generate typetests --generate --dir .",
|
|
43
|
+
"typetests:prepare": "flub generate typetests --prepare --dir . --pin"
|
|
42
44
|
},
|
|
43
45
|
"nyc": {
|
|
44
46
|
"all": true,
|
|
@@ -63,14 +65,14 @@
|
|
|
63
65
|
"dependencies": {
|
|
64
66
|
"@fluidframework/common-definitions": "^0.20.1",
|
|
65
67
|
"@fluidframework/common-utils": "^1.0.0",
|
|
66
|
-
"@fluidframework/container-definitions": "2.0.0-dev.
|
|
67
|
-
"@fluidframework/container-utils": "2.0.0-dev.
|
|
68
|
-
"@fluidframework/core-interfaces": "2.0.0-dev.
|
|
69
|
-
"@fluidframework/driver-definitions": "2.0.0-dev.
|
|
70
|
-
"@fluidframework/driver-utils": "2.0.0-dev.
|
|
71
|
-
"@fluidframework/protocol-base": "^0.
|
|
72
|
-
"@fluidframework/protocol-definitions": "^1.
|
|
73
|
-
"@fluidframework/telemetry-utils": "2.0.0-dev.
|
|
68
|
+
"@fluidframework/container-definitions": ">=2.0.0-dev.2.2.0.111723 <2.0.0-dev.3.0.0",
|
|
69
|
+
"@fluidframework/container-utils": ">=2.0.0-dev.2.2.0.111723 <2.0.0-dev.3.0.0",
|
|
70
|
+
"@fluidframework/core-interfaces": ">=2.0.0-dev.2.2.0.111723 <2.0.0-dev.3.0.0",
|
|
71
|
+
"@fluidframework/driver-definitions": ">=2.0.0-dev.2.2.0.111723 <2.0.0-dev.3.0.0",
|
|
72
|
+
"@fluidframework/driver-utils": ">=2.0.0-dev.2.2.0.111723 <2.0.0-dev.3.0.0",
|
|
73
|
+
"@fluidframework/protocol-base": "^0.1038.2000",
|
|
74
|
+
"@fluidframework/protocol-definitions": "^1.1.0",
|
|
75
|
+
"@fluidframework/telemetry-utils": ">=2.0.0-dev.2.2.0.111723 <2.0.0-dev.3.0.0",
|
|
74
76
|
"abort-controller": "^3.0.0",
|
|
75
77
|
"double-ended-queue": "^2.1.0-0",
|
|
76
78
|
"lodash": "^4.17.21",
|
|
@@ -78,12 +80,13 @@
|
|
|
78
80
|
"uuid": "^8.3.1"
|
|
79
81
|
},
|
|
80
82
|
"devDependencies": {
|
|
81
|
-
"@
|
|
82
|
-
"@fluidframework/build-
|
|
83
|
-
"@fluidframework/
|
|
84
|
-
"@fluidframework/
|
|
85
|
-
"@fluidframework/
|
|
86
|
-
"@fluidframework/test-
|
|
83
|
+
"@fluid-tools/build-cli": "^0.6.0-109663",
|
|
84
|
+
"@fluidframework/build-common": "^1.1.0",
|
|
85
|
+
"@fluidframework/build-tools": "^0.6.0-109663",
|
|
86
|
+
"@fluidframework/container-loader-previous": "npm:@fluidframework/container-loader@2.0.0-internal.2.1.0",
|
|
87
|
+
"@fluidframework/eslint-config-fluid": "^1.2.0",
|
|
88
|
+
"@fluidframework/mocha-test-setup": ">=2.0.0-dev.2.2.0.111723 <2.0.0-dev.3.0.0",
|
|
89
|
+
"@fluidframework/test-loader-utils": ">=2.0.0-dev.2.2.0.111723 <2.0.0-dev.3.0.0",
|
|
87
90
|
"@microsoft/api-extractor": "^7.22.2",
|
|
88
91
|
"@rushstack/eslint-config": "^2.5.1",
|
|
89
92
|
"@types/double-ended-queue": "^2.1.0",
|
|
@@ -97,20 +100,15 @@
|
|
|
97
100
|
"eslint": "~8.6.0",
|
|
98
101
|
"mocha": "^10.0.0",
|
|
99
102
|
"nyc": "^15.0.0",
|
|
103
|
+
"prettier": "~2.6.2",
|
|
100
104
|
"rimraf": "^2.6.2",
|
|
101
105
|
"sinon": "^7.4.2",
|
|
102
|
-
"typescript": "~4.5.5"
|
|
103
|
-
"typescript-formatter": "7.1.0"
|
|
106
|
+
"typescript": "~4.5.5"
|
|
104
107
|
},
|
|
105
108
|
"typeValidation": {
|
|
106
|
-
"version": "2.0.0",
|
|
107
|
-
"
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
},
|
|
111
|
-
"ClassDeclaration_Container": {
|
|
112
|
-
"forwardCompat": false
|
|
113
|
-
}
|
|
114
|
-
}
|
|
109
|
+
"version": "2.0.0-internal.2.2.0",
|
|
110
|
+
"baselineRange": ">=2.0.0-internal.2.1.0 <2.0.0-internal.2.2.0",
|
|
111
|
+
"baselineVersion": "2.0.0-internal.2.1.0",
|
|
112
|
+
"broken": {}
|
|
115
113
|
}
|
|
116
114
|
}
|
package/src/audience.ts
CHANGED
|
@@ -21,8 +21,12 @@ export class Audience extends EventEmitter implements IAudienceOwner {
|
|
|
21
21
|
* Adds a new client to the audience
|
|
22
22
|
*/
|
|
23
23
|
public addMember(clientId: string, details: IClient) {
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
// Given that signal delivery is unreliable process, we might observe same client being added twice
|
|
25
|
+
// In such case we should see exactly same payload (IClient), and should not raise event twice!
|
|
26
|
+
if (!this.members.has(clientId)) {
|
|
27
|
+
this.members.set(clientId, details);
|
|
28
|
+
this.emit("addMember", clientId, details);
|
|
29
|
+
}
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
/**
|
|
@@ -53,14 +57,4 @@ export class Audience extends EventEmitter implements IAudienceOwner {
|
|
|
53
57
|
public getMember(clientId: string): IClient | undefined {
|
|
54
58
|
return this.members.get(clientId);
|
|
55
59
|
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Clears the audience
|
|
59
|
-
*/
|
|
60
|
-
public clear(): void {
|
|
61
|
-
const clientIds = this.members.keys();
|
|
62
|
-
for (const clientId of clientIds) {
|
|
63
|
-
this.removeMember(clientId);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
60
|
}
|
package/src/connectionManager.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from "@fluidframework/container-definitions";
|
|
19
19
|
import { GenericError, UsageError } from "@fluidframework/container-utils";
|
|
20
20
|
import {
|
|
21
|
+
IAnyDriverError,
|
|
21
22
|
IDocumentService,
|
|
22
23
|
IDocumentDeltaConnection,
|
|
23
24
|
IDocumentDeltaConnectionEvents,
|
|
@@ -27,7 +28,6 @@ import {
|
|
|
27
28
|
createWriteError,
|
|
28
29
|
createGenericNetworkError,
|
|
29
30
|
getRetryDelayFromError,
|
|
30
|
-
IAnyDriverError,
|
|
31
31
|
waitForConnectedState,
|
|
32
32
|
DeltaStreamConnectionForbiddenError,
|
|
33
33
|
logNetworkFailure,
|
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
IConnectionManagerFactoryArgs,
|
|
60
60
|
} from "./contracts";
|
|
61
61
|
import { DeltaQueue } from "./deltaQueue";
|
|
62
|
+
import { SignalType } from "./protocol";
|
|
62
63
|
|
|
63
64
|
const MaxReconnectDelayInMs = 8000;
|
|
64
65
|
const InitialReconnectDelayInMs = 1000;
|
|
@@ -80,10 +81,19 @@ function getNackReconnectInfo(nackContent: INackContent) {
|
|
|
80
81
|
* Implementation of IDocumentDeltaConnection that does not support submitting
|
|
81
82
|
* or receiving ops. Used in storage-only mode.
|
|
82
83
|
*/
|
|
84
|
+
const clientNoDeltaStream: IClient = {
|
|
85
|
+
mode: "read",
|
|
86
|
+
details: { capabilities: { interactive: true } },
|
|
87
|
+
permission: [],
|
|
88
|
+
user: { id: "storage-only client" }, // we need some "fake" ID here.
|
|
89
|
+
scopes: [],
|
|
90
|
+
};
|
|
91
|
+
const clientIdNoDeltaStream: string = "storage-only client";
|
|
92
|
+
|
|
83
93
|
class NoDeltaStream
|
|
84
94
|
extends TypedEventEmitter<IDocumentDeltaConnectionEvents>
|
|
85
95
|
implements IDocumentDeltaConnection, IDisposable {
|
|
86
|
-
clientId
|
|
96
|
+
clientId = clientIdNoDeltaStream;
|
|
87
97
|
claims: ITokenClaims = {
|
|
88
98
|
scopes: [ScopeType.DocRead],
|
|
89
99
|
} as any;
|
|
@@ -93,7 +103,7 @@ class NoDeltaStream
|
|
|
93
103
|
version: string = "";
|
|
94
104
|
initialMessages: ISequencedDocumentMessage[] = [];
|
|
95
105
|
initialSignals: ISignalMessage[] = [];
|
|
96
|
-
initialClients: ISignalClient[] = [];
|
|
106
|
+
initialClients: ISignalClient[] = [{ client: clientNoDeltaStream, clientId: clientIdNoDeltaStream }];
|
|
97
107
|
serviceConfiguration: IClientConfiguration = {
|
|
98
108
|
maxMessageSize: 0,
|
|
99
109
|
blockSize: 0,
|
|
@@ -570,7 +580,13 @@ export class ConnectionManager implements IConnectionManager {
|
|
|
570
580
|
* @param args - The connection arguments
|
|
571
581
|
*/
|
|
572
582
|
private triggerConnect(connectionMode: ConnectionMode) {
|
|
573
|
-
|
|
583
|
+
// reconnect() has async await of waitForConnectedState(), and that causes potential race conditions
|
|
584
|
+
// where we might already have a connection. If it were to happen, it's possible that we will connect
|
|
585
|
+
// with different mode to `connectionMode`. Glancing through the caller chains, it looks like code should be
|
|
586
|
+
// fine (if needed, reconnect flow will get triggered again). Places where new mode matters should encode it
|
|
587
|
+
// directly in connectCore - see this.shouldJoinWrite() test as an example.
|
|
588
|
+
// assert(this.connection === undefined, 0x239 /* "called only in disconnected state" */);
|
|
589
|
+
|
|
574
590
|
if (this.reconnectMode !== ReconnectMode.Enabled) {
|
|
575
591
|
return;
|
|
576
592
|
}
|
|
@@ -713,17 +729,43 @@ export class ConnectionManager implements IConnectionManager {
|
|
|
713
729
|
initialMessages,
|
|
714
730
|
this.connectFirstConnection ? "InitialOps" : "ReconnectOps");
|
|
715
731
|
|
|
716
|
-
if (connection.initialSignals !== undefined) {
|
|
717
|
-
for (const signal of connection.initialSignals) {
|
|
718
|
-
this.props.signalHandler(signal);
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
|
|
722
732
|
const details = ConnectionManager.detailsFromConnection(connection);
|
|
723
733
|
details.checkpointSequenceNumber = checkpointSequenceNumber;
|
|
724
734
|
this.props.connectHandler(details);
|
|
725
735
|
|
|
726
736
|
this.connectFirstConnection = false;
|
|
737
|
+
|
|
738
|
+
// Synthesize clear & join signals out of initialClients state.
|
|
739
|
+
// This allows us to have single way to process signals, and makes it simpler to initialize
|
|
740
|
+
// protocol in Container.
|
|
741
|
+
const clearSignal: ISignalMessage = {
|
|
742
|
+
clientId: null, // system message
|
|
743
|
+
content: JSON.stringify({
|
|
744
|
+
type: SignalType.Clear,
|
|
745
|
+
}),
|
|
746
|
+
};
|
|
747
|
+
this.props.signalHandler(clearSignal);
|
|
748
|
+
|
|
749
|
+
for (const priorClient of connection.initialClients ?? []) {
|
|
750
|
+
const joinSignal: ISignalMessage = {
|
|
751
|
+
clientId: null, // system signal
|
|
752
|
+
content: JSON.stringify({
|
|
753
|
+
type: SignalType.ClientJoin,
|
|
754
|
+
content: priorClient, // ISignalClient
|
|
755
|
+
}),
|
|
756
|
+
};
|
|
757
|
+
this.props.signalHandler(joinSignal);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// Unfortunately, there is no defined order between initialSignals (including join & leave signals)
|
|
761
|
+
// and connection.initialClients. In practice, connection.initialSignals quite often contains join signal
|
|
762
|
+
// for "self" and connection.initialClients does not contain "self", so we have to process them after
|
|
763
|
+
// "clear" signal above.
|
|
764
|
+
if (connection.initialSignals !== undefined) {
|
|
765
|
+
for (const signal of connection.initialSignals) {
|
|
766
|
+
this.props.signalHandler(signal);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
727
769
|
}
|
|
728
770
|
|
|
729
771
|
/**
|
|
@@ -7,14 +7,20 @@ import { ITelemetryLogger, ITelemetryProperties } from "@fluidframework/common-d
|
|
|
7
7
|
import { assert, Timer } from "@fluidframework/common-utils";
|
|
8
8
|
import { IConnectionDetails, IDeltaManager } from "@fluidframework/container-definitions";
|
|
9
9
|
import { ILocalSequencedClient } from "@fluidframework/protocol-base";
|
|
10
|
-
import {
|
|
10
|
+
import { ISequencedClient, IClient } from "@fluidframework/protocol-definitions";
|
|
11
11
|
import { PerformanceEvent, loggerToMonitoringContext } from "@fluidframework/telemetry-utils";
|
|
12
12
|
import { ConnectionState } from "./connectionState";
|
|
13
13
|
import { CatchUpMonitor, ICatchUpMonitor } from "./catchUpMonitor";
|
|
14
14
|
import { IProtocolHandler } from "./protocol";
|
|
15
15
|
|
|
16
|
+
// Based on recent data, it looks like majority of cases where we get stuck are due to really slow or
|
|
17
|
+
// timing out ops fetches. So attempt recovery infrequently. Also fetch uses 30 second timeout, so
|
|
18
|
+
// if retrying fixes the problem, we should not see these events.
|
|
16
19
|
const JoinOpTimeoutMs = 45000;
|
|
17
20
|
|
|
21
|
+
// Timeout waiting for "self" join signal, before giving up
|
|
22
|
+
const JoinSignalTimeoutMs = 5000;
|
|
23
|
+
|
|
18
24
|
/** Constructor parameter type for passing in dependencies needed by the ConnectionStateHandler */
|
|
19
25
|
export interface IConnectionStateHandlerInputs {
|
|
20
26
|
logger: ITelemetryLogger;
|
|
@@ -39,7 +45,7 @@ export interface IConnectionStateHandler {
|
|
|
39
45
|
containerSaved(): void;
|
|
40
46
|
dispose(): void;
|
|
41
47
|
initProtocol(protocol: IProtocolHandler): void;
|
|
42
|
-
receivedConnectEvent(
|
|
48
|
+
receivedConnectEvent(details: IConnectionDetails): void;
|
|
43
49
|
receivedDisconnectEvent(reason: string): void;
|
|
44
50
|
}
|
|
45
51
|
|
|
@@ -50,7 +56,8 @@ export function createConnectionStateHandler(
|
|
|
50
56
|
) {
|
|
51
57
|
const mc = loggerToMonitoringContext(inputs.logger);
|
|
52
58
|
return createConnectionStateHandlerCore(
|
|
53
|
-
mc.config.getBoolean("Fluid.Container.CatchUpBeforeDeclaringConnected") === true,
|
|
59
|
+
mc.config.getBoolean("Fluid.Container.CatchUpBeforeDeclaringConnected") === true, // connectedRaisedWhenCaughtUp
|
|
60
|
+
mc.config.getBoolean("Fluid.Container.DisableJoinSignalWait") !== true, // readClientsWaitForJoinSignal
|
|
54
61
|
inputs,
|
|
55
62
|
deltaManager,
|
|
56
63
|
clientId,
|
|
@@ -58,20 +65,34 @@ export function createConnectionStateHandler(
|
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
export function createConnectionStateHandlerCore(
|
|
61
|
-
|
|
68
|
+
connectedRaisedWhenCaughtUp: boolean,
|
|
69
|
+
readClientsWaitForJoinSignal: boolean,
|
|
62
70
|
inputs: IConnectionStateHandlerInputs,
|
|
63
71
|
deltaManager: IDeltaManager<any, any>,
|
|
64
72
|
clientId?: string,
|
|
65
73
|
) {
|
|
66
|
-
if (!
|
|
67
|
-
return new ConnectionStateHandler(inputs, clientId);
|
|
74
|
+
if (!connectedRaisedWhenCaughtUp) {
|
|
75
|
+
return new ConnectionStateHandler(inputs, readClientsWaitForJoinSignal, clientId);
|
|
68
76
|
}
|
|
69
77
|
return new ConnectionStateCatchup(
|
|
70
78
|
inputs,
|
|
71
|
-
(handler: IConnectionStateHandlerInputs) => new ConnectionStateHandler(
|
|
79
|
+
(handler: IConnectionStateHandlerInputs) => new ConnectionStateHandler(
|
|
80
|
+
handler,
|
|
81
|
+
readClientsWaitForJoinSignal,
|
|
82
|
+
clientId),
|
|
72
83
|
deltaManager);
|
|
73
84
|
}
|
|
74
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Helper internal interface to abstract away Audience & Quorum
|
|
88
|
+
*/
|
|
89
|
+
interface IMembership {
|
|
90
|
+
on(
|
|
91
|
+
eventName: "addMember" | "removeMember",
|
|
92
|
+
listener: (clientId: string, details: IClient | ISequencedClient) => void);
|
|
93
|
+
getMember(clientId: string): undefined | unknown;
|
|
94
|
+
}
|
|
95
|
+
|
|
75
96
|
/**
|
|
76
97
|
* Class that can be used as a base class for building IConnectionStateHandler adapters / pipeline.
|
|
77
98
|
* It implements both ends of communication interfaces and passes data back and forward
|
|
@@ -97,8 +118,8 @@ class ConnectionStateHandlerPassThrough implements IConnectionStateHandler, ICon
|
|
|
97
118
|
public initProtocol(protocol: IProtocolHandler) { return this.pimpl.initProtocol(protocol); }
|
|
98
119
|
public receivedDisconnectEvent(reason: string) { return this.pimpl.receivedDisconnectEvent(reason); }
|
|
99
120
|
|
|
100
|
-
public receivedConnectEvent(
|
|
101
|
-
return this.pimpl.receivedConnectEvent(
|
|
121
|
+
public receivedConnectEvent(details: IConnectionDetails) {
|
|
122
|
+
return this.pimpl.receivedConnectEvent(details);
|
|
102
123
|
}
|
|
103
124
|
|
|
104
125
|
/**
|
|
@@ -201,8 +222,8 @@ class ConnectionStateCatchup extends ConnectionStateHandlerPassThrough {
|
|
|
201
222
|
*
|
|
202
223
|
* For (a) we give up waiting after some time (same timeout as server uses), and go ahead and transition to Connected.
|
|
203
224
|
*
|
|
204
|
-
* For (b) we log telemetry if it takes too long, but still only transition to Connected when the Join op is
|
|
205
|
-
*
|
|
225
|
+
* For (b) we log telemetry if it takes too long, but still only transition to Connected when the Join op/signal is
|
|
226
|
+
* processed.
|
|
206
227
|
*
|
|
207
228
|
* For (c) this is optional behavior, controlled by the parameters of receivedConnectEvent
|
|
208
229
|
*/
|
|
@@ -212,6 +233,8 @@ class ConnectionStateHandler implements IConnectionStateHandler {
|
|
|
212
233
|
private readonly prevClientLeftTimer: Timer;
|
|
213
234
|
private readonly joinOpTimer: Timer;
|
|
214
235
|
private protocol?: IProtocolHandler;
|
|
236
|
+
private connection?: IConnectionDetails;
|
|
237
|
+
private _clientId?: string;
|
|
215
238
|
|
|
216
239
|
private waitEvent: PerformanceEvent | undefined;
|
|
217
240
|
|
|
@@ -229,8 +252,10 @@ class ConnectionStateHandler implements IConnectionStateHandler {
|
|
|
229
252
|
|
|
230
253
|
constructor(
|
|
231
254
|
private readonly handler: IConnectionStateHandlerInputs,
|
|
232
|
-
private
|
|
255
|
+
private readonly readClientsWaitForJoinSignal: boolean,
|
|
256
|
+
clientIdFromPausedSession?: string,
|
|
233
257
|
) {
|
|
258
|
+
this._clientId = clientIdFromPausedSession;
|
|
234
259
|
this.prevClientLeftTimer = new Timer(
|
|
235
260
|
// Default is 5 min for which we are going to wait for its own "leave" message. This is same as
|
|
236
261
|
// the max time on server after which leave op is sent.
|
|
@@ -242,11 +267,8 @@ class ConnectionStateHandler implements IConnectionStateHandler {
|
|
|
242
267
|
},
|
|
243
268
|
);
|
|
244
269
|
|
|
245
|
-
// Based on recent data, it looks like majority of cases where we get stuck are due to really slow or
|
|
246
|
-
// timing out ops fetches. So attempt recovery infrequently. Also fetch uses 30 second timeout, so
|
|
247
|
-
// if retrying fixes the problem, we should not see these events.
|
|
248
270
|
this.joinOpTimer = new Timer(
|
|
249
|
-
|
|
271
|
+
0, // default value is not used - startJoinOpTimer() explicitly provides timeout
|
|
250
272
|
() => {
|
|
251
273
|
// I've observed timer firing within couple ms from disconnect event, looks like
|
|
252
274
|
// queued timer callback is not cancelled if timer is cancelled while callback sits in the queue.
|
|
@@ -266,7 +288,10 @@ class ConnectionStateHandler implements IConnectionStateHandler {
|
|
|
266
288
|
|
|
267
289
|
private startJoinOpTimer() {
|
|
268
290
|
assert(!this.joinOpTimer.hasTimer, 0x234 /* "has joinOpTimer" */);
|
|
269
|
-
this.
|
|
291
|
+
assert(this.connection !== undefined, "have connection");
|
|
292
|
+
this.joinOpTimer.start(
|
|
293
|
+
this.connection.mode === "write" ? JoinOpTimeoutMs : JoinSignalTimeoutMs,
|
|
294
|
+
);
|
|
270
295
|
}
|
|
271
296
|
|
|
272
297
|
private stopJoinOpTimer() {
|
|
@@ -297,8 +322,8 @@ class ConnectionStateHandler implements IConnectionStateHandler {
|
|
|
297
322
|
if (clientId === this.pendingClientId) {
|
|
298
323
|
if (this.joinOpTimer.hasTimer) {
|
|
299
324
|
this.stopJoinOpTimer();
|
|
300
|
-
} else {
|
|
301
|
-
// timer has already fired, meaning it took too long to get join
|
|
325
|
+
} else if (this.shouldWaitForJoinSignal()) {
|
|
326
|
+
// timer has already fired, meaning it took too long to get join op/signal.
|
|
302
327
|
// Record how long it actually took to recover.
|
|
303
328
|
this.handler.logConnectionIssue("ReceivedJoinOp");
|
|
304
329
|
}
|
|
@@ -333,9 +358,11 @@ class ConnectionStateHandler implements IConnectionStateHandler {
|
|
|
333
358
|
this.setConnectionState(ConnectionState.Connected);
|
|
334
359
|
} else {
|
|
335
360
|
// Adding this event temporarily so that we can get help debugging if something goes wrong.
|
|
361
|
+
// We may not see any ops due to being disconnected all that time - that's not an error!
|
|
362
|
+
const error = source === "timeout" && this.connectionState !== ConnectionState.Disconnected;
|
|
336
363
|
this.handler.logger.sendTelemetryEvent({
|
|
337
364
|
eventName: "connectedStateRejected",
|
|
338
|
-
category:
|
|
365
|
+
category: error ? "error" : "generic",
|
|
339
366
|
details: JSON.stringify({
|
|
340
367
|
source,
|
|
341
368
|
pendingClientId: this.pendingClientId,
|
|
@@ -356,29 +383,35 @@ class ConnectionStateHandler implements IConnectionStateHandler {
|
|
|
356
383
|
}
|
|
357
384
|
|
|
358
385
|
public receivedDisconnectEvent(reason: string) {
|
|
386
|
+
this.connection = undefined;
|
|
359
387
|
this.setConnectionState(ConnectionState.Disconnected, reason);
|
|
360
388
|
}
|
|
361
389
|
|
|
390
|
+
private shouldWaitForJoinSignal() {
|
|
391
|
+
assert(this.connection !== undefined, "all callers call here with active connection");
|
|
392
|
+
return this.connection.mode === "write" || this.readClientsWaitForJoinSignal;
|
|
393
|
+
}
|
|
394
|
+
|
|
362
395
|
/**
|
|
363
396
|
* The "connect" event indicates the connection to the Relay Service is live.
|
|
364
397
|
* However, some additional conditions must be met before we can fully transition to
|
|
365
398
|
* "Connected" state. This function handles that interim period, known as "Connecting" state.
|
|
366
|
-
* @param connectionMode - Read or Write connection
|
|
367
399
|
* @param details - Connection details returned from the Relay Service
|
|
368
400
|
* @param deltaManager - DeltaManager to be used for delaying Connected transition until caught up.
|
|
369
401
|
* If it's undefined, then don't delay and transition to Connected as soon as Leave/Join op are accounted for
|
|
370
402
|
*/
|
|
371
403
|
public receivedConnectEvent(
|
|
372
|
-
connectionMode: ConnectionMode,
|
|
373
404
|
details: IConnectionDetails,
|
|
374
405
|
) {
|
|
406
|
+
this.connection = details;
|
|
407
|
+
|
|
375
408
|
const oldState = this._connectionState;
|
|
376
409
|
this._connectionState = ConnectionState.CatchingUp;
|
|
377
410
|
|
|
378
|
-
const writeConnection = connectionMode === "write";
|
|
379
|
-
|
|
380
411
|
// The following checks are wrong. They are only valid if user has write access to a file.
|
|
381
412
|
// If user lost such access mid-session, user will not be able to get "write" connection.
|
|
413
|
+
//
|
|
414
|
+
// const writeConnection = details.mode === "write";
|
|
382
415
|
// assert(!this.handler.shouldClientJoinWrite() || writeConnection,
|
|
383
416
|
// 0x30a /* shouldClientJoinWrite should imply this is a writeConnection */);
|
|
384
417
|
// assert(!this.waitingForLeaveOp || writeConnection,
|
|
@@ -395,16 +428,14 @@ class ConnectionStateHandler implements IConnectionStateHandler {
|
|
|
395
428
|
// IMPORTANT: Report telemetry after we set _pendingClientId, but before transitioning to Connected state
|
|
396
429
|
this.handler.connectionStateChanged(ConnectionState.CatchingUp, oldState);
|
|
397
430
|
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
// Previous client left, and we are waiting for our own join op. When it is processed we'll join the quorum
|
|
407
|
-
// and attempt to transition to Connected state via receivedAddMemberEvent.
|
|
431
|
+
// Check if we need to wait for join op/signal, and if we need to wait for leave op from previous connection.
|
|
432
|
+
// Pending clientId could have joined already (i.e. join op/signal already processed):
|
|
433
|
+
// We are fetching ops from storage in parallel to connecting to Relay Service,
|
|
434
|
+
// and given async processes, it's possible that we have already processed our own join message before
|
|
435
|
+
// connection was fully established.
|
|
436
|
+
if (!this.hasMember(this._pendingClientId) && this.shouldWaitForJoinSignal()) {
|
|
437
|
+
// We are waiting for our own join op / signal. When it is processed
|
|
438
|
+
// we'll attempt to transition to Connected state via receivedAddMemberEvent() flow.
|
|
408
439
|
this.startJoinOpTimer();
|
|
409
440
|
} else if (!this.waitingForLeaveOp) {
|
|
410
441
|
// We're not waiting for Join or Leave op (if read-only connection those don't even apply),
|
|
@@ -426,6 +457,10 @@ class ConnectionStateHandler implements IConnectionStateHandler {
|
|
|
426
457
|
|
|
427
458
|
const oldState = this._connectionState;
|
|
428
459
|
this._connectionState = value;
|
|
460
|
+
|
|
461
|
+
// This is the only place in code that deals with quorum. The rest works with audience
|
|
462
|
+
// The code below ensures that we do not send ops until we know that old "write" client's disconnect
|
|
463
|
+
// produced (and sequenced) leave op
|
|
429
464
|
let client: ILocalSequencedClient | undefined;
|
|
430
465
|
if (this._clientId !== undefined) {
|
|
431
466
|
client = this.protocol?.quorum?.getMember(this._clientId);
|
|
@@ -476,23 +511,36 @@ class ConnectionStateHandler implements IConnectionStateHandler {
|
|
|
476
511
|
// Helper method to switch between quorum and audience.
|
|
477
512
|
// Old design was checking only quorum for "write" clients.
|
|
478
513
|
// Latest change checks audience for all types of connections.
|
|
479
|
-
protected get membership() {
|
|
480
|
-
|
|
514
|
+
protected get membership(): IMembership | undefined {
|
|
515
|
+
// We could always use audience here, and in practice it will probably be correct.
|
|
516
|
+
// (including case when this.readClientsWaitForJoinSignal === false).
|
|
517
|
+
// But only if it's superset of quorum, i.e. when filtered to "write" clients, they are always identical!
|
|
518
|
+
// It's safer to assume that we have bugs and engaging kill-bit switch should bring us back to well-known
|
|
519
|
+
// and tested state!
|
|
520
|
+
return this.readClientsWaitForJoinSignal ? this.protocol?.audience : this.protocol?.quorum;
|
|
481
521
|
}
|
|
482
522
|
|
|
483
523
|
public initProtocol(protocol: IProtocolHandler) {
|
|
484
524
|
this.protocol = protocol;
|
|
485
525
|
|
|
486
|
-
this.membership?.on("addMember", (clientId) => {
|
|
526
|
+
this.membership?.on("addMember", (clientId, details) => {
|
|
527
|
+
assert((details as IClient).mode === "read" || protocol.quorum.getMember(clientId) !== undefined,
|
|
528
|
+
"Audience is subset of quorum");
|
|
487
529
|
this.receivedAddMemberEvent(clientId);
|
|
488
530
|
});
|
|
489
531
|
|
|
490
532
|
this.membership?.on("removeMember", (clientId) => {
|
|
533
|
+
assert(protocol.quorum.getMember(clientId) === undefined, "Audience is subset of quorum");
|
|
491
534
|
this.receivedRemoveMemberEvent(clientId);
|
|
492
535
|
});
|
|
493
536
|
|
|
494
|
-
|
|
495
|
-
|
|
537
|
+
/* There is a tiny tiny race possible, where these events happen in this order:
|
|
538
|
+
1. A connection is established (no "cached" mode is used, so it happens in parallel / faster than other steps)
|
|
539
|
+
2. Some other client produces a summary
|
|
540
|
+
3. We get "lucky" and load from that summary as our initial snapshot
|
|
541
|
+
4. ConnectionStateHandler.initProtocol is called, "self" is already in the quorum.
|
|
542
|
+
We could avoid this sequence (and delete test case for it) if we move connection lower in Container.load()
|
|
543
|
+
*/
|
|
496
544
|
if (this.hasMember(this.pendingClientId)) {
|
|
497
545
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
498
546
|
this.receivedAddMemberEvent(this.pendingClientId!);
|