@fluidframework/container-loader 2.0.0-internal.1.1.2 → 2.0.0-internal.1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/catchUpMonitor.d.ts +6 -17
- package/dist/catchUpMonitor.d.ts.map +1 -1
- package/dist/catchUpMonitor.js +5 -36
- package/dist/catchUpMonitor.js.map +1 -1
- package/dist/collabWindowTracker.d.ts +1 -1
- package/dist/collabWindowTracker.d.ts.map +1 -1
- package/dist/collabWindowTracker.js +2 -1
- package/dist/collabWindowTracker.js.map +1 -1
- package/dist/connectionManager.d.ts +1 -1
- package/dist/connectionManager.d.ts.map +1 -1
- package/dist/connectionManager.js +8 -11
- package/dist/connectionManager.js.map +1 -1
- package/dist/connectionStateHandler.d.ts +80 -26
- package/dist/connectionStateHandler.d.ts.map +1 -1
- package/dist/connectionStateHandler.js +170 -89
- package/dist/connectionStateHandler.js.map +1 -1
- package/dist/container.d.ts +22 -11
- package/dist/container.d.ts.map +1 -1
- package/dist/container.js +130 -142
- 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 +10 -24
- package/dist/containerStorageAdapter.d.ts.map +1 -1
- package/dist/containerStorageAdapter.js +50 -16
- package/dist/containerStorageAdapter.js.map +1 -1
- package/dist/deltaManager.d.ts +1 -1
- package/dist/deltaManager.d.ts.map +1 -1
- package/dist/deltaManager.js +18 -6
- package/dist/deltaManager.js.map +1 -1
- package/dist/loader.d.ts +1 -1
- package/dist/loader.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.map +1 -1
- package/dist/protocol.js +2 -1
- package/dist/protocol.js.map +1 -1
- package/lib/catchUpMonitor.d.ts +6 -17
- package/lib/catchUpMonitor.d.ts.map +1 -1
- package/lib/catchUpMonitor.js +5 -35
- package/lib/catchUpMonitor.js.map +1 -1
- package/lib/collabWindowTracker.d.ts +1 -1
- package/lib/collabWindowTracker.d.ts.map +1 -1
- package/lib/collabWindowTracker.js +3 -2
- package/lib/collabWindowTracker.js.map +1 -1
- package/lib/connectionManager.d.ts +1 -1
- package/lib/connectionManager.d.ts.map +1 -1
- package/lib/connectionManager.js +9 -14
- package/lib/connectionManager.js.map +1 -1
- package/lib/connectionStateHandler.d.ts +80 -26
- package/lib/connectionStateHandler.d.ts.map +1 -1
- package/lib/connectionStateHandler.js +170 -90
- package/lib/connectionStateHandler.js.map +1 -1
- package/lib/container.d.ts +22 -11
- package/lib/container.d.ts.map +1 -1
- package/lib/container.js +133 -145
- 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 +10 -24
- package/lib/containerStorageAdapter.d.ts.map +1 -1
- package/lib/containerStorageAdapter.js +50 -15
- package/lib/containerStorageAdapter.js.map +1 -1
- package/lib/deltaManager.d.ts +1 -1
- package/lib/deltaManager.d.ts.map +1 -1
- package/lib/deltaManager.js +18 -6
- package/lib/deltaManager.js.map +1 -1
- package/lib/loader.d.ts +1 -1
- package/lib/loader.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.map +1 -1
- package/lib/protocol.js +2 -1
- package/lib/protocol.js.map +1 -1
- package/package.json +13 -13
- package/src/catchUpMonitor.ts +7 -47
- package/src/collabWindowTracker.ts +4 -3
- package/src/connectionManager.ts +9 -11
- package/src/connectionStateHandler.ts +231 -106
- package/src/container.ts +156 -168
- package/src/containerContext.ts +22 -8
- package/src/containerStorageAdapter.ts +64 -15
- package/src/deltaManager.ts +20 -7
- package/src/loader.ts +1 -1
- package/src/packageVersion.ts +1 -1
- package/src/protocol.ts +2 -1
|
@@ -6,18 +6,20 @@
|
|
|
6
6
|
import { ITelemetryLogger, ITelemetryProperties } from "@fluidframework/common-definitions";
|
|
7
7
|
import { assert, Timer } from "@fluidframework/common-utils";
|
|
8
8
|
import { IConnectionDetails, IDeltaManager } from "@fluidframework/container-definitions";
|
|
9
|
-
import { ILocalSequencedClient
|
|
10
|
-
import { ConnectionMode
|
|
11
|
-
import { PerformanceEvent } from "@fluidframework/telemetry-utils";
|
|
9
|
+
import { ILocalSequencedClient } from "@fluidframework/protocol-base";
|
|
10
|
+
import { ConnectionMode } from "@fluidframework/protocol-definitions";
|
|
11
|
+
import { PerformanceEvent, loggerToMonitoringContext } from "@fluidframework/telemetry-utils";
|
|
12
12
|
import { ConnectionState } from "./connectionState";
|
|
13
|
-
import { CatchUpMonitor, ICatchUpMonitor
|
|
13
|
+
import { CatchUpMonitor, ICatchUpMonitor } from "./catchUpMonitor";
|
|
14
|
+
import { IProtocolHandler } from "./protocol";
|
|
15
|
+
|
|
16
|
+
const JoinOpTimeoutMs = 45000;
|
|
14
17
|
|
|
15
18
|
/** Constructor parameter type for passing in dependencies needed by the ConnectionStateHandler */
|
|
16
19
|
export interface IConnectionStateHandlerInputs {
|
|
17
|
-
|
|
18
|
-
quorumClients: () => IQuorumClients | undefined;
|
|
20
|
+
logger: ITelemetryLogger;
|
|
19
21
|
/** Log to telemetry any change in state, included to Connecting */
|
|
20
|
-
|
|
22
|
+
connectionStateChanged:
|
|
21
23
|
(value: ConnectionState, oldState: ConnectionState, reason?: string | undefined) => void;
|
|
22
24
|
/** Whether to expect the client to join in write mode on next connection */
|
|
23
25
|
shouldClientJoinWrite: () => boolean;
|
|
@@ -25,11 +27,155 @@ export interface IConnectionStateHandlerInputs {
|
|
|
25
27
|
maxClientLeaveWaitTime: number | undefined;
|
|
26
28
|
/** Log an issue encountered while in the Connecting state. details will be logged as a JSON string */
|
|
27
29
|
logConnectionIssue: (eventName: string, details?: ITelemetryProperties) => void;
|
|
28
|
-
/** Callback whenever the ConnectionState changes between Disconnected and Connected */
|
|
29
|
-
connectionStateChanged: () => void;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
/**
|
|
33
|
+
* interface that connection state handler implements
|
|
34
|
+
*/
|
|
35
|
+
export interface IConnectionStateHandler {
|
|
36
|
+
readonly connectionState: ConnectionState;
|
|
37
|
+
readonly pendingClientId: string | undefined;
|
|
38
|
+
|
|
39
|
+
containerSaved(): void;
|
|
40
|
+
dispose(): void;
|
|
41
|
+
initProtocol(protocol: IProtocolHandler): void;
|
|
42
|
+
receivedConnectEvent(connectionMode: ConnectionMode, details: IConnectionDetails): void;
|
|
43
|
+
receivedDisconnectEvent(reason: string): void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function createConnectionStateHandler(
|
|
47
|
+
inputs: IConnectionStateHandlerInputs,
|
|
48
|
+
deltaManager: IDeltaManager<any, any>,
|
|
49
|
+
clientId?: string,
|
|
50
|
+
) {
|
|
51
|
+
const mc = loggerToMonitoringContext(inputs.logger);
|
|
52
|
+
return createConnectionStateHandlerCore(
|
|
53
|
+
mc.config.getBoolean("Fluid.Container.CatchUpBeforeDeclaringConnected") === true,
|
|
54
|
+
inputs,
|
|
55
|
+
deltaManager,
|
|
56
|
+
clientId,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function createConnectionStateHandlerCore(
|
|
61
|
+
wait: boolean,
|
|
62
|
+
inputs: IConnectionStateHandlerInputs,
|
|
63
|
+
deltaManager: IDeltaManager<any, any>,
|
|
64
|
+
clientId?: string,
|
|
65
|
+
) {
|
|
66
|
+
if (!wait) {
|
|
67
|
+
return new ConnectionStateHandler(inputs, clientId);
|
|
68
|
+
}
|
|
69
|
+
return new ConnectionStateCatchup(
|
|
70
|
+
inputs,
|
|
71
|
+
(handler: IConnectionStateHandlerInputs) => new ConnectionStateHandler(handler, clientId),
|
|
72
|
+
deltaManager);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Class that can be used as a base class for building IConnectionStateHandler adapters / pipeline.
|
|
77
|
+
* It implements both ends of communication interfaces and passes data back and forward
|
|
78
|
+
*/
|
|
79
|
+
class ConnectionStateHandlerPassThrough implements IConnectionStateHandler, IConnectionStateHandlerInputs {
|
|
80
|
+
protected readonly pimpl: IConnectionStateHandler;
|
|
81
|
+
|
|
82
|
+
constructor(
|
|
83
|
+
protected readonly inputs: IConnectionStateHandlerInputs,
|
|
84
|
+
pimplFactory: (handler: IConnectionStateHandlerInputs) => IConnectionStateHandler,
|
|
85
|
+
) {
|
|
86
|
+
this.pimpl = pimplFactory(this);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* IConnectionStateHandler
|
|
91
|
+
*/
|
|
92
|
+
public get connectionState() { return this.pimpl.connectionState; }
|
|
93
|
+
public get pendingClientId() { return this.pimpl.pendingClientId; }
|
|
94
|
+
|
|
95
|
+
public containerSaved() { return this.pimpl.containerSaved(); }
|
|
96
|
+
public dispose() { return this.pimpl.dispose(); }
|
|
97
|
+
public initProtocol(protocol: IProtocolHandler) { return this.pimpl.initProtocol(protocol); }
|
|
98
|
+
public receivedDisconnectEvent(reason: string) { return this.pimpl.receivedDisconnectEvent(reason); }
|
|
99
|
+
|
|
100
|
+
public receivedConnectEvent(connectionMode: ConnectionMode, details: IConnectionDetails) {
|
|
101
|
+
return this.pimpl.receivedConnectEvent(connectionMode, details);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* IConnectionStateHandlerInputs
|
|
106
|
+
*/
|
|
107
|
+
|
|
108
|
+
public get logger() { return this.inputs.logger; }
|
|
109
|
+
public connectionStateChanged(
|
|
110
|
+
value: ConnectionState,
|
|
111
|
+
oldState: ConnectionState,
|
|
112
|
+
reason?: string | undefined,
|
|
113
|
+
) {
|
|
114
|
+
return this.inputs.connectionStateChanged(value, oldState, reason);
|
|
115
|
+
}
|
|
116
|
+
public shouldClientJoinWrite() { return this.inputs.shouldClientJoinWrite(); }
|
|
117
|
+
public get maxClientLeaveWaitTime() { return this.inputs.maxClientLeaveWaitTime; }
|
|
118
|
+
public logConnectionIssue(eventName: string, details?: ITelemetryProperties) {
|
|
119
|
+
return this.inputs.logConnectionIssue(eventName, details);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Implementation of IConnectionStateHandler pass-through adapter that waits for specific sequence number
|
|
125
|
+
* before raising connected event
|
|
126
|
+
*/
|
|
127
|
+
class ConnectionStateCatchup extends ConnectionStateHandlerPassThrough {
|
|
128
|
+
private catchUpMonitor: ICatchUpMonitor | undefined;
|
|
129
|
+
|
|
130
|
+
constructor(
|
|
131
|
+
inputs: IConnectionStateHandlerInputs,
|
|
132
|
+
pimplFactory: (handler: IConnectionStateHandlerInputs) => IConnectionStateHandler,
|
|
133
|
+
private readonly deltaManager: IDeltaManager<any, any>,
|
|
134
|
+
) {
|
|
135
|
+
super(inputs, pimplFactory);
|
|
136
|
+
this._connectionState = this.pimpl.connectionState;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private _connectionState: ConnectionState;
|
|
140
|
+
public get connectionState() {
|
|
141
|
+
return this._connectionState;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
public connectionStateChanged(value: ConnectionState, oldState: ConnectionState, reason?: string | undefined) {
|
|
145
|
+
switch (value) {
|
|
146
|
+
case ConnectionState.Connected:
|
|
147
|
+
assert(this._connectionState === ConnectionState.CatchingUp, 0x3e1 /* connectivity transitions */);
|
|
148
|
+
// Create catch-up monitor here (not earlier), as we might get more exact info by now about how far
|
|
149
|
+
// client is behind through join signal. This is only true if base layer uses signals (i.e. audience,
|
|
150
|
+
// not quorum, including for "rea" connections) to make decisions about moving to "connected" state.
|
|
151
|
+
// In addition to that, in its current form, doing this in ConnectionState.CatchingUp is dangerous as
|
|
152
|
+
// we might get callback right away, and it will screw up state transition (as code outside of switch
|
|
153
|
+
// statement will overwrite current state).
|
|
154
|
+
assert(this.catchUpMonitor === undefined, 0x3eb /* catchUpMonitor should be gone */);
|
|
155
|
+
this.catchUpMonitor = new CatchUpMonitor(this.deltaManager, this.transitionToConnectedState);
|
|
156
|
+
return;
|
|
157
|
+
case ConnectionState.Disconnected:
|
|
158
|
+
this.catchUpMonitor?.dispose();
|
|
159
|
+
this.catchUpMonitor = undefined;
|
|
160
|
+
break;
|
|
161
|
+
case ConnectionState.CatchingUp:
|
|
162
|
+
assert(this._connectionState === ConnectionState.Disconnected, 0x3e3 /* connectivity transitions */);
|
|
163
|
+
break;
|
|
164
|
+
default:
|
|
165
|
+
}
|
|
166
|
+
this._connectionState = value;
|
|
167
|
+
this.inputs.connectionStateChanged(value, oldState, reason);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private readonly transitionToConnectedState = () => {
|
|
171
|
+
// Defensive measure, we should always be in Connecting state when this is called.
|
|
172
|
+
const state = this.pimpl.connectionState;
|
|
173
|
+
assert(state === ConnectionState.Connected, 0x3e5 /* invariant broken */);
|
|
174
|
+
assert(this._connectionState === ConnectionState.CatchingUp, 0x3e6 /* invariant broken */);
|
|
175
|
+
this._connectionState = ConnectionState.Connected;
|
|
176
|
+
this.inputs.connectionStateChanged(ConnectionState.Connected, ConnectionState.CatchingUp, "caught up");
|
|
177
|
+
};
|
|
178
|
+
}
|
|
33
179
|
|
|
34
180
|
/**
|
|
35
181
|
* In the lifetime of a container, the connection will likely disconnect and reconnect periodically.
|
|
@@ -43,24 +189,29 @@ const JoinOpTimeoutMs = 45000;
|
|
|
43
189
|
*
|
|
44
190
|
* The job of this class is to encapsulate the transition period during reconnect, which is identified by
|
|
45
191
|
* ConnectionState.CatchingUp. Specifically, before moving to Connected state with the new clientId, it ensures that:
|
|
46
|
-
* (A) We process the Leave op for the previous clientId. This allows us to properly handle any acks from in-flight ops
|
|
47
|
-
* that got sequenced with the old clientId (we'll recognize them as local ops). After the Leave op, any other
|
|
48
|
-
* pending ops can safely be submitted with the new clientId without fear of duplication in the sequenced op stream.
|
|
49
|
-
* (B) We process the Join op for the new clientId (identified when the underlying connection was first established),
|
|
50
|
-
* indicating the service is ready to sequence ops sent with the new clientId.
|
|
51
|
-
* (C) We process all ops known at the time the underlying connection was established (so we are "caught up")
|
|
52
192
|
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
193
|
+
* a. We process the Leave op for the previous clientId. This allows us to properly handle any acks from in-flight ops
|
|
194
|
+
* that got sequenced with the old clientId (we'll recognize them as local ops). After the Leave op, any other
|
|
195
|
+
* pending ops can safely be submitted with the new clientId without fear of duplication in the sequenced op stream.
|
|
196
|
+
*
|
|
197
|
+
* b. We process the Join op for the new clientId (identified when the underlying connection was first established),
|
|
198
|
+
* indicating the service is ready to sequence ops sent with the new clientId.
|
|
199
|
+
*
|
|
200
|
+
* c. We process all ops known at the time the underlying connection was established (so we are "caught up")
|
|
201
|
+
*
|
|
202
|
+
* For (a) we give up waiting after some time (same timeout as server uses), and go ahead and transition to Connected.
|
|
203
|
+
*
|
|
204
|
+
* For (b) we log telemetry if it takes too long, but still only transition to Connected when the Join op is processed
|
|
55
205
|
* and we are added to the Quorum.
|
|
56
|
-
*
|
|
206
|
+
*
|
|
207
|
+
* For (c) this is optional behavior, controlled by the parameters of receivedConnectEvent
|
|
57
208
|
*/
|
|
58
|
-
|
|
209
|
+
class ConnectionStateHandler implements IConnectionStateHandler {
|
|
59
210
|
private _connectionState = ConnectionState.Disconnected;
|
|
60
211
|
private _pendingClientId: string | undefined;
|
|
61
|
-
private catchUpMonitor: ICatchUpMonitor | undefined;
|
|
62
212
|
private readonly prevClientLeftTimer: Timer;
|
|
63
213
|
private readonly joinOpTimer: Timer;
|
|
214
|
+
private protocol?: IProtocolHandler;
|
|
64
215
|
|
|
65
216
|
private waitEvent: PerformanceEvent | undefined;
|
|
66
217
|
|
|
@@ -68,11 +219,7 @@ export class ConnectionStateHandler {
|
|
|
68
219
|
return this._connectionState;
|
|
69
220
|
}
|
|
70
221
|
|
|
71
|
-
|
|
72
|
-
return this.connectionState === ConnectionState.Connected;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
public get clientId(): string | undefined {
|
|
222
|
+
private get clientId(): string | undefined {
|
|
76
223
|
return this._clientId;
|
|
77
224
|
}
|
|
78
225
|
|
|
@@ -82,7 +229,6 @@ export class ConnectionStateHandler {
|
|
|
82
229
|
|
|
83
230
|
constructor(
|
|
84
231
|
private readonly handler: IConnectionStateHandlerInputs,
|
|
85
|
-
private readonly logger: ITelemetryLogger,
|
|
86
232
|
private _clientId?: string,
|
|
87
233
|
) {
|
|
88
234
|
this.prevClientLeftTimer = new Timer(
|
|
@@ -90,7 +236,7 @@ export class ConnectionStateHandler {
|
|
|
90
236
|
// the max time on server after which leave op is sent.
|
|
91
237
|
this.handler.maxClientLeaveWaitTime ?? 300000,
|
|
92
238
|
() => {
|
|
93
|
-
assert(
|
|
239
|
+
assert(this.connectionState !== ConnectionState.Connected,
|
|
94
240
|
0x2ac /* "Connected when timeout waiting for leave from previous session fired!" */);
|
|
95
241
|
this.applyForConnectedState("timeout");
|
|
96
242
|
},
|
|
@@ -107,11 +253,10 @@ export class ConnectionStateHandler {
|
|
|
107
253
|
if (this.connectionState !== ConnectionState.CatchingUp) {
|
|
108
254
|
return;
|
|
109
255
|
}
|
|
110
|
-
const quorumClients = this.handler.quorumClients();
|
|
111
256
|
const details = {
|
|
112
|
-
|
|
257
|
+
protocolInitialized: this.protocol !== undefined,
|
|
113
258
|
pendingClientId: this.pendingClientId,
|
|
114
|
-
|
|
259
|
+
clientJoined: this.hasMember(this.pendingClientId),
|
|
115
260
|
waitingForLeaveOp: this.waitingForLeaveOp,
|
|
116
261
|
};
|
|
117
262
|
this.handler.logConnectionIssue("NoJoinOp", details);
|
|
@@ -159,7 +304,7 @@ export class ConnectionStateHandler {
|
|
|
159
304
|
}
|
|
160
305
|
// Start the event in case we are waiting for leave or timeout.
|
|
161
306
|
if (this.waitingForLeaveOp) {
|
|
162
|
-
this.waitEvent = PerformanceEvent.start(this.logger, {
|
|
307
|
+
this.waitEvent = PerformanceEvent.start(this.handler.logger, {
|
|
163
308
|
eventName: "WaitBeforeClientLeave",
|
|
164
309
|
details: JSON.stringify({
|
|
165
310
|
waitOnClientId: this._clientId,
|
|
@@ -172,29 +317,23 @@ export class ConnectionStateHandler {
|
|
|
172
317
|
}
|
|
173
318
|
|
|
174
319
|
private applyForConnectedState(source: "removeMemberEvent" | "addMemberEvent" | "timeout" | "containerSaved") {
|
|
175
|
-
|
|
176
|
-
assert(quorumClients !== undefined, 0x236 /* "In all cases it should be already installed" */);
|
|
320
|
+
assert(this.protocol !== undefined, 0x236 /* "In all cases it should be already installed" */);
|
|
177
321
|
|
|
178
|
-
assert(this.waitingForLeaveOp
|
|
179
|
-
(this.clientId !== undefined && quorumClients.getMember(this.clientId) !== undefined),
|
|
322
|
+
assert(!this.waitingForLeaveOp || this.hasMember(this.clientId),
|
|
180
323
|
0x2e2 /* "Must only wait for leave message when clientId in quorum" */);
|
|
181
324
|
|
|
182
325
|
// Move to connected state only if we are in Connecting state, we have seen our join op
|
|
183
326
|
// and there is no timer running which means we are not waiting for previous client to leave
|
|
184
327
|
// or timeout has occurred while doing so.
|
|
185
328
|
if (this.pendingClientId !== this.clientId
|
|
186
|
-
&& this.pendingClientId
|
|
187
|
-
&& quorumClients.getMember(this.pendingClientId) !== undefined
|
|
329
|
+
&& this.hasMember(this.pendingClientId)
|
|
188
330
|
&& !this.waitingForLeaveOp
|
|
189
331
|
) {
|
|
190
332
|
this.waitEvent?.end({ source });
|
|
191
|
-
|
|
192
|
-
assert(this.catchUpMonitor !== undefined,
|
|
193
|
-
0x37d /* catchUpMonitor should always be set if pendingClientId is set */);
|
|
194
|
-
this.catchUpMonitor.on("caughtUp", this.transitionToConnectedState);
|
|
333
|
+
this.setConnectionState(ConnectionState.Connected);
|
|
195
334
|
} else {
|
|
196
335
|
// Adding this event temporarily so that we can get help debugging if something goes wrong.
|
|
197
|
-
this.logger.sendTelemetryEvent({
|
|
336
|
+
this.handler.logger.sendTelemetryEvent({
|
|
198
337
|
eventName: "connectedStateRejected",
|
|
199
338
|
category: source === "timeout" ? "error" : "generic",
|
|
200
339
|
details: JSON.stringify({
|
|
@@ -202,7 +341,7 @@ export class ConnectionStateHandler {
|
|
|
202
341
|
pendingClientId: this.pendingClientId,
|
|
203
342
|
clientId: this.clientId,
|
|
204
343
|
waitingForLeaveOp: this.waitingForLeaveOp,
|
|
205
|
-
|
|
344
|
+
clientJoined: this.hasMember(this.pendingClientId),
|
|
206
345
|
}),
|
|
207
346
|
});
|
|
208
347
|
}
|
|
@@ -220,18 +359,6 @@ export class ConnectionStateHandler {
|
|
|
220
359
|
this.setConnectionState(ConnectionState.Disconnected, reason);
|
|
221
360
|
}
|
|
222
361
|
|
|
223
|
-
private readonly transitionToConnectedState = () => {
|
|
224
|
-
// Defensive measure, we should always be in CatchingUp state when this is called.
|
|
225
|
-
if (this._connectionState === ConnectionState.CatchingUp) {
|
|
226
|
-
this.setConnectionState(ConnectionState.Connected);
|
|
227
|
-
} else {
|
|
228
|
-
this.logger.sendTelemetryEvent({
|
|
229
|
-
eventName: "cannotTransitionToConnectedState",
|
|
230
|
-
connectionState: ConnectionState[this._connectionState],
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
};
|
|
234
|
-
|
|
235
362
|
/**
|
|
236
363
|
* The "connect" event indicates the connection to the Relay Service is live.
|
|
237
364
|
* However, some additional conditions must be met before we can fully transition to
|
|
@@ -244,23 +371,18 @@ export class ConnectionStateHandler {
|
|
|
244
371
|
public receivedConnectEvent(
|
|
245
372
|
connectionMode: ConnectionMode,
|
|
246
373
|
details: IConnectionDetails,
|
|
247
|
-
deltaManager?: IDeltaManager<any, any>,
|
|
248
374
|
) {
|
|
249
375
|
const oldState = this._connectionState;
|
|
250
376
|
this._connectionState = ConnectionState.CatchingUp;
|
|
251
377
|
|
|
252
378
|
const writeConnection = connectionMode === "write";
|
|
253
|
-
assert(!this.handler.shouldClientJoinWrite() || writeConnection,
|
|
254
|
-
0x30a /* shouldClientJoinWrite should imply this is a writeConnection */);
|
|
255
|
-
assert(!this.waitingForLeaveOp || writeConnection,
|
|
256
|
-
0x2a6 /* "waitingForLeaveOp should imply writeConnection (we need to be ready to flush pending ops)" */);
|
|
257
379
|
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
|
|
380
|
+
// The following checks are wrong. They are only valid if user has write access to a file.
|
|
381
|
+
// If user lost such access mid-session, user will not be able to get "write" connection.
|
|
382
|
+
// assert(!this.handler.shouldClientJoinWrite() || writeConnection,
|
|
383
|
+
// 0x30a /* shouldClientJoinWrite should imply this is a writeConnection */);
|
|
384
|
+
// assert(!this.waitingForLeaveOp || writeConnection,
|
|
385
|
+
// 0x2a6 /* "waitingForLeaveOp should imply writeConnection (we need to be ready to flush pending ops)" */);
|
|
264
386
|
|
|
265
387
|
// Stash the clientID to detect when transitioning from connecting (socket.io channel open) to connected
|
|
266
388
|
// (have received the join message for the client ID)
|
|
@@ -270,20 +392,15 @@ export class ConnectionStateHandler {
|
|
|
270
392
|
// we know there can no longer be outstanding ops that we sent with the previous client id.
|
|
271
393
|
this._pendingClientId = details.clientId;
|
|
272
394
|
|
|
273
|
-
// We may want to catch up to known ops as of now before transitioning to Connected state
|
|
274
|
-
this.catchUpMonitor = deltaManager !== undefined
|
|
275
|
-
? new CatchUpMonitor(deltaManager)
|
|
276
|
-
: new ImmediateCatchUpMonitor();
|
|
277
|
-
|
|
278
395
|
// IMPORTANT: Report telemetry after we set _pendingClientId, but before transitioning to Connected state
|
|
279
|
-
this.handler.
|
|
396
|
+
this.handler.connectionStateChanged(ConnectionState.CatchingUp, oldState);
|
|
280
397
|
|
|
281
398
|
// For write connections, this pending clientId could be in the quorum already (i.e. join op already processed).
|
|
282
399
|
// We are fetching ops from storage in parallel to connecting to Relay Service,
|
|
283
400
|
// and given async processes, it's possible that we have already processed our own join message before
|
|
284
401
|
// connection was fully established.
|
|
285
|
-
// If
|
|
286
|
-
const waitingForJoinOp = writeConnection &&
|
|
402
|
+
// If protocol is not initialized yet, we expect it will process the join op after it's initialized.
|
|
403
|
+
const waitingForJoinOp = writeConnection && !this.hasMember(this._pendingClientId);
|
|
287
404
|
|
|
288
405
|
if (waitingForJoinOp) {
|
|
289
406
|
// Previous client left, and we are waiting for our own join op. When it is processed we'll join the quorum
|
|
@@ -291,39 +408,27 @@ export class ConnectionStateHandler {
|
|
|
291
408
|
this.startJoinOpTimer();
|
|
292
409
|
} else if (!this.waitingForLeaveOp) {
|
|
293
410
|
// We're not waiting for Join or Leave op (if read-only connection those don't even apply),
|
|
294
|
-
//
|
|
295
|
-
|
|
411
|
+
// go ahead and declare the state to be Connected!
|
|
412
|
+
// If we are waiting for Leave op still, do nothing for now, we will transition to Connected later.
|
|
413
|
+
this.setConnectionState(ConnectionState.Connected);
|
|
296
414
|
}
|
|
297
415
|
// else - We are waiting for Leave op still, do nothing for now, we will transition to Connected later
|
|
298
416
|
}
|
|
299
417
|
|
|
300
|
-
/** Clear all the state used during the Connecting phase (set in receivedConnectEvent) */
|
|
301
|
-
private clearPendingConnectionState() {
|
|
302
|
-
this._pendingClientId = undefined;
|
|
303
|
-
|
|
304
|
-
this.catchUpMonitor?.dispose();
|
|
305
|
-
this.catchUpMonitor = undefined;
|
|
306
|
-
|
|
307
|
-
if (this.joinOpTimer.hasTimer) {
|
|
308
|
-
this.stopJoinOpTimer();
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
418
|
private setConnectionState(value: ConnectionState.Disconnected, reason: string): void;
|
|
313
419
|
private setConnectionState(value: ConnectionState.Connected): void;
|
|
314
|
-
private setConnectionState(value: ConnectionState, reason?: string): void {
|
|
420
|
+
private setConnectionState(value: ConnectionState.Disconnected | ConnectionState.Connected, reason?: string): void {
|
|
315
421
|
if (this.connectionState === value) {
|
|
316
422
|
// Already in the desired state - exit early
|
|
317
|
-
this.logger.sendErrorEvent({ eventName: "setConnectionStateSame", value });
|
|
423
|
+
this.handler.logger.sendErrorEvent({ eventName: "setConnectionStateSame", value });
|
|
318
424
|
return;
|
|
319
425
|
}
|
|
320
426
|
|
|
321
427
|
const oldState = this._connectionState;
|
|
322
428
|
this._connectionState = value;
|
|
323
|
-
const quorumClients = this.handler.quorumClients();
|
|
324
429
|
let client: ILocalSequencedClient | undefined;
|
|
325
430
|
if (this._clientId !== undefined) {
|
|
326
|
-
client =
|
|
431
|
+
client = this.protocol?.quorum?.getMember(this._clientId);
|
|
327
432
|
}
|
|
328
433
|
if (value === ConnectionState.Connected) {
|
|
329
434
|
assert(oldState === ConnectionState.CatchingUp,
|
|
@@ -335,24 +440,27 @@ export class ConnectionStateHandler {
|
|
|
335
440
|
this._clientId = this.pendingClientId;
|
|
336
441
|
} else if (value === ConnectionState.Disconnected) {
|
|
337
442
|
// Clear pending state immediately to prepare for reconnect
|
|
338
|
-
this.
|
|
443
|
+
this._pendingClientId = undefined;
|
|
339
444
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
445
|
+
if (this.joinOpTimer.hasTimer) {
|
|
446
|
+
this.stopJoinOpTimer();
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Only wait for "leave" message if the connected client exists in the quorum and had some non-acked ops
|
|
450
|
+
// Also check if the timer is not already running as
|
|
344
451
|
// we could receive "Disconnected" event multiple times without getting connected and in that case we
|
|
345
452
|
// don't want to reset the timer as we still want to wait on original client which started this timer.
|
|
346
453
|
if (client !== undefined
|
|
347
454
|
&& this.handler.shouldClientJoinWrite()
|
|
348
|
-
&& this.prevClientLeftTimer.hasTimer
|
|
455
|
+
&& !this.waitingForLeaveOp // same as !this.prevClientLeftTimer.hasTimer
|
|
349
456
|
) {
|
|
350
457
|
this.prevClientLeftTimer.restart();
|
|
351
458
|
} else {
|
|
352
459
|
// Adding this event temporarily so that we can get help debugging if something goes wrong.
|
|
353
|
-
this.logger.sendTelemetryEvent({
|
|
460
|
+
this.handler.logger.sendTelemetryEvent({
|
|
354
461
|
eventName: "noWaitOnDisconnected",
|
|
355
462
|
details: JSON.stringify({
|
|
463
|
+
clientId: this._clientId,
|
|
356
464
|
inQuorum: client !== undefined,
|
|
357
465
|
waitingForLeaveOp: this.waitingForLeaveOp,
|
|
358
466
|
hadOutstandingOps: this.handler.shouldClientJoinWrite(),
|
|
@@ -362,24 +470,41 @@ export class ConnectionStateHandler {
|
|
|
362
470
|
}
|
|
363
471
|
|
|
364
472
|
// Report transition before we propagate event across layers
|
|
365
|
-
this.handler.
|
|
473
|
+
this.handler.connectionStateChanged(this._connectionState, oldState, reason);
|
|
474
|
+
}
|
|
366
475
|
|
|
367
|
-
|
|
368
|
-
|
|
476
|
+
// Helper method to switch between quorum and audience.
|
|
477
|
+
// Old design was checking only quorum for "write" clients.
|
|
478
|
+
// Latest change checks audience for all types of connections.
|
|
479
|
+
protected get membership() {
|
|
480
|
+
return this.protocol?.quorum;
|
|
369
481
|
}
|
|
370
482
|
|
|
371
483
|
public initProtocol(protocol: IProtocolHandler) {
|
|
372
|
-
protocol
|
|
484
|
+
this.protocol = protocol;
|
|
485
|
+
|
|
486
|
+
this.membership?.on("addMember", (clientId) => {
|
|
373
487
|
this.receivedAddMemberEvent(clientId);
|
|
374
488
|
});
|
|
375
489
|
|
|
376
|
-
|
|
490
|
+
this.membership?.on("removeMember", (clientId) => {
|
|
377
491
|
this.receivedRemoveMemberEvent(clientId);
|
|
378
492
|
});
|
|
379
493
|
|
|
494
|
+
// Very unlikely race condition, but theoretically can happen - our new connection is already
|
|
495
|
+
// summarized and we are loading from such summary.
|
|
496
|
+
if (this.hasMember(this.pendingClientId)) {
|
|
497
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
498
|
+
this.receivedAddMemberEvent(this.pendingClientId!);
|
|
499
|
+
}
|
|
500
|
+
|
|
380
501
|
// if we have a clientId from a previous container we need to wait for its leave message
|
|
381
|
-
if (this.clientId !== undefined &&
|
|
502
|
+
if (this.clientId !== undefined && this.hasMember(this.clientId)) {
|
|
382
503
|
this.prevClientLeftTimer.restart();
|
|
383
504
|
}
|
|
384
505
|
}
|
|
506
|
+
|
|
507
|
+
protected hasMember(clientId?: string) {
|
|
508
|
+
return this.membership?.getMember(clientId ?? "") !== undefined;
|
|
509
|
+
}
|
|
385
510
|
}
|