@fluidframework/container-loader 1.3.0 → 2.0.0-dev.1.4.5.105745
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +8 -21
- package/.mocharc.js +12 -0
- package/dist/audience.d.ts +2 -2
- package/dist/audience.d.ts.map +1 -1
- 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 +13 -18
- 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 +30 -17
- package/dist/container.d.ts.map +1 -1
- package/dist/container.js +173 -165
- 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 +33 -6
- package/dist/deltaManager.js.map +1 -1
- package/dist/deltaQueue.js +3 -3
- 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 +22 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +53 -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 -2
- package/lib/audience.d.ts.map +1 -1
- 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 +14 -21
- 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 +30 -17
- package/lib/container.d.ts.map +1 -1
- package/lib/container.js +176 -168
- 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 +35 -8
- package/lib/deltaManager.js.map +1 -1
- package/lib/deltaQueue.js +3 -3
- 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 +22 -0
- package/lib/protocol.d.ts.map +1 -0
- package/lib/protocol.js +49 -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 +26 -20
- package/src/audience.ts +2 -2
- package/src/catchUpMonitor.ts +59 -0
- package/src/collabWindowTracker.ts +15 -6
- package/src/connectionManager.ts +23 -27
- package/src/connectionState.ts +0 -6
- package/src/connectionStateHandler.ts +235 -70
- package/src/container.ts +223 -209
- package/src/containerContext.ts +22 -8
- package/src/containerStorageAdapter.ts +71 -16
- package/src/contracts.ts +7 -7
- package/src/deltaManager.ts +42 -11
- package/src/deltaQueue.ts +3 -3
- package/src/index.ts +4 -0
- package/src/loader.ts +14 -3
- package/src/packageVersion.ts +1 -1
- package/src/protocol.ts +97 -0
- package/src/retriableDocumentStorageService.ts +8 -2
|
@@ -5,18 +5,21 @@
|
|
|
5
5
|
|
|
6
6
|
import { ITelemetryLogger, ITelemetryProperties } from "@fluidframework/common-definitions";
|
|
7
7
|
import { assert, Timer } from "@fluidframework/common-utils";
|
|
8
|
-
import { IConnectionDetails } from "@fluidframework/container-definitions";
|
|
9
|
-
import { ILocalSequencedClient
|
|
10
|
-
import { ConnectionMode
|
|
11
|
-
import { PerformanceEvent } from "@fluidframework/telemetry-utils";
|
|
8
|
+
import { IConnectionDetails, IDeltaManager } from "@fluidframework/container-definitions";
|
|
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 } from "./catchUpMonitor";
|
|
14
|
+
import { IProtocolHandler } from "./protocol";
|
|
15
|
+
|
|
16
|
+
const JoinOpTimeoutMs = 45000;
|
|
13
17
|
|
|
14
18
|
/** Constructor parameter type for passing in dependencies needed by the ConnectionStateHandler */
|
|
15
19
|
export interface IConnectionStateHandlerInputs {
|
|
16
|
-
|
|
17
|
-
quorumClients: () => IQuorumClients | undefined;
|
|
20
|
+
logger: ITelemetryLogger;
|
|
18
21
|
/** Log to telemetry any change in state, included to Connecting */
|
|
19
|
-
|
|
22
|
+
connectionStateChanged:
|
|
20
23
|
(value: ConnectionState, oldState: ConnectionState, reason?: string | undefined) => void;
|
|
21
24
|
/** Whether to expect the client to join in write mode on next connection */
|
|
22
25
|
shouldClientJoinWrite: () => boolean;
|
|
@@ -24,11 +27,155 @@ export interface IConnectionStateHandlerInputs {
|
|
|
24
27
|
maxClientLeaveWaitTime: number | undefined;
|
|
25
28
|
/** Log an issue encountered while in the Connecting state. details will be logged as a JSON string */
|
|
26
29
|
logConnectionIssue: (eventName: string, details?: ITelemetryProperties) => void;
|
|
27
|
-
/** Callback whenever the ConnectionState changes between Disconnected and Connected */
|
|
28
|
-
connectionStateChanged: () => void;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
|
|
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
|
+
}
|
|
32
179
|
|
|
33
180
|
/**
|
|
34
181
|
* In the lifetime of a container, the connection will likely disconnect and reconnect periodically.
|
|
@@ -36,27 +183,35 @@ const JoinOpTimeoutMs = 45000;
|
|
|
36
183
|
* sequenced or blocked by the server before emitting the new "connected" event and allowing runtime to resubmit ops.
|
|
37
184
|
*
|
|
38
185
|
* Each connection is assigned a clientId by the service, and the connection is book-ended by a Join and a Leave op
|
|
39
|
-
* generated by the service. Due to the distributed nature of the
|
|
186
|
+
* generated by the service. Due to the distributed nature of the Relay Service, in the case of reconnect we cannot
|
|
40
187
|
* make any assumptions about ordering of operations between the old and new connections - i.e. new Join op could
|
|
41
188
|
* be sequenced before old Leave op (and some acks from pending ops that were in flight when we disconnected).
|
|
42
189
|
*
|
|
43
190
|
* The job of this class is to encapsulate the transition period during reconnect, which is identified by
|
|
44
191
|
* ConnectionState.CatchingUp. Specifically, before moving to Connected state with the new clientId, it ensures that:
|
|
45
|
-
* (A) We process the Leave op for the previous clientId. This allows us to properly handle any acks from in-flight ops
|
|
46
|
-
* that got sequenced with the old clientId (we'll recognize them as local ops). After the Leave op, any other
|
|
47
|
-
* pending ops can safely be submitted with the new clientId without fear of duplication in the sequenced op stream.
|
|
48
|
-
* (B) We process the Join op for the new clientId (identified when the underlying connection was first established),
|
|
49
|
-
* indicating the service is ready to sequence ops sent with the new clientId.
|
|
50
192
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
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
|
|
53
205
|
* and we are added to the Quorum.
|
|
206
|
+
*
|
|
207
|
+
* For (c) this is optional behavior, controlled by the parameters of receivedConnectEvent
|
|
54
208
|
*/
|
|
55
|
-
|
|
209
|
+
class ConnectionStateHandler implements IConnectionStateHandler {
|
|
56
210
|
private _connectionState = ConnectionState.Disconnected;
|
|
57
211
|
private _pendingClientId: string | undefined;
|
|
58
212
|
private readonly prevClientLeftTimer: Timer;
|
|
59
213
|
private readonly joinOpTimer: Timer;
|
|
214
|
+
private protocol?: IProtocolHandler;
|
|
60
215
|
|
|
61
216
|
private waitEvent: PerformanceEvent | undefined;
|
|
62
217
|
|
|
@@ -64,11 +219,7 @@ export class ConnectionStateHandler {
|
|
|
64
219
|
return this._connectionState;
|
|
65
220
|
}
|
|
66
221
|
|
|
67
|
-
|
|
68
|
-
return this.connectionState === ConnectionState.Connected;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
public get clientId(): string | undefined {
|
|
222
|
+
private get clientId(): string | undefined {
|
|
72
223
|
return this._clientId;
|
|
73
224
|
}
|
|
74
225
|
|
|
@@ -78,7 +229,6 @@ export class ConnectionStateHandler {
|
|
|
78
229
|
|
|
79
230
|
constructor(
|
|
80
231
|
private readonly handler: IConnectionStateHandlerInputs,
|
|
81
|
-
private readonly logger: ITelemetryLogger,
|
|
82
232
|
private _clientId?: string,
|
|
83
233
|
) {
|
|
84
234
|
this.prevClientLeftTimer = new Timer(
|
|
@@ -86,7 +236,7 @@ export class ConnectionStateHandler {
|
|
|
86
236
|
// the max time on server after which leave op is sent.
|
|
87
237
|
this.handler.maxClientLeaveWaitTime ?? 300000,
|
|
88
238
|
() => {
|
|
89
|
-
assert(
|
|
239
|
+
assert(this.connectionState !== ConnectionState.Connected,
|
|
90
240
|
0x2ac /* "Connected when timeout waiting for leave from previous session fired!" */);
|
|
91
241
|
this.applyForConnectedState("timeout");
|
|
92
242
|
},
|
|
@@ -103,11 +253,10 @@ export class ConnectionStateHandler {
|
|
|
103
253
|
if (this.connectionState !== ConnectionState.CatchingUp) {
|
|
104
254
|
return;
|
|
105
255
|
}
|
|
106
|
-
const quorumClients = this.handler.quorumClients();
|
|
107
256
|
const details = {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
257
|
+
protocolInitialized: this.protocol !== undefined,
|
|
258
|
+
pendingClientId: this.pendingClientId,
|
|
259
|
+
clientJoined: this.hasMember(this.pendingClientId),
|
|
111
260
|
waitingForLeaveOp: this.waitingForLeaveOp,
|
|
112
261
|
};
|
|
113
262
|
this.handler.logConnectionIssue("NoJoinOp", details);
|
|
@@ -155,7 +304,7 @@ export class ConnectionStateHandler {
|
|
|
155
304
|
}
|
|
156
305
|
// Start the event in case we are waiting for leave or timeout.
|
|
157
306
|
if (this.waitingForLeaveOp) {
|
|
158
|
-
this.waitEvent = PerformanceEvent.start(this.logger, {
|
|
307
|
+
this.waitEvent = PerformanceEvent.start(this.handler.logger, {
|
|
159
308
|
eventName: "WaitBeforeClientLeave",
|
|
160
309
|
details: JSON.stringify({
|
|
161
310
|
waitOnClientId: this._clientId,
|
|
@@ -168,26 +317,23 @@ export class ConnectionStateHandler {
|
|
|
168
317
|
}
|
|
169
318
|
|
|
170
319
|
private applyForConnectedState(source: "removeMemberEvent" | "addMemberEvent" | "timeout" | "containerSaved") {
|
|
171
|
-
|
|
172
|
-
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" */);
|
|
173
321
|
|
|
174
|
-
assert(this.waitingForLeaveOp
|
|
175
|
-
(this.clientId !== undefined && quorumClients.getMember(this.clientId) !== undefined),
|
|
322
|
+
assert(!this.waitingForLeaveOp || this.hasMember(this.clientId),
|
|
176
323
|
0x2e2 /* "Must only wait for leave message when clientId in quorum" */);
|
|
177
324
|
|
|
178
325
|
// Move to connected state only if we are in Connecting state, we have seen our join op
|
|
179
326
|
// and there is no timer running which means we are not waiting for previous client to leave
|
|
180
327
|
// or timeout has occurred while doing so.
|
|
181
328
|
if (this.pendingClientId !== this.clientId
|
|
182
|
-
&& this.pendingClientId
|
|
183
|
-
&& quorumClients.getMember(this.pendingClientId) !== undefined
|
|
329
|
+
&& this.hasMember(this.pendingClientId)
|
|
184
330
|
&& !this.waitingForLeaveOp
|
|
185
331
|
) {
|
|
186
332
|
this.waitEvent?.end({ source });
|
|
187
333
|
this.setConnectionState(ConnectionState.Connected);
|
|
188
334
|
} else {
|
|
189
335
|
// Adding this event temporarily so that we can get help debugging if something goes wrong.
|
|
190
|
-
this.logger.sendTelemetryEvent({
|
|
336
|
+
this.handler.logger.sendTelemetryEvent({
|
|
191
337
|
eventName: "connectedStateRejected",
|
|
192
338
|
category: source === "timeout" ? "error" : "generic",
|
|
193
339
|
details: JSON.stringify({
|
|
@@ -195,7 +341,7 @@ export class ConnectionStateHandler {
|
|
|
195
341
|
pendingClientId: this.pendingClientId,
|
|
196
342
|
clientId: this.clientId,
|
|
197
343
|
waitingForLeaveOp: this.waitingForLeaveOp,
|
|
198
|
-
|
|
344
|
+
clientJoined: this.hasMember(this.pendingClientId),
|
|
199
345
|
}),
|
|
200
346
|
});
|
|
201
347
|
}
|
|
@@ -210,9 +356,6 @@ export class ConnectionStateHandler {
|
|
|
210
356
|
}
|
|
211
357
|
|
|
212
358
|
public receivedDisconnectEvent(reason: string) {
|
|
213
|
-
if (this.joinOpTimer.hasTimer) {
|
|
214
|
-
this.stopJoinOpTimer();
|
|
215
|
-
}
|
|
216
359
|
this.setConnectionState(ConnectionState.Disconnected, reason);
|
|
217
360
|
}
|
|
218
361
|
|
|
@@ -221,7 +364,9 @@ export class ConnectionStateHandler {
|
|
|
221
364
|
* However, some additional conditions must be met before we can fully transition to
|
|
222
365
|
* "Connected" state. This function handles that interim period, known as "Connecting" state.
|
|
223
366
|
* @param connectionMode - Read or Write connection
|
|
224
|
-
* @param details - Connection details returned from the
|
|
367
|
+
* @param details - Connection details returned from the Relay Service
|
|
368
|
+
* @param deltaManager - DeltaManager to be used for delaying Connected transition until caught up.
|
|
369
|
+
* If it's undefined, then don't delay and transition to Connected as soon as Leave/Join op are accounted for
|
|
225
370
|
*/
|
|
226
371
|
public receivedConnectEvent(
|
|
227
372
|
connectionMode: ConnectionMode,
|
|
@@ -231,14 +376,13 @@ export class ConnectionStateHandler {
|
|
|
231
376
|
this._connectionState = ConnectionState.CatchingUp;
|
|
232
377
|
|
|
233
378
|
const writeConnection = connectionMode === "write";
|
|
234
|
-
assert(writeConnection || !this.handler.shouldClientJoinWrite(),
|
|
235
|
-
0x30a /* shouldClientJoinWrite should imply this is a writeConnection */);
|
|
236
|
-
assert(writeConnection || !this.waitingForLeaveOp,
|
|
237
|
-
0x2a6 /* "waitingForLeaveOp should imply writeConnection (we need to be ready to flush pending ops)" */);
|
|
238
379
|
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
|
|
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)" */);
|
|
242
386
|
|
|
243
387
|
// Stash the clientID to detect when transitioning from connecting (socket.io channel open) to connected
|
|
244
388
|
// (have received the join message for the client ID)
|
|
@@ -249,14 +393,14 @@ export class ConnectionStateHandler {
|
|
|
249
393
|
this._pendingClientId = details.clientId;
|
|
250
394
|
|
|
251
395
|
// IMPORTANT: Report telemetry after we set _pendingClientId, but before transitioning to Connected state
|
|
252
|
-
this.handler.
|
|
396
|
+
this.handler.connectionStateChanged(ConnectionState.CatchingUp, oldState);
|
|
253
397
|
|
|
254
398
|
// For write connections, this pending clientId could be in the quorum already (i.e. join op already processed).
|
|
255
399
|
// We are fetching ops from storage in parallel to connecting to Relay Service,
|
|
256
400
|
// and given async processes, it's possible that we have already processed our own join message before
|
|
257
401
|
// connection was fully established.
|
|
258
|
-
// If
|
|
259
|
-
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);
|
|
260
404
|
|
|
261
405
|
if (waitingForJoinOp) {
|
|
262
406
|
// Previous client left, and we are waiting for our own join op. When it is processed we'll join the quorum
|
|
@@ -268,23 +412,23 @@ export class ConnectionStateHandler {
|
|
|
268
412
|
// If we are waiting for Leave op still, do nothing for now, we will transition to Connected later.
|
|
269
413
|
this.setConnectionState(ConnectionState.Connected);
|
|
270
414
|
}
|
|
415
|
+
// else - We are waiting for Leave op still, do nothing for now, we will transition to Connected later
|
|
271
416
|
}
|
|
272
417
|
|
|
273
418
|
private setConnectionState(value: ConnectionState.Disconnected, reason: string): void;
|
|
274
419
|
private setConnectionState(value: ConnectionState.Connected): void;
|
|
275
|
-
private setConnectionState(value: ConnectionState, reason?: string): void {
|
|
420
|
+
private setConnectionState(value: ConnectionState.Disconnected | ConnectionState.Connected, reason?: string): void {
|
|
276
421
|
if (this.connectionState === value) {
|
|
277
422
|
// Already in the desired state - exit early
|
|
278
|
-
this.logger.sendErrorEvent({ eventName: "setConnectionStateSame", value });
|
|
423
|
+
this.handler.logger.sendErrorEvent({ eventName: "setConnectionStateSame", value });
|
|
279
424
|
return;
|
|
280
425
|
}
|
|
281
426
|
|
|
282
427
|
const oldState = this._connectionState;
|
|
283
428
|
this._connectionState = value;
|
|
284
|
-
const quorumClients = this.handler.quorumClients();
|
|
285
429
|
let client: ILocalSequencedClient | undefined;
|
|
286
430
|
if (this._clientId !== undefined) {
|
|
287
|
-
client =
|
|
431
|
+
client = this.protocol?.quorum?.getMember(this._clientId);
|
|
288
432
|
}
|
|
289
433
|
if (value === ConnectionState.Connected) {
|
|
290
434
|
assert(oldState === ConnectionState.CatchingUp,
|
|
@@ -295,24 +439,28 @@ export class ConnectionStateHandler {
|
|
|
295
439
|
}
|
|
296
440
|
this._clientId = this.pendingClientId;
|
|
297
441
|
} else if (value === ConnectionState.Disconnected) {
|
|
298
|
-
//
|
|
442
|
+
// Clear pending state immediately to prepare for reconnect
|
|
299
443
|
this._pendingClientId = undefined;
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
444
|
+
|
|
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
|
|
304
451
|
// we could receive "Disconnected" event multiple times without getting connected and in that case we
|
|
305
452
|
// don't want to reset the timer as we still want to wait on original client which started this timer.
|
|
306
453
|
if (client !== undefined
|
|
307
454
|
&& this.handler.shouldClientJoinWrite()
|
|
308
|
-
&& this.prevClientLeftTimer.hasTimer
|
|
455
|
+
&& !this.waitingForLeaveOp // same as !this.prevClientLeftTimer.hasTimer
|
|
309
456
|
) {
|
|
310
457
|
this.prevClientLeftTimer.restart();
|
|
311
458
|
} else {
|
|
312
459
|
// Adding this event temporarily so that we can get help debugging if something goes wrong.
|
|
313
|
-
this.logger.sendTelemetryEvent({
|
|
460
|
+
this.handler.logger.sendTelemetryEvent({
|
|
314
461
|
eventName: "noWaitOnDisconnected",
|
|
315
462
|
details: JSON.stringify({
|
|
463
|
+
clientId: this._clientId,
|
|
316
464
|
inQuorum: client !== undefined,
|
|
317
465
|
waitingForLeaveOp: this.waitingForLeaveOp,
|
|
318
466
|
hadOutstandingOps: this.handler.shouldClientJoinWrite(),
|
|
@@ -322,24 +470,41 @@ export class ConnectionStateHandler {
|
|
|
322
470
|
}
|
|
323
471
|
|
|
324
472
|
// Report transition before we propagate event across layers
|
|
325
|
-
this.handler.
|
|
473
|
+
this.handler.connectionStateChanged(this._connectionState, oldState, reason);
|
|
474
|
+
}
|
|
326
475
|
|
|
327
|
-
|
|
328
|
-
|
|
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;
|
|
329
481
|
}
|
|
330
482
|
|
|
331
483
|
public initProtocol(protocol: IProtocolHandler) {
|
|
332
|
-
protocol
|
|
484
|
+
this.protocol = protocol;
|
|
485
|
+
|
|
486
|
+
this.membership?.on("addMember", (clientId) => {
|
|
333
487
|
this.receivedAddMemberEvent(clientId);
|
|
334
488
|
});
|
|
335
489
|
|
|
336
|
-
|
|
490
|
+
this.membership?.on("removeMember", (clientId) => {
|
|
337
491
|
this.receivedRemoveMemberEvent(clientId);
|
|
338
492
|
});
|
|
339
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
|
+
|
|
340
501
|
// if we have a clientId from a previous container we need to wait for its leave message
|
|
341
|
-
if (this.clientId !== undefined &&
|
|
502
|
+
if (this.clientId !== undefined && this.hasMember(this.clientId)) {
|
|
342
503
|
this.prevClientLeftTimer.restart();
|
|
343
504
|
}
|
|
344
505
|
}
|
|
506
|
+
|
|
507
|
+
protected hasMember(clientId?: string) {
|
|
508
|
+
return this.membership?.getMember(clientId ?? "") !== undefined;
|
|
509
|
+
}
|
|
345
510
|
}
|