@fluidframework/container-loader 1.2.6 → 2.0.0-dev.1.3.0.96595
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/.mocharc.js +12 -0
- package/dist/audience.d.ts +2 -6
- package/dist/audience.d.ts.map +1 -1
- package/dist/audience.js +6 -11
- package/dist/audience.js.map +1 -1
- package/dist/catchUpMonitor.d.ts +29 -0
- package/dist/catchUpMonitor.d.ts.map +1 -0
- package/dist/catchUpMonitor.js +43 -0
- package/dist/catchUpMonitor.js.map +1 -0
- package/dist/collabWindowTracker.d.ts +1 -1
- package/dist/collabWindowTracker.d.ts.map +1 -1
- package/dist/collabWindowTracker.js +12 -4
- package/dist/collabWindowTracker.js.map +1 -1
- package/dist/connectionManager.d.ts +5 -5
- package/dist/connectionManager.d.ts.map +1 -1
- package/dist/connectionManager.js +43 -22
- package/dist/connectionManager.js.map +1 -1
- package/dist/connectionState.d.ts +0 -5
- package/dist/connectionState.d.ts.map +1 -1
- package/dist/connectionState.js +0 -5
- package/dist/connectionState.js.map +1 -1
- package/dist/connectionStateHandler.d.ts +84 -22
- package/dist/connectionStateHandler.d.ts.map +1 -1
- package/dist/connectionStateHandler.js +172 -59
- package/dist/connectionStateHandler.js.map +1 -1
- package/dist/container.d.ts +29 -17
- package/dist/container.d.ts.map +1 -1
- package/dist/container.js +181 -171
- package/dist/container.js.map +1 -1
- package/dist/containerContext.d.ts +18 -7
- package/dist/containerContext.d.ts.map +1 -1
- package/dist/containerContext.js +18 -8
- package/dist/containerContext.js.map +1 -1
- package/dist/containerStorageAdapter.d.ts +11 -25
- package/dist/containerStorageAdapter.d.ts.map +1 -1
- package/dist/containerStorageAdapter.js +51 -17
- package/dist/containerStorageAdapter.js.map +1 -1
- package/dist/contracts.d.ts +5 -5
- package/dist/contracts.js.map +1 -1
- package/dist/deltaManager.d.ts +4 -1
- package/dist/deltaManager.d.ts.map +1 -1
- package/dist/deltaManager.js +39 -12
- package/dist/deltaManager.js.map +1 -1
- package/dist/deltaManagerProxy.d.ts +4 -1
- package/dist/deltaManagerProxy.d.ts.map +1 -1
- package/dist/deltaQueue.d.ts +9 -2
- package/dist/deltaQueue.d.ts.map +1 -1
- package/dist/deltaQueue.js +31 -26
- package/dist/deltaQueue.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts +8 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +4 -3
- package/dist/loader.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/protocol.d.ts +27 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +79 -0
- package/dist/protocol.js.map +1 -0
- package/dist/protocolTreeDocumentStorageService.d.ts +1 -1
- package/dist/protocolTreeDocumentStorageService.d.ts.map +1 -1
- package/dist/retriableDocumentStorageService.d.ts +2 -2
- package/dist/retriableDocumentStorageService.d.ts.map +1 -1
- package/dist/retriableDocumentStorageService.js +2 -2
- package/dist/retriableDocumentStorageService.js.map +1 -1
- package/lib/audience.d.ts +2 -6
- package/lib/audience.d.ts.map +1 -1
- package/lib/audience.js +6 -11
- package/lib/audience.js.map +1 -1
- package/lib/catchUpMonitor.d.ts +29 -0
- package/lib/catchUpMonitor.d.ts.map +1 -0
- package/lib/catchUpMonitor.js +39 -0
- package/lib/catchUpMonitor.js.map +1 -0
- package/lib/collabWindowTracker.d.ts +1 -1
- package/lib/collabWindowTracker.d.ts.map +1 -1
- package/lib/collabWindowTracker.js +13 -5
- package/lib/collabWindowTracker.js.map +1 -1
- package/lib/connectionManager.d.ts +5 -5
- package/lib/connectionManager.d.ts.map +1 -1
- package/lib/connectionManager.js +44 -25
- package/lib/connectionManager.js.map +1 -1
- package/lib/connectionState.d.ts +0 -5
- package/lib/connectionState.d.ts.map +1 -1
- package/lib/connectionState.js +0 -5
- package/lib/connectionState.js.map +1 -1
- package/lib/connectionStateHandler.d.ts +84 -22
- package/lib/connectionStateHandler.d.ts.map +1 -1
- package/lib/connectionStateHandler.js +171 -59
- package/lib/connectionStateHandler.js.map +1 -1
- package/lib/container.d.ts +29 -17
- package/lib/container.d.ts.map +1 -1
- package/lib/container.js +184 -174
- package/lib/container.js.map +1 -1
- package/lib/containerContext.d.ts +18 -7
- package/lib/containerContext.d.ts.map +1 -1
- package/lib/containerContext.js +19 -9
- package/lib/containerContext.js.map +1 -1
- package/lib/containerStorageAdapter.d.ts +11 -25
- package/lib/containerStorageAdapter.d.ts.map +1 -1
- package/lib/containerStorageAdapter.js +51 -16
- package/lib/containerStorageAdapter.js.map +1 -1
- package/lib/contracts.d.ts +5 -5
- package/lib/contracts.js.map +1 -1
- package/lib/deltaManager.d.ts +4 -1
- package/lib/deltaManager.d.ts.map +1 -1
- package/lib/deltaManager.js +41 -14
- package/lib/deltaManager.js.map +1 -1
- package/lib/deltaManagerProxy.d.ts +4 -1
- package/lib/deltaManagerProxy.d.ts.map +1 -1
- package/lib/deltaQueue.d.ts +9 -2
- package/lib/deltaQueue.d.ts.map +1 -1
- package/lib/deltaQueue.js +32 -27
- package/lib/deltaQueue.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js.map +1 -1
- package/lib/loader.d.ts +8 -1
- package/lib/loader.d.ts.map +1 -1
- package/lib/loader.js +4 -3
- package/lib/loader.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/protocol.d.ts +27 -0
- package/lib/protocol.d.ts.map +1 -0
- package/lib/protocol.js +75 -0
- package/lib/protocol.js.map +1 -0
- package/lib/protocolTreeDocumentStorageService.d.ts +1 -1
- package/lib/protocolTreeDocumentStorageService.d.ts.map +1 -1
- package/lib/retriableDocumentStorageService.d.ts +2 -2
- package/lib/retriableDocumentStorageService.d.ts.map +1 -1
- package/lib/retriableDocumentStorageService.js +2 -2
- package/lib/retriableDocumentStorageService.js.map +1 -1
- package/package.json +27 -19
- package/src/audience.ts +8 -14
- package/src/catchUpMonitor.ts +59 -0
- package/src/collabWindowTracker.ts +15 -6
- package/src/connectionManager.ts +56 -33
- package/src/connectionState.ts +0 -6
- package/src/connectionStateHandler.ts +235 -70
- package/src/container.ts +241 -218
- package/src/containerContext.ts +22 -8
- package/src/containerStorageAdapter.ts +71 -16
- package/src/contracts.ts +7 -7
- package/src/deltaManager.ts +48 -15
- package/src/deltaQueue.ts +34 -28
- package/src/index.ts +4 -0
- package/src/loader.ts +14 -3
- package/src/packageVersion.ts +1 -1
- package/src/protocol.ts +120 -0
- package/src/retriableDocumentStorageService.ts +8 -2
|
@@ -4,62 +4,161 @@
|
|
|
4
4
|
* Licensed under the MIT License.
|
|
5
5
|
*/
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.
|
|
7
|
+
exports.createConnectionStateHandlerCore = exports.createConnectionStateHandler = void 0;
|
|
8
8
|
const common_utils_1 = require("@fluidframework/common-utils");
|
|
9
9
|
const telemetry_utils_1 = require("@fluidframework/telemetry-utils");
|
|
10
10
|
const connectionState_1 = require("./connectionState");
|
|
11
|
+
const catchUpMonitor_1 = require("./catchUpMonitor");
|
|
11
12
|
const JoinOpTimeoutMs = 45000;
|
|
13
|
+
function createConnectionStateHandler(inputs, deltaManager, clientId) {
|
|
14
|
+
const mc = (0, telemetry_utils_1.loggerToMonitoringContext)(inputs.logger);
|
|
15
|
+
return createConnectionStateHandlerCore(mc.config.getBoolean("Fluid.Container.CatchUpBeforeDeclaringConnected") === true, inputs, deltaManager, clientId);
|
|
16
|
+
}
|
|
17
|
+
exports.createConnectionStateHandler = createConnectionStateHandler;
|
|
18
|
+
function createConnectionStateHandlerCore(wait, inputs, deltaManager, clientId) {
|
|
19
|
+
if (!wait) {
|
|
20
|
+
return new ConnectionStateHandler(inputs, clientId);
|
|
21
|
+
}
|
|
22
|
+
return new ConnectionStateCatchup(inputs, (handler) => new ConnectionStateHandler(handler, clientId), deltaManager);
|
|
23
|
+
}
|
|
24
|
+
exports.createConnectionStateHandlerCore = createConnectionStateHandlerCore;
|
|
25
|
+
/**
|
|
26
|
+
* Class that can be used as a base class for building IConnectionStateHandler adapters / pipeline.
|
|
27
|
+
* It implements both ends of communication interfaces and passes data back and forward
|
|
28
|
+
*/
|
|
29
|
+
class ConnectionStateHandlerPassThrough {
|
|
30
|
+
constructor(inputs, pimplFactory) {
|
|
31
|
+
this.inputs = inputs;
|
|
32
|
+
this.pimpl = pimplFactory(this);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* IConnectionStateHandler
|
|
36
|
+
*/
|
|
37
|
+
get connectionState() { return this.pimpl.connectionState; }
|
|
38
|
+
get pendingClientId() { return this.pimpl.pendingClientId; }
|
|
39
|
+
containerSaved() { return this.pimpl.containerSaved(); }
|
|
40
|
+
dispose() { return this.pimpl.dispose(); }
|
|
41
|
+
initProtocol(protocol) { return this.pimpl.initProtocol(protocol); }
|
|
42
|
+
receivedDisconnectEvent(reason) { return this.pimpl.receivedDisconnectEvent(reason); }
|
|
43
|
+
receivedConnectEvent(connectionMode, details) {
|
|
44
|
+
return this.pimpl.receivedConnectEvent(connectionMode, details);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* IConnectionStateHandlerInputs
|
|
48
|
+
*/
|
|
49
|
+
get logger() { return this.inputs.logger; }
|
|
50
|
+
connectionStateChanged(value, oldState, reason) {
|
|
51
|
+
return this.inputs.connectionStateChanged(value, oldState, reason);
|
|
52
|
+
}
|
|
53
|
+
shouldClientJoinWrite() { return this.inputs.shouldClientJoinWrite(); }
|
|
54
|
+
get maxClientLeaveWaitTime() { return this.inputs.maxClientLeaveWaitTime; }
|
|
55
|
+
logConnectionIssue(eventName, details) {
|
|
56
|
+
return this.inputs.logConnectionIssue(eventName, details);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Implementation of IConnectionStateHandler pass-through adapter that waits for specific sequence number
|
|
61
|
+
* before raising connected event
|
|
62
|
+
*/
|
|
63
|
+
class ConnectionStateCatchup extends ConnectionStateHandlerPassThrough {
|
|
64
|
+
constructor(inputs, pimplFactory, deltaManager) {
|
|
65
|
+
super(inputs, pimplFactory);
|
|
66
|
+
this.deltaManager = deltaManager;
|
|
67
|
+
this.transitionToConnectedState = () => {
|
|
68
|
+
// Defensive measure, we should always be in Connecting state when this is called.
|
|
69
|
+
const state = this.pimpl.connectionState;
|
|
70
|
+
(0, common_utils_1.assert)(state === connectionState_1.ConnectionState.Connected, 0x3e5 /* invariant broken */);
|
|
71
|
+
(0, common_utils_1.assert)(this._connectionState === connectionState_1.ConnectionState.CatchingUp, 0x3e6 /* invariant broken */);
|
|
72
|
+
this._connectionState = connectionState_1.ConnectionState.Connected;
|
|
73
|
+
this.inputs.connectionStateChanged(connectionState_1.ConnectionState.Connected, connectionState_1.ConnectionState.CatchingUp, "caught up");
|
|
74
|
+
};
|
|
75
|
+
this._connectionState = this.pimpl.connectionState;
|
|
76
|
+
}
|
|
77
|
+
get connectionState() {
|
|
78
|
+
return this._connectionState;
|
|
79
|
+
}
|
|
80
|
+
connectionStateChanged(value, oldState, reason) {
|
|
81
|
+
var _a;
|
|
82
|
+
switch (value) {
|
|
83
|
+
case connectionState_1.ConnectionState.Connected:
|
|
84
|
+
(0, common_utils_1.assert)(this._connectionState === connectionState_1.ConnectionState.CatchingUp, 0x3e1 /* connectivity transitions */);
|
|
85
|
+
// Create catch-up monitor here (not earlier), as we might get more exact info by now about how far
|
|
86
|
+
// client is behind through join signal. This is only true if base layer uses signals (i.e. audience,
|
|
87
|
+
// not quorum, including for "rea" connections) to make decisions about moving to "connected" state.
|
|
88
|
+
// In addition to that, in its current form, doing this in ConnectionState.CatchingUp is dangerous as
|
|
89
|
+
// we might get callback right away, and it will screw up state transition (as code outside of switch
|
|
90
|
+
// statement will overwrite current state).
|
|
91
|
+
(0, common_utils_1.assert)(this.catchUpMonitor === undefined, 0x3eb /* catchUpMonitor should be gone */);
|
|
92
|
+
this.catchUpMonitor = new catchUpMonitor_1.CatchUpMonitor(this.deltaManager, this.transitionToConnectedState);
|
|
93
|
+
return;
|
|
94
|
+
case connectionState_1.ConnectionState.Disconnected:
|
|
95
|
+
(_a = this.catchUpMonitor) === null || _a === void 0 ? void 0 : _a.dispose();
|
|
96
|
+
this.catchUpMonitor = undefined;
|
|
97
|
+
break;
|
|
98
|
+
case connectionState_1.ConnectionState.CatchingUp:
|
|
99
|
+
(0, common_utils_1.assert)(this._connectionState === connectionState_1.ConnectionState.Disconnected, 0x3e3 /* connectivity transitions */);
|
|
100
|
+
break;
|
|
101
|
+
default:
|
|
102
|
+
}
|
|
103
|
+
this._connectionState = value;
|
|
104
|
+
this.inputs.connectionStateChanged(value, oldState, reason);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
12
107
|
/**
|
|
13
108
|
* In the lifetime of a container, the connection will likely disconnect and reconnect periodically.
|
|
14
109
|
* This class ensures that any ops sent by this container instance on previous connection are either
|
|
15
110
|
* sequenced or blocked by the server before emitting the new "connected" event and allowing runtime to resubmit ops.
|
|
16
111
|
*
|
|
17
112
|
* Each connection is assigned a clientId by the service, and the connection is book-ended by a Join and a Leave op
|
|
18
|
-
* generated by the service. Due to the distributed nature of the
|
|
113
|
+
* generated by the service. Due to the distributed nature of the Relay Service, in the case of reconnect we cannot
|
|
19
114
|
* make any assumptions about ordering of operations between the old and new connections - i.e. new Join op could
|
|
20
115
|
* be sequenced before old Leave op (and some acks from pending ops that were in flight when we disconnected).
|
|
21
116
|
*
|
|
22
117
|
* The job of this class is to encapsulate the transition period during reconnect, which is identified by
|
|
23
118
|
* ConnectionState.CatchingUp. Specifically, before moving to Connected state with the new clientId, it ensures that:
|
|
24
|
-
* (A) We process the Leave op for the previous clientId. This allows us to properly handle any acks from in-flight ops
|
|
25
|
-
* that got sequenced with the old clientId (we'll recognize them as local ops). After the Leave op, any other
|
|
26
|
-
* pending ops can safely be submitted with the new clientId without fear of duplication in the sequenced op stream.
|
|
27
|
-
* (B) We process the Join op for the new clientId (identified when the underlying connection was first established),
|
|
28
|
-
* indicating the service is ready to sequence ops sent with the new clientId.
|
|
29
119
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
120
|
+
* a. We process the Leave op for the previous clientId. This allows us to properly handle any acks from in-flight ops
|
|
121
|
+
* that got sequenced with the old clientId (we'll recognize them as local ops). After the Leave op, any other
|
|
122
|
+
* pending ops can safely be submitted with the new clientId without fear of duplication in the sequenced op stream.
|
|
123
|
+
*
|
|
124
|
+
* b. We process the Join op for the new clientId (identified when the underlying connection was first established),
|
|
125
|
+
* indicating the service is ready to sequence ops sent with the new clientId.
|
|
126
|
+
*
|
|
127
|
+
* c. We process all ops known at the time the underlying connection was established (so we are "caught up")
|
|
128
|
+
*
|
|
129
|
+
* For (a) we give up waiting after some time (same timeout as server uses), and go ahead and transition to Connected.
|
|
130
|
+
*
|
|
131
|
+
* For (b) we log telemetry if it takes too long, but still only transition to Connected when the Join op is processed
|
|
32
132
|
* and we are added to the Quorum.
|
|
133
|
+
*
|
|
134
|
+
* For (c) this is optional behavior, controlled by the parameters of receivedConnectEvent
|
|
33
135
|
*/
|
|
34
136
|
class ConnectionStateHandler {
|
|
35
|
-
constructor(handler,
|
|
137
|
+
constructor(handler, _clientId) {
|
|
36
138
|
var _a;
|
|
37
139
|
this.handler = handler;
|
|
38
|
-
this.logger = logger;
|
|
39
140
|
this._clientId = _clientId;
|
|
40
141
|
this._connectionState = connectionState_1.ConnectionState.Disconnected;
|
|
41
142
|
this.prevClientLeftTimer = new common_utils_1.Timer(
|
|
42
143
|
// Default is 5 min for which we are going to wait for its own "leave" message. This is same as
|
|
43
144
|
// the max time on server after which leave op is sent.
|
|
44
145
|
(_a = this.handler.maxClientLeaveWaitTime) !== null && _a !== void 0 ? _a : 300000, () => {
|
|
45
|
-
(0, common_utils_1.assert)(
|
|
146
|
+
(0, common_utils_1.assert)(this.connectionState !== connectionState_1.ConnectionState.Connected, 0x2ac /* "Connected when timeout waiting for leave from previous session fired!" */);
|
|
46
147
|
this.applyForConnectedState("timeout");
|
|
47
148
|
});
|
|
48
149
|
// Based on recent data, it looks like majority of cases where we get stuck are due to really slow or
|
|
49
150
|
// timing out ops fetches. So attempt recovery infrequently. Also fetch uses 30 second timeout, so
|
|
50
151
|
// if retrying fixes the problem, we should not see these events.
|
|
51
152
|
this.joinOpTimer = new common_utils_1.Timer(JoinOpTimeoutMs, () => {
|
|
52
|
-
var _a;
|
|
53
153
|
// I've observed timer firing within couple ms from disconnect event, looks like
|
|
54
154
|
// queued timer callback is not cancelled if timer is cancelled while callback sits in the queue.
|
|
55
155
|
if (this.connectionState !== connectionState_1.ConnectionState.CatchingUp) {
|
|
56
156
|
return;
|
|
57
157
|
}
|
|
58
|
-
const quorumClients = this.handler.quorumClients();
|
|
59
158
|
const details = {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
159
|
+
protocolInitialized: this.protocol !== undefined,
|
|
160
|
+
pendingClientId: this.pendingClientId,
|
|
161
|
+
clientJoined: this.hasMember(this.pendingClientId),
|
|
63
162
|
waitingForLeaveOp: this.waitingForLeaveOp,
|
|
64
163
|
};
|
|
65
164
|
this.handler.logConnectionIssue("NoJoinOp", details);
|
|
@@ -68,9 +167,6 @@ class ConnectionStateHandler {
|
|
|
68
167
|
get connectionState() {
|
|
69
168
|
return this._connectionState;
|
|
70
169
|
}
|
|
71
|
-
get connected() {
|
|
72
|
-
return this.connectionState === connectionState_1.ConnectionState.Connected;
|
|
73
|
-
}
|
|
74
170
|
get clientId() {
|
|
75
171
|
return this._clientId;
|
|
76
172
|
}
|
|
@@ -113,7 +209,7 @@ class ConnectionStateHandler {
|
|
|
113
209
|
}
|
|
114
210
|
// Start the event in case we are waiting for leave or timeout.
|
|
115
211
|
if (this.waitingForLeaveOp) {
|
|
116
|
-
this.waitEvent = telemetry_utils_1.PerformanceEvent.start(this.logger, {
|
|
212
|
+
this.waitEvent = telemetry_utils_1.PerformanceEvent.start(this.handler.logger, {
|
|
117
213
|
eventName: "WaitBeforeClientLeave",
|
|
118
214
|
details: JSON.stringify({
|
|
119
215
|
waitOnClientId: this._clientId,
|
|
@@ -125,24 +221,21 @@ class ConnectionStateHandler {
|
|
|
125
221
|
}
|
|
126
222
|
}
|
|
127
223
|
applyForConnectedState(source) {
|
|
128
|
-
var _a
|
|
129
|
-
|
|
130
|
-
(0, common_utils_1.assert)(
|
|
131
|
-
(0, common_utils_1.assert)(this.waitingForLeaveOp === false ||
|
|
132
|
-
(this.clientId !== undefined && quorumClients.getMember(this.clientId) !== undefined), 0x2e2 /* "Must only wait for leave message when clientId in quorum" */);
|
|
224
|
+
var _a;
|
|
225
|
+
(0, common_utils_1.assert)(this.protocol !== undefined, 0x236 /* "In all cases it should be already installed" */);
|
|
226
|
+
(0, common_utils_1.assert)(!this.waitingForLeaveOp || this.hasMember(this.clientId), 0x2e2 /* "Must only wait for leave message when clientId in quorum" */);
|
|
133
227
|
// Move to connected state only if we are in Connecting state, we have seen our join op
|
|
134
228
|
// and there is no timer running which means we are not waiting for previous client to leave
|
|
135
229
|
// or timeout has occurred while doing so.
|
|
136
230
|
if (this.pendingClientId !== this.clientId
|
|
137
|
-
&& this.pendingClientId
|
|
138
|
-
&& quorumClients.getMember(this.pendingClientId) !== undefined
|
|
231
|
+
&& this.hasMember(this.pendingClientId)
|
|
139
232
|
&& !this.waitingForLeaveOp) {
|
|
140
233
|
(_a = this.waitEvent) === null || _a === void 0 ? void 0 : _a.end({ source });
|
|
141
234
|
this.setConnectionState(connectionState_1.ConnectionState.Connected);
|
|
142
235
|
}
|
|
143
236
|
else {
|
|
144
237
|
// Adding this event temporarily so that we can get help debugging if something goes wrong.
|
|
145
|
-
this.logger.sendTelemetryEvent({
|
|
238
|
+
this.handler.logger.sendTelemetryEvent({
|
|
146
239
|
eventName: "connectedStateRejected",
|
|
147
240
|
category: source === "timeout" ? "error" : "generic",
|
|
148
241
|
details: JSON.stringify({
|
|
@@ -150,7 +243,7 @@ class ConnectionStateHandler {
|
|
|
150
243
|
pendingClientId: this.pendingClientId,
|
|
151
244
|
clientId: this.clientId,
|
|
152
245
|
waitingForLeaveOp: this.waitingForLeaveOp,
|
|
153
|
-
|
|
246
|
+
clientJoined: this.hasMember(this.pendingClientId),
|
|
154
247
|
}),
|
|
155
248
|
});
|
|
156
249
|
}
|
|
@@ -163,9 +256,6 @@ class ConnectionStateHandler {
|
|
|
163
256
|
}
|
|
164
257
|
}
|
|
165
258
|
receivedDisconnectEvent(reason) {
|
|
166
|
-
if (this.joinOpTimer.hasTimer) {
|
|
167
|
-
this.stopJoinOpTimer();
|
|
168
|
-
}
|
|
169
259
|
this.setConnectionState(connectionState_1.ConnectionState.Disconnected, reason);
|
|
170
260
|
}
|
|
171
261
|
/**
|
|
@@ -173,17 +263,20 @@ class ConnectionStateHandler {
|
|
|
173
263
|
* However, some additional conditions must be met before we can fully transition to
|
|
174
264
|
* "Connected" state. This function handles that interim period, known as "Connecting" state.
|
|
175
265
|
* @param connectionMode - Read or Write connection
|
|
176
|
-
* @param details - Connection details returned from the
|
|
266
|
+
* @param details - Connection details returned from the Relay Service
|
|
267
|
+
* @param deltaManager - DeltaManager to be used for delaying Connected transition until caught up.
|
|
268
|
+
* If it's undefined, then don't delay and transition to Connected as soon as Leave/Join op are accounted for
|
|
177
269
|
*/
|
|
178
270
|
receivedConnectEvent(connectionMode, details) {
|
|
179
271
|
const oldState = this._connectionState;
|
|
180
272
|
this._connectionState = connectionState_1.ConnectionState.CatchingUp;
|
|
181
273
|
const writeConnection = connectionMode === "write";
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
|
|
274
|
+
// The following checks are wrong. They are only valid if user has write access to a file.
|
|
275
|
+
// If user lost such access mid-session, user will not be able to get "write" connection.
|
|
276
|
+
// assert(!this.handler.shouldClientJoinWrite() || writeConnection,
|
|
277
|
+
// 0x30a /* shouldClientJoinWrite should imply this is a writeConnection */);
|
|
278
|
+
// assert(!this.waitingForLeaveOp || writeConnection,
|
|
279
|
+
// 0x2a6 /* "waitingForLeaveOp should imply writeConnection (we need to be ready to flush pending ops)" */);
|
|
187
280
|
// Stash the clientID to detect when transitioning from connecting (socket.io channel open) to connected
|
|
188
281
|
// (have received the join message for the client ID)
|
|
189
282
|
// This is especially important in the reconnect case. It's possible there could be outstanding
|
|
@@ -192,13 +285,13 @@ class ConnectionStateHandler {
|
|
|
192
285
|
// we know there can no longer be outstanding ops that we sent with the previous client id.
|
|
193
286
|
this._pendingClientId = details.clientId;
|
|
194
287
|
// IMPORTANT: Report telemetry after we set _pendingClientId, but before transitioning to Connected state
|
|
195
|
-
this.handler.
|
|
288
|
+
this.handler.connectionStateChanged(connectionState_1.ConnectionState.CatchingUp, oldState);
|
|
196
289
|
// For write connections, this pending clientId could be in the quorum already (i.e. join op already processed).
|
|
197
290
|
// We are fetching ops from storage in parallel to connecting to Relay Service,
|
|
198
291
|
// and given async processes, it's possible that we have already processed our own join message before
|
|
199
292
|
// connection was fully established.
|
|
200
|
-
// If
|
|
201
|
-
const waitingForJoinOp = writeConnection &&
|
|
293
|
+
// If protocol is not initialized yet, we expect it will process the join op after it's initialized.
|
|
294
|
+
const waitingForJoinOp = writeConnection && !this.hasMember(this._pendingClientId);
|
|
202
295
|
if (waitingForJoinOp) {
|
|
203
296
|
// Previous client left, and we are waiting for our own join op. When it is processed we'll join the quorum
|
|
204
297
|
// and attempt to transition to Connected state via receivedAddMemberEvent.
|
|
@@ -210,19 +303,20 @@ class ConnectionStateHandler {
|
|
|
210
303
|
// If we are waiting for Leave op still, do nothing for now, we will transition to Connected later.
|
|
211
304
|
this.setConnectionState(connectionState_1.ConnectionState.Connected);
|
|
212
305
|
}
|
|
306
|
+
// else - We are waiting for Leave op still, do nothing for now, we will transition to Connected later
|
|
213
307
|
}
|
|
214
308
|
setConnectionState(value, reason) {
|
|
309
|
+
var _a, _b;
|
|
215
310
|
if (this.connectionState === value) {
|
|
216
311
|
// Already in the desired state - exit early
|
|
217
|
-
this.logger.sendErrorEvent({ eventName: "setConnectionStateSame", value });
|
|
312
|
+
this.handler.logger.sendErrorEvent({ eventName: "setConnectionStateSame", value });
|
|
218
313
|
return;
|
|
219
314
|
}
|
|
220
315
|
const oldState = this._connectionState;
|
|
221
316
|
this._connectionState = value;
|
|
222
|
-
const quorumClients = this.handler.quorumClients();
|
|
223
317
|
let client;
|
|
224
318
|
if (this._clientId !== undefined) {
|
|
225
|
-
client =
|
|
319
|
+
client = (_b = (_a = this.protocol) === null || _a === void 0 ? void 0 : _a.quorum) === null || _b === void 0 ? void 0 : _b.getMember(this._clientId);
|
|
226
320
|
}
|
|
227
321
|
if (value === connectionState_1.ConnectionState.Connected) {
|
|
228
322
|
(0, common_utils_1.assert)(oldState === connectionState_1.ConnectionState.CatchingUp, 0x1d8 /* "Should only transition from Connecting state" */);
|
|
@@ -233,24 +327,27 @@ class ConnectionStateHandler {
|
|
|
233
327
|
this._clientId = this.pendingClientId;
|
|
234
328
|
}
|
|
235
329
|
else if (value === connectionState_1.ConnectionState.Disconnected) {
|
|
236
|
-
//
|
|
330
|
+
// Clear pending state immediately to prepare for reconnect
|
|
237
331
|
this._pendingClientId = undefined;
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
//
|
|
332
|
+
if (this.joinOpTimer.hasTimer) {
|
|
333
|
+
this.stopJoinOpTimer();
|
|
334
|
+
}
|
|
335
|
+
// Only wait for "leave" message if the connected client exists in the quorum and had some non-acked ops
|
|
336
|
+
// Also check if the timer is not already running as
|
|
242
337
|
// we could receive "Disconnected" event multiple times without getting connected and in that case we
|
|
243
338
|
// don't want to reset the timer as we still want to wait on original client which started this timer.
|
|
244
339
|
if (client !== undefined
|
|
245
340
|
&& this.handler.shouldClientJoinWrite()
|
|
246
|
-
&& this.prevClientLeftTimer.hasTimer
|
|
341
|
+
&& !this.waitingForLeaveOp // same as !this.prevClientLeftTimer.hasTimer
|
|
342
|
+
) {
|
|
247
343
|
this.prevClientLeftTimer.restart();
|
|
248
344
|
}
|
|
249
345
|
else {
|
|
250
346
|
// Adding this event temporarily so that we can get help debugging if something goes wrong.
|
|
251
|
-
this.logger.sendTelemetryEvent({
|
|
347
|
+
this.handler.logger.sendTelemetryEvent({
|
|
252
348
|
eventName: "noWaitOnDisconnected",
|
|
253
349
|
details: JSON.stringify({
|
|
350
|
+
clientId: this._clientId,
|
|
254
351
|
inQuorum: client !== undefined,
|
|
255
352
|
waitingForLeaveOp: this.waitingForLeaveOp,
|
|
256
353
|
hadOutstandingOps: this.handler.shouldClientJoinWrite(),
|
|
@@ -259,22 +356,38 @@ class ConnectionStateHandler {
|
|
|
259
356
|
}
|
|
260
357
|
}
|
|
261
358
|
// Report transition before we propagate event across layers
|
|
262
|
-
this.handler.
|
|
263
|
-
|
|
264
|
-
|
|
359
|
+
this.handler.connectionStateChanged(this._connectionState, oldState, reason);
|
|
360
|
+
}
|
|
361
|
+
// Helper method to switch between quorum and audience.
|
|
362
|
+
// Old design was checking only quorum for "write" clients.
|
|
363
|
+
// Latest change checks audience for all types of connections.
|
|
364
|
+
get membership() {
|
|
365
|
+
var _a;
|
|
366
|
+
return (_a = this.protocol) === null || _a === void 0 ? void 0 : _a.quorum;
|
|
265
367
|
}
|
|
266
368
|
initProtocol(protocol) {
|
|
267
|
-
|
|
369
|
+
var _a, _b;
|
|
370
|
+
this.protocol = protocol;
|
|
371
|
+
(_a = this.membership) === null || _a === void 0 ? void 0 : _a.on("addMember", (clientId) => {
|
|
268
372
|
this.receivedAddMemberEvent(clientId);
|
|
269
373
|
});
|
|
270
|
-
|
|
374
|
+
(_b = this.membership) === null || _b === void 0 ? void 0 : _b.on("removeMember", (clientId) => {
|
|
271
375
|
this.receivedRemoveMemberEvent(clientId);
|
|
272
376
|
});
|
|
377
|
+
// Very unlikely race condition, but theoretically can happen - our new connection is already
|
|
378
|
+
// summarized and we are loading from such summary.
|
|
379
|
+
if (this.hasMember(this.pendingClientId)) {
|
|
380
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
381
|
+
this.receivedAddMemberEvent(this.pendingClientId);
|
|
382
|
+
}
|
|
273
383
|
// if we have a clientId from a previous container we need to wait for its leave message
|
|
274
|
-
if (this.clientId !== undefined &&
|
|
384
|
+
if (this.clientId !== undefined && this.hasMember(this.clientId)) {
|
|
275
385
|
this.prevClientLeftTimer.restart();
|
|
276
386
|
}
|
|
277
387
|
}
|
|
388
|
+
hasMember(clientId) {
|
|
389
|
+
var _a;
|
|
390
|
+
return ((_a = this.membership) === null || _a === void 0 ? void 0 : _a.getMember(clientId !== null && clientId !== void 0 ? clientId : "")) !== undefined;
|
|
391
|
+
}
|
|
278
392
|
}
|
|
279
|
-
exports.ConnectionStateHandler = ConnectionStateHandler;
|
|
280
393
|
//# sourceMappingURL=connectionStateHandler.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connectionStateHandler.js","sourceRoot":"","sources":["../src/connectionStateHandler.ts"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport { ITelemetryLogger, ITelemetryProperties } from \"@fluidframework/common-definitions\";\nimport { assert, Timer } from \"@fluidframework/common-utils\";\nimport { IConnectionDetails } from \"@fluidframework/container-definitions\";\nimport { ILocalSequencedClient, IProtocolHandler } from \"@fluidframework/protocol-base\";\nimport { ConnectionMode, IQuorumClients } from \"@fluidframework/protocol-definitions\";\nimport { PerformanceEvent } from \"@fluidframework/telemetry-utils\";\nimport { ConnectionState } from \"./connectionState\";\n\n/** Constructor parameter type for passing in dependencies needed by the ConnectionStateHandler */\nexport interface IConnectionStateHandlerInputs {\n /** Provides access to the clients currently in the quorum */\n quorumClients: () => IQuorumClients | undefined;\n /** Log to telemetry any change in state, included to Connecting */\n logConnectionStateChangeTelemetry:\n (value: ConnectionState, oldState: ConnectionState, reason?: string | undefined) => void;\n /** Whether to expect the client to join in write mode on next connection */\n shouldClientJoinWrite: () => boolean;\n /** (Optional) How long should we wait on our previous client's Leave op before transitioning to Connected again */\n maxClientLeaveWaitTime: number | undefined;\n /** Log an issue encountered while in the Connecting state. details will be logged as a JSON string */\n logConnectionIssue: (eventName: string, details?: ITelemetryProperties) => void;\n /** Callback whenever the ConnectionState changes between Disconnected and Connected */\n connectionStateChanged: () => void;\n}\n\nconst JoinOpTimeoutMs = 45000;\n\n/**\n * In the lifetime of a container, the connection will likely disconnect and reconnect periodically.\n * This class ensures that any ops sent by this container instance on previous connection are either\n * sequenced or blocked by the server before emitting the new \"connected\" event and allowing runtime to resubmit ops.\n *\n * Each connection is assigned a clientId by the service, and the connection is book-ended by a Join and a Leave op\n * generated by the service. Due to the distributed nature of the ordering service, in the case of reconnect we cannot\n * make any assumptions about ordering of operations between the old and new connections - i.e. new Join op could\n * be sequenced before old Leave op (and some acks from pending ops that were in flight when we disconnected).\n *\n * The job of this class is to encapsulate the transition period during reconnect, which is identified by\n * ConnectionState.CatchingUp. Specifically, before moving to Connected state with the new clientId, it ensures that:\n * (A) We process the Leave op for the previous clientId. This allows us to properly handle any acks from in-flight ops\n * that got sequenced with the old clientId (we'll recognize them as local ops). After the Leave op, any other\n * pending ops can safely be submitted with the new clientId without fear of duplication in the sequenced op stream.\n * (B) We process the Join op for the new clientId (identified when the underlying connection was first established),\n * indicating the service is ready to sequence ops sent with the new clientId.\n *\n * For (A) we give up waiting after some time (same timeout as server uses), and go ahead and transition to Connected.\n * For (B) we log telemetry if it takes too long, but still only transition to Connected when the Join op is processed\n * and we are added to the Quorum.\n */\nexport class ConnectionStateHandler {\n private _connectionState = ConnectionState.Disconnected;\n private _pendingClientId: string | undefined;\n private readonly prevClientLeftTimer: Timer;\n private readonly joinOpTimer: Timer;\n\n private waitEvent: PerformanceEvent | undefined;\n\n public get connectionState(): ConnectionState {\n return this._connectionState;\n }\n\n public get connected(): boolean {\n return this.connectionState === ConnectionState.Connected;\n }\n\n public get clientId(): string | undefined {\n return this._clientId;\n }\n\n public get pendingClientId(): string | undefined {\n return this._pendingClientId;\n }\n\n constructor(\n private readonly handler: IConnectionStateHandlerInputs,\n private readonly logger: ITelemetryLogger,\n private _clientId?: string,\n ) {\n this.prevClientLeftTimer = new Timer(\n // Default is 5 min for which we are going to wait for its own \"leave\" message. This is same as\n // the max time on server after which leave op is sent.\n this.handler.maxClientLeaveWaitTime ?? 300000,\n () => {\n assert(!this.connected,\n 0x2ac /* \"Connected when timeout waiting for leave from previous session fired!\" */);\n this.applyForConnectedState(\"timeout\");\n },\n );\n\n // Based on recent data, it looks like majority of cases where we get stuck are due to really slow or\n // timing out ops fetches. So attempt recovery infrequently. Also fetch uses 30 second timeout, so\n // if retrying fixes the problem, we should not see these events.\n this.joinOpTimer = new Timer(\n JoinOpTimeoutMs,\n () => {\n // I've observed timer firing within couple ms from disconnect event, looks like\n // queued timer callback is not cancelled if timer is cancelled while callback sits in the queue.\n if (this.connectionState !== ConnectionState.CatchingUp) {\n return;\n }\n const quorumClients = this.handler.quorumClients();\n const details = {\n quorumInitialized: quorumClients !== undefined,\n hasPendingClientId: this.pendingClientId !== undefined,\n inQuorum: quorumClients?.getMember(this.pendingClientId ?? \"\") !== undefined,\n waitingForLeaveOp: this.waitingForLeaveOp,\n };\n this.handler.logConnectionIssue(\"NoJoinOp\", details);\n },\n );\n }\n\n private startJoinOpTimer() {\n assert(!this.joinOpTimer.hasTimer, 0x234 /* \"has joinOpTimer\" */);\n this.joinOpTimer.start();\n }\n\n private stopJoinOpTimer() {\n assert(this.joinOpTimer.hasTimer, 0x235 /* \"no joinOpTimer\" */);\n this.joinOpTimer.clear();\n }\n\n private get waitingForLeaveOp() {\n return this.prevClientLeftTimer.hasTimer;\n }\n\n public dispose() {\n assert(!this.joinOpTimer.hasTimer, 0x2a5 /* \"join timer\" */);\n this.prevClientLeftTimer.clear();\n }\n\n public containerSaved() {\n // If we were waiting for moving to Connected state, then only apply for state change. Since the container\n // is now saved and we don't have any ops to roundtrip, we can clear the timer and apply for connected state.\n if (this.waitingForLeaveOp) {\n this.prevClientLeftTimer.clear();\n this.applyForConnectedState(\"containerSaved\");\n }\n }\n\n private receivedAddMemberEvent(clientId: string) {\n // This is the only one that requires the pending client ID\n if (clientId === this.pendingClientId) {\n if (this.joinOpTimer.hasTimer) {\n this.stopJoinOpTimer();\n } else {\n // timer has already fired, meaning it took too long to get join on.\n // Record how long it actually took to recover.\n this.handler.logConnectionIssue(\"ReceivedJoinOp\");\n }\n // Start the event in case we are waiting for leave or timeout.\n if (this.waitingForLeaveOp) {\n this.waitEvent = PerformanceEvent.start(this.logger, {\n eventName: \"WaitBeforeClientLeave\",\n details: JSON.stringify({\n waitOnClientId: this._clientId,\n hadOutstandingOps: this.handler.shouldClientJoinWrite(),\n }),\n });\n }\n this.applyForConnectedState(\"addMemberEvent\");\n }\n }\n\n private applyForConnectedState(source: \"removeMemberEvent\" | \"addMemberEvent\" | \"timeout\" | \"containerSaved\") {\n const quorumClients = this.handler.quorumClients();\n assert(quorumClients !== undefined, 0x236 /* \"In all cases it should be already installed\" */);\n\n assert(this.waitingForLeaveOp === false ||\n (this.clientId !== undefined && quorumClients.getMember(this.clientId) !== undefined),\n 0x2e2 /* \"Must only wait for leave message when clientId in quorum\" */);\n\n // Move to connected state only if we are in Connecting state, we have seen our join op\n // and there is no timer running which means we are not waiting for previous client to leave\n // or timeout has occurred while doing so.\n if (this.pendingClientId !== this.clientId\n && this.pendingClientId !== undefined\n && quorumClients.getMember(this.pendingClientId) !== undefined\n && !this.waitingForLeaveOp\n ) {\n this.waitEvent?.end({ source });\n this.setConnectionState(ConnectionState.Connected);\n } else {\n // Adding this event temporarily so that we can get help debugging if something goes wrong.\n this.logger.sendTelemetryEvent({\n eventName: \"connectedStateRejected\",\n category: source === \"timeout\" ? \"error\" : \"generic\",\n details: JSON.stringify({\n source,\n pendingClientId: this.pendingClientId,\n clientId: this.clientId,\n waitingForLeaveOp: this.waitingForLeaveOp,\n inQuorum: quorumClients?.getMember(this.pendingClientId ?? \"\") !== undefined,\n }),\n });\n }\n }\n\n private receivedRemoveMemberEvent(clientId: string) {\n // If the client which has left was us, then finish the timer.\n if (this.clientId === clientId) {\n this.prevClientLeftTimer.clear();\n this.applyForConnectedState(\"removeMemberEvent\");\n }\n }\n\n public receivedDisconnectEvent(reason: string) {\n if (this.joinOpTimer.hasTimer) {\n this.stopJoinOpTimer();\n }\n this.setConnectionState(ConnectionState.Disconnected, reason);\n }\n\n /**\n * The \"connect\" event indicates the connection to the Relay Service is live.\n * However, some additional conditions must be met before we can fully transition to\n * \"Connected\" state. This function handles that interim period, known as \"Connecting\" state.\n * @param connectionMode - Read or Write connection\n * @param details - Connection details returned from the ordering service\n */\n public receivedConnectEvent(\n connectionMode: ConnectionMode,\n details: IConnectionDetails,\n ) {\n const oldState = this._connectionState;\n this._connectionState = ConnectionState.CatchingUp;\n\n const writeConnection = connectionMode === \"write\";\n assert(writeConnection || !this.handler.shouldClientJoinWrite(),\n 0x30a /* shouldClientJoinWrite should imply this is a writeConnection */);\n assert(writeConnection || !this.waitingForLeaveOp,\n 0x2a6 /* \"waitingForLeaveOp should imply writeConnection (we need to be ready to flush pending ops)\" */);\n\n // Note that this may be undefined since the connection is established proactively on load\n // and the quorum may still be under initialization.\n const quorumClients: IQuorumClients | undefined = this.handler.quorumClients();\n\n // Stash the clientID to detect when transitioning from connecting (socket.io channel open) to connected\n // (have received the join message for the client ID)\n // This is especially important in the reconnect case. It's possible there could be outstanding\n // ops sent by this client, so we should keep the old client id until we see our own client's\n // join message. after we see the join message for our new connection with our new client id,\n // we know there can no longer be outstanding ops that we sent with the previous client id.\n this._pendingClientId = details.clientId;\n\n // IMPORTANT: Report telemetry after we set _pendingClientId, but before transitioning to Connected state\n this.handler.logConnectionStateChangeTelemetry(ConnectionState.CatchingUp, oldState);\n\n // For write connections, this pending clientId could be in the quorum already (i.e. join op already processed).\n // We are fetching ops from storage in parallel to connecting to Relay Service,\n // and given async processes, it's possible that we have already processed our own join message before\n // connection was fully established.\n // If quorumClients itself is undefined, we expect it will process the join op after it's initialized.\n const waitingForJoinOp = writeConnection && quorumClients?.getMember(this._pendingClientId) === undefined;\n\n if (waitingForJoinOp) {\n // Previous client left, and we are waiting for our own join op. When it is processed we'll join the quorum\n // and attempt to transition to Connected state via receivedAddMemberEvent.\n this.startJoinOpTimer();\n } else if (!this.waitingForLeaveOp) {\n // We're not waiting for Join or Leave op (if read-only connection those don't even apply),\n // go ahead and declare the state to be Connected!\n // If we are waiting for Leave op still, do nothing for now, we will transition to Connected later.\n this.setConnectionState(ConnectionState.Connected);\n }\n }\n\n private setConnectionState(value: ConnectionState.Disconnected, reason: string): void;\n private setConnectionState(value: ConnectionState.Connected): void;\n private setConnectionState(value: ConnectionState, reason?: string): void {\n if (this.connectionState === value) {\n // Already in the desired state - exit early\n this.logger.sendErrorEvent({ eventName: \"setConnectionStateSame\", value });\n return;\n }\n\n const oldState = this._connectionState;\n this._connectionState = value;\n const quorumClients = this.handler.quorumClients();\n let client: ILocalSequencedClient | undefined;\n if (this._clientId !== undefined) {\n client = quorumClients?.getMember(this._clientId);\n }\n if (value === ConnectionState.Connected) {\n assert(oldState === ConnectionState.CatchingUp,\n 0x1d8 /* \"Should only transition from Connecting state\" */);\n // Mark our old client should have left in the quorum if it's still there\n if (client !== undefined) {\n client.shouldHaveLeft = true;\n }\n this._clientId = this.pendingClientId;\n } else if (value === ConnectionState.Disconnected) {\n // Important as we process our own joinSession message through delta request\n this._pendingClientId = undefined;\n // Only wait for \"leave\" message if the connected client exists in the quorum because only the write\n // client will exist in the quorum and only for those clients we will receive \"removeMember\" event and\n // the client has some unacked ops.\n // Also server would not accept ops from read client. Also check if the timer is not already running as\n // we could receive \"Disconnected\" event multiple times without getting connected and in that case we\n // don't want to reset the timer as we still want to wait on original client which started this timer.\n if (client !== undefined\n && this.handler.shouldClientJoinWrite()\n && this.prevClientLeftTimer.hasTimer === false\n ) {\n this.prevClientLeftTimer.restart();\n } else {\n // Adding this event temporarily so that we can get help debugging if something goes wrong.\n this.logger.sendTelemetryEvent({\n eventName: \"noWaitOnDisconnected\",\n details: JSON.stringify({\n inQuorum: client !== undefined,\n waitingForLeaveOp: this.waitingForLeaveOp,\n hadOutstandingOps: this.handler.shouldClientJoinWrite(),\n }),\n });\n }\n }\n\n // Report transition before we propagate event across layers\n this.handler.logConnectionStateChangeTelemetry(this._connectionState, oldState, reason);\n\n // Propagate event across layers\n this.handler.connectionStateChanged();\n }\n\n public initProtocol(protocol: IProtocolHandler) {\n protocol.quorum.on(\"addMember\", (clientId, _details) => {\n this.receivedAddMemberEvent(clientId);\n });\n\n protocol.quorum.on(\"removeMember\", (clientId) => {\n this.receivedRemoveMemberEvent(clientId);\n });\n\n // if we have a clientId from a previous container we need to wait for its leave message\n if (this.clientId !== undefined && protocol.quorum.getMember(this.clientId) !== undefined) {\n this.prevClientLeftTimer.restart();\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"connectionStateHandler.js","sourceRoot":"","sources":["../src/connectionStateHandler.ts"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport { ITelemetryLogger, ITelemetryProperties } from \"@fluidframework/common-definitions\";\nimport { assert, Timer } from \"@fluidframework/common-utils\";\nimport { IConnectionDetails, IDeltaManager } from \"@fluidframework/container-definitions\";\nimport { ILocalSequencedClient } from \"@fluidframework/protocol-base\";\nimport { ConnectionMode } from \"@fluidframework/protocol-definitions\";\nimport { PerformanceEvent, loggerToMonitoringContext } from \"@fluidframework/telemetry-utils\";\nimport { ConnectionState } from \"./connectionState\";\nimport { CatchUpMonitor, ICatchUpMonitor } from \"./catchUpMonitor\";\nimport { IProtocolHandler } from \"./protocol\";\n\nconst JoinOpTimeoutMs = 45000;\n\n/** Constructor parameter type for passing in dependencies needed by the ConnectionStateHandler */\nexport interface IConnectionStateHandlerInputs {\n logger: ITelemetryLogger;\n /** Log to telemetry any change in state, included to Connecting */\n connectionStateChanged:\n (value: ConnectionState, oldState: ConnectionState, reason?: string | undefined) => void;\n /** Whether to expect the client to join in write mode on next connection */\n shouldClientJoinWrite: () => boolean;\n /** (Optional) How long should we wait on our previous client's Leave op before transitioning to Connected again */\n maxClientLeaveWaitTime: number | undefined;\n /** Log an issue encountered while in the Connecting state. details will be logged as a JSON string */\n logConnectionIssue: (eventName: string, details?: ITelemetryProperties) => void;\n}\n\n/**\n * interface that connection state handler implements\n */\nexport interface IConnectionStateHandler {\n readonly connectionState: ConnectionState;\n readonly pendingClientId: string | undefined;\n\n containerSaved(): void;\n dispose(): void;\n initProtocol(protocol: IProtocolHandler): void;\n receivedConnectEvent(connectionMode: ConnectionMode, details: IConnectionDetails): void;\n receivedDisconnectEvent(reason: string): void;\n}\n\nexport function createConnectionStateHandler(\n inputs: IConnectionStateHandlerInputs,\n deltaManager: IDeltaManager<any, any>,\n clientId?: string,\n) {\n const mc = loggerToMonitoringContext(inputs.logger);\n return createConnectionStateHandlerCore(\n mc.config.getBoolean(\"Fluid.Container.CatchUpBeforeDeclaringConnected\") === true,\n inputs,\n deltaManager,\n clientId,\n );\n}\n\nexport function createConnectionStateHandlerCore(\n wait: boolean,\n inputs: IConnectionStateHandlerInputs,\n deltaManager: IDeltaManager<any, any>,\n clientId?: string,\n) {\n if (!wait) {\n return new ConnectionStateHandler(inputs, clientId);\n }\n return new ConnectionStateCatchup(\n inputs,\n (handler: IConnectionStateHandlerInputs) => new ConnectionStateHandler(handler, clientId),\n deltaManager);\n}\n\n/**\n * Class that can be used as a base class for building IConnectionStateHandler adapters / pipeline.\n * It implements both ends of communication interfaces and passes data back and forward\n */\nclass ConnectionStateHandlerPassThrough implements IConnectionStateHandler, IConnectionStateHandlerInputs {\n protected readonly pimpl: IConnectionStateHandler;\n\n constructor(\n protected readonly inputs: IConnectionStateHandlerInputs,\n pimplFactory: (handler: IConnectionStateHandlerInputs) => IConnectionStateHandler,\n ) {\n this.pimpl = pimplFactory(this);\n }\n\n /**\n * IConnectionStateHandler\n */\n public get connectionState() { return this.pimpl.connectionState; }\n public get pendingClientId() { return this.pimpl.pendingClientId; }\n\n public containerSaved() { return this.pimpl.containerSaved(); }\n public dispose() { return this.pimpl.dispose(); }\n public initProtocol(protocol: IProtocolHandler) { return this.pimpl.initProtocol(protocol); }\n public receivedDisconnectEvent(reason: string) { return this.pimpl.receivedDisconnectEvent(reason); }\n\n public receivedConnectEvent(connectionMode: ConnectionMode, details: IConnectionDetails) {\n return this.pimpl.receivedConnectEvent(connectionMode, details);\n }\n\n /**\n * IConnectionStateHandlerInputs\n */\n\n public get logger() { return this.inputs.logger; }\n public connectionStateChanged(\n value: ConnectionState,\n oldState: ConnectionState,\n reason?: string | undefined,\n ) {\n return this.inputs.connectionStateChanged(value, oldState, reason);\n }\n public shouldClientJoinWrite() { return this.inputs.shouldClientJoinWrite(); }\n public get maxClientLeaveWaitTime() { return this.inputs.maxClientLeaveWaitTime; }\n public logConnectionIssue(eventName: string, details?: ITelemetryProperties) {\n return this.inputs.logConnectionIssue(eventName, details);\n }\n}\n\n/**\n * Implementation of IConnectionStateHandler pass-through adapter that waits for specific sequence number\n * before raising connected event\n */\nclass ConnectionStateCatchup extends ConnectionStateHandlerPassThrough {\n private catchUpMonitor: ICatchUpMonitor | undefined;\n\n constructor(\n inputs: IConnectionStateHandlerInputs,\n pimplFactory: (handler: IConnectionStateHandlerInputs) => IConnectionStateHandler,\n private readonly deltaManager: IDeltaManager<any, any>,\n ) {\n super(inputs, pimplFactory);\n this._connectionState = this.pimpl.connectionState;\n }\n\n private _connectionState: ConnectionState;\n public get connectionState() {\n return this._connectionState;\n }\n\n public connectionStateChanged(value: ConnectionState, oldState: ConnectionState, reason?: string | undefined) {\n switch (value) {\n case ConnectionState.Connected:\n assert(this._connectionState === ConnectionState.CatchingUp, 0x3e1 /* connectivity transitions */);\n // Create catch-up monitor here (not earlier), as we might get more exact info by now about how far\n // client is behind through join signal. This is only true if base layer uses signals (i.e. audience,\n // not quorum, including for \"rea\" connections) to make decisions about moving to \"connected\" state.\n // In addition to that, in its current form, doing this in ConnectionState.CatchingUp is dangerous as\n // we might get callback right away, and it will screw up state transition (as code outside of switch\n // statement will overwrite current state).\n assert(this.catchUpMonitor === undefined, 0x3eb /* catchUpMonitor should be gone */);\n this.catchUpMonitor = new CatchUpMonitor(this.deltaManager, this.transitionToConnectedState);\n return;\n case ConnectionState.Disconnected:\n this.catchUpMonitor?.dispose();\n this.catchUpMonitor = undefined;\n break;\n case ConnectionState.CatchingUp:\n assert(this._connectionState === ConnectionState.Disconnected, 0x3e3 /* connectivity transitions */);\n break;\n default:\n }\n this._connectionState = value;\n this.inputs.connectionStateChanged(value, oldState, reason);\n }\n\n private readonly transitionToConnectedState = () => {\n // Defensive measure, we should always be in Connecting state when this is called.\n const state = this.pimpl.connectionState;\n assert(state === ConnectionState.Connected, 0x3e5 /* invariant broken */);\n assert(this._connectionState === ConnectionState.CatchingUp, 0x3e6 /* invariant broken */);\n this._connectionState = ConnectionState.Connected;\n this.inputs.connectionStateChanged(ConnectionState.Connected, ConnectionState.CatchingUp, \"caught up\");\n };\n}\n\n/**\n * In the lifetime of a container, the connection will likely disconnect and reconnect periodically.\n * This class ensures that any ops sent by this container instance on previous connection are either\n * sequenced or blocked by the server before emitting the new \"connected\" event and allowing runtime to resubmit ops.\n *\n * Each connection is assigned a clientId by the service, and the connection is book-ended by a Join and a Leave op\n * generated by the service. Due to the distributed nature of the Relay Service, in the case of reconnect we cannot\n * make any assumptions about ordering of operations between the old and new connections - i.e. new Join op could\n * be sequenced before old Leave op (and some acks from pending ops that were in flight when we disconnected).\n *\n * The job of this class is to encapsulate the transition period during reconnect, which is identified by\n * ConnectionState.CatchingUp. Specifically, before moving to Connected state with the new clientId, it ensures that:\n *\n * a. We process the Leave op for the previous clientId. This allows us to properly handle any acks from in-flight ops\n * that got sequenced with the old clientId (we'll recognize them as local ops). After the Leave op, any other\n * pending ops can safely be submitted with the new clientId without fear of duplication in the sequenced op stream.\n *\n * b. We process the Join op for the new clientId (identified when the underlying connection was first established),\n * indicating the service is ready to sequence ops sent with the new clientId.\n *\n * c. We process all ops known at the time the underlying connection was established (so we are \"caught up\")\n *\n * For (a) we give up waiting after some time (same timeout as server uses), and go ahead and transition to Connected.\n *\n * For (b) we log telemetry if it takes too long, but still only transition to Connected when the Join op is processed\n * and we are added to the Quorum.\n *\n * For (c) this is optional behavior, controlled by the parameters of receivedConnectEvent\n */\nclass ConnectionStateHandler implements IConnectionStateHandler {\n private _connectionState = ConnectionState.Disconnected;\n private _pendingClientId: string | undefined;\n private readonly prevClientLeftTimer: Timer;\n private readonly joinOpTimer: Timer;\n private protocol?: IProtocolHandler;\n\n private waitEvent: PerformanceEvent | undefined;\n\n public get connectionState(): ConnectionState {\n return this._connectionState;\n }\n\n private get clientId(): string | undefined {\n return this._clientId;\n }\n\n public get pendingClientId(): string | undefined {\n return this._pendingClientId;\n }\n\n constructor(\n private readonly handler: IConnectionStateHandlerInputs,\n private _clientId?: string,\n ) {\n this.prevClientLeftTimer = new Timer(\n // Default is 5 min for which we are going to wait for its own \"leave\" message. This is same as\n // the max time on server after which leave op is sent.\n this.handler.maxClientLeaveWaitTime ?? 300000,\n () => {\n assert(this.connectionState !== ConnectionState.Connected,\n 0x2ac /* \"Connected when timeout waiting for leave from previous session fired!\" */);\n this.applyForConnectedState(\"timeout\");\n },\n );\n\n // Based on recent data, it looks like majority of cases where we get stuck are due to really slow or\n // timing out ops fetches. So attempt recovery infrequently. Also fetch uses 30 second timeout, so\n // if retrying fixes the problem, we should not see these events.\n this.joinOpTimer = new Timer(\n JoinOpTimeoutMs,\n () => {\n // I've observed timer firing within couple ms from disconnect event, looks like\n // queued timer callback is not cancelled if timer is cancelled while callback sits in the queue.\n if (this.connectionState !== ConnectionState.CatchingUp) {\n return;\n }\n const details = {\n protocolInitialized: this.protocol !== undefined,\n pendingClientId: this.pendingClientId,\n clientJoined: this.hasMember(this.pendingClientId),\n waitingForLeaveOp: this.waitingForLeaveOp,\n };\n this.handler.logConnectionIssue(\"NoJoinOp\", details);\n },\n );\n }\n\n private startJoinOpTimer() {\n assert(!this.joinOpTimer.hasTimer, 0x234 /* \"has joinOpTimer\" */);\n this.joinOpTimer.start();\n }\n\n private stopJoinOpTimer() {\n assert(this.joinOpTimer.hasTimer, 0x235 /* \"no joinOpTimer\" */);\n this.joinOpTimer.clear();\n }\n\n private get waitingForLeaveOp() {\n return this.prevClientLeftTimer.hasTimer;\n }\n\n public dispose() {\n assert(!this.joinOpTimer.hasTimer, 0x2a5 /* \"join timer\" */);\n this.prevClientLeftTimer.clear();\n }\n\n public containerSaved() {\n // If we were waiting for moving to Connected state, then only apply for state change. Since the container\n // is now saved and we don't have any ops to roundtrip, we can clear the timer and apply for connected state.\n if (this.waitingForLeaveOp) {\n this.prevClientLeftTimer.clear();\n this.applyForConnectedState(\"containerSaved\");\n }\n }\n\n private receivedAddMemberEvent(clientId: string) {\n // This is the only one that requires the pending client ID\n if (clientId === this.pendingClientId) {\n if (this.joinOpTimer.hasTimer) {\n this.stopJoinOpTimer();\n } else {\n // timer has already fired, meaning it took too long to get join on.\n // Record how long it actually took to recover.\n this.handler.logConnectionIssue(\"ReceivedJoinOp\");\n }\n // Start the event in case we are waiting for leave or timeout.\n if (this.waitingForLeaveOp) {\n this.waitEvent = PerformanceEvent.start(this.handler.logger, {\n eventName: \"WaitBeforeClientLeave\",\n details: JSON.stringify({\n waitOnClientId: this._clientId,\n hadOutstandingOps: this.handler.shouldClientJoinWrite(),\n }),\n });\n }\n this.applyForConnectedState(\"addMemberEvent\");\n }\n }\n\n private applyForConnectedState(source: \"removeMemberEvent\" | \"addMemberEvent\" | \"timeout\" | \"containerSaved\") {\n assert(this.protocol !== undefined, 0x236 /* \"In all cases it should be already installed\" */);\n\n assert(!this.waitingForLeaveOp || this.hasMember(this.clientId),\n 0x2e2 /* \"Must only wait for leave message when clientId in quorum\" */);\n\n // Move to connected state only if we are in Connecting state, we have seen our join op\n // and there is no timer running which means we are not waiting for previous client to leave\n // or timeout has occurred while doing so.\n if (this.pendingClientId !== this.clientId\n && this.hasMember(this.pendingClientId)\n && !this.waitingForLeaveOp\n ) {\n this.waitEvent?.end({ source });\n this.setConnectionState(ConnectionState.Connected);\n } else {\n // Adding this event temporarily so that we can get help debugging if something goes wrong.\n this.handler.logger.sendTelemetryEvent({\n eventName: \"connectedStateRejected\",\n category: source === \"timeout\" ? \"error\" : \"generic\",\n details: JSON.stringify({\n source,\n pendingClientId: this.pendingClientId,\n clientId: this.clientId,\n waitingForLeaveOp: this.waitingForLeaveOp,\n clientJoined: this.hasMember(this.pendingClientId),\n }),\n });\n }\n }\n\n private receivedRemoveMemberEvent(clientId: string) {\n // If the client which has left was us, then finish the timer.\n if (this.clientId === clientId) {\n this.prevClientLeftTimer.clear();\n this.applyForConnectedState(\"removeMemberEvent\");\n }\n }\n\n public receivedDisconnectEvent(reason: string) {\n this.setConnectionState(ConnectionState.Disconnected, reason);\n }\n\n /**\n * The \"connect\" event indicates the connection to the Relay Service is live.\n * However, some additional conditions must be met before we can fully transition to\n * \"Connected\" state. This function handles that interim period, known as \"Connecting\" state.\n * @param connectionMode - Read or Write connection\n * @param details - Connection details returned from the Relay Service\n * @param deltaManager - DeltaManager to be used for delaying Connected transition until caught up.\n * If it's undefined, then don't delay and transition to Connected as soon as Leave/Join op are accounted for\n */\n public receivedConnectEvent(\n connectionMode: ConnectionMode,\n details: IConnectionDetails,\n ) {\n const oldState = this._connectionState;\n this._connectionState = ConnectionState.CatchingUp;\n\n const writeConnection = connectionMode === \"write\";\n\n // The following checks are wrong. They are only valid if user has write access to a file.\n // If user lost such access mid-session, user will not be able to get \"write\" connection.\n // assert(!this.handler.shouldClientJoinWrite() || writeConnection,\n // 0x30a /* shouldClientJoinWrite should imply this is a writeConnection */);\n // assert(!this.waitingForLeaveOp || writeConnection,\n // 0x2a6 /* \"waitingForLeaveOp should imply writeConnection (we need to be ready to flush pending ops)\" */);\n\n // Stash the clientID to detect when transitioning from connecting (socket.io channel open) to connected\n // (have received the join message for the client ID)\n // This is especially important in the reconnect case. It's possible there could be outstanding\n // ops sent by this client, so we should keep the old client id until we see our own client's\n // join message. after we see the join message for our new connection with our new client id,\n // we know there can no longer be outstanding ops that we sent with the previous client id.\n this._pendingClientId = details.clientId;\n\n // IMPORTANT: Report telemetry after we set _pendingClientId, but before transitioning to Connected state\n this.handler.connectionStateChanged(ConnectionState.CatchingUp, oldState);\n\n // For write connections, this pending clientId could be in the quorum already (i.e. join op already processed).\n // We are fetching ops from storage in parallel to connecting to Relay Service,\n // and given async processes, it's possible that we have already processed our own join message before\n // connection was fully established.\n // If protocol is not initialized yet, we expect it will process the join op after it's initialized.\n const waitingForJoinOp = writeConnection && !this.hasMember(this._pendingClientId);\n\n if (waitingForJoinOp) {\n // Previous client left, and we are waiting for our own join op. When it is processed we'll join the quorum\n // and attempt to transition to Connected state via receivedAddMemberEvent.\n this.startJoinOpTimer();\n } else if (!this.waitingForLeaveOp) {\n // We're not waiting for Join or Leave op (if read-only connection those don't even apply),\n // go ahead and declare the state to be Connected!\n // If we are waiting for Leave op still, do nothing for now, we will transition to Connected later.\n this.setConnectionState(ConnectionState.Connected);\n }\n // else - We are waiting for Leave op still, do nothing for now, we will transition to Connected later\n }\n\n private setConnectionState(value: ConnectionState.Disconnected, reason: string): void;\n private setConnectionState(value: ConnectionState.Connected): void;\n private setConnectionState(value: ConnectionState.Disconnected | ConnectionState.Connected, reason?: string): void {\n if (this.connectionState === value) {\n // Already in the desired state - exit early\n this.handler.logger.sendErrorEvent({ eventName: \"setConnectionStateSame\", value });\n return;\n }\n\n const oldState = this._connectionState;\n this._connectionState = value;\n let client: ILocalSequencedClient | undefined;\n if (this._clientId !== undefined) {\n client = this.protocol?.quorum?.getMember(this._clientId);\n }\n if (value === ConnectionState.Connected) {\n assert(oldState === ConnectionState.CatchingUp,\n 0x1d8 /* \"Should only transition from Connecting state\" */);\n // Mark our old client should have left in the quorum if it's still there\n if (client !== undefined) {\n client.shouldHaveLeft = true;\n }\n this._clientId = this.pendingClientId;\n } else if (value === ConnectionState.Disconnected) {\n // Clear pending state immediately to prepare for reconnect\n this._pendingClientId = undefined;\n\n if (this.joinOpTimer.hasTimer) {\n this.stopJoinOpTimer();\n }\n\n // Only wait for \"leave\" message if the connected client exists in the quorum and had some non-acked ops\n // Also check if the timer is not already running as\n // we could receive \"Disconnected\" event multiple times without getting connected and in that case we\n // don't want to reset the timer as we still want to wait on original client which started this timer.\n if (client !== undefined\n && this.handler.shouldClientJoinWrite()\n && !this.waitingForLeaveOp // same as !this.prevClientLeftTimer.hasTimer\n ) {\n this.prevClientLeftTimer.restart();\n } else {\n // Adding this event temporarily so that we can get help debugging if something goes wrong.\n this.handler.logger.sendTelemetryEvent({\n eventName: \"noWaitOnDisconnected\",\n details: JSON.stringify({\n clientId: this._clientId,\n inQuorum: client !== undefined,\n waitingForLeaveOp: this.waitingForLeaveOp,\n hadOutstandingOps: this.handler.shouldClientJoinWrite(),\n }),\n });\n }\n }\n\n // Report transition before we propagate event across layers\n this.handler.connectionStateChanged(this._connectionState, oldState, reason);\n }\n\n // Helper method to switch between quorum and audience.\n // Old design was checking only quorum for \"write\" clients.\n // Latest change checks audience for all types of connections.\n protected get membership() {\n return this.protocol?.quorum;\n }\n\n public initProtocol(protocol: IProtocolHandler) {\n this.protocol = protocol;\n\n this.membership?.on(\"addMember\", (clientId) => {\n this.receivedAddMemberEvent(clientId);\n });\n\n this.membership?.on(\"removeMember\", (clientId) => {\n this.receivedRemoveMemberEvent(clientId);\n });\n\n // Very unlikely race condition, but theoretically can happen - our new connection is already\n // summarized and we are loading from such summary.\n if (this.hasMember(this.pendingClientId)) {\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n this.receivedAddMemberEvent(this.pendingClientId!);\n }\n\n // if we have a clientId from a previous container we need to wait for its leave message\n if (this.clientId !== undefined && this.hasMember(this.clientId)) {\n this.prevClientLeftTimer.restart();\n }\n }\n\n protected hasMember(clientId?: string) {\n return this.membership?.getMember(clientId ?? \"\") !== undefined;\n }\n}\n"]}
|