@fluidframework/container-loader 2.0.0-internal.2.1.1 → 2.0.0-internal.2.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/.eslintrc.js +1 -1
- package/README.md +21 -11
- package/dist/audience.d.ts.map +1 -1
- package/dist/audience.js +6 -1
- package/dist/audience.js.map +1 -1
- package/dist/collabWindowTracker.js +5 -4
- package/dist/collabWindowTracker.js.map +1 -1
- package/dist/connectionManager.d.ts.map +1 -1
- package/dist/connectionManager.js +13 -2
- package/dist/connectionManager.js.map +1 -1
- package/dist/connectionStateHandler.d.ts +20 -11
- package/dist/connectionStateHandler.d.ts.map +1 -1
- package/dist/connectionStateHandler.js +65 -36
- package/dist/connectionStateHandler.js.map +1 -1
- package/dist/container.d.ts +8 -0
- package/dist/container.d.ts.map +1 -1
- package/dist/container.js +23 -7
- package/dist/container.js.map +1 -1
- package/dist/containerContext.d.ts.map +1 -1
- package/dist/containerContext.js +5 -1
- package/dist/containerContext.js.map +1 -1
- package/dist/contracts.d.ts +8 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js.map +1 -1
- package/dist/deltaManager.d.ts +1 -1
- package/dist/deltaManager.d.ts.map +1 -1
- package/dist/deltaManager.js +4 -3
- package/dist/deltaManager.js.map +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/lib/audience.d.ts.map +1 -1
- package/lib/audience.js +6 -1
- package/lib/audience.js.map +1 -1
- package/lib/collabWindowTracker.js +5 -4
- package/lib/collabWindowTracker.js.map +1 -1
- package/lib/connectionManager.d.ts.map +1 -1
- package/lib/connectionManager.js +13 -2
- package/lib/connectionManager.js.map +1 -1
- package/lib/connectionStateHandler.d.ts +20 -11
- package/lib/connectionStateHandler.d.ts.map +1 -1
- package/lib/connectionStateHandler.js +65 -36
- package/lib/connectionStateHandler.js.map +1 -1
- package/lib/container.d.ts +8 -0
- package/lib/container.d.ts.map +1 -1
- package/lib/container.js +22 -7
- package/lib/container.js.map +1 -1
- package/lib/containerContext.d.ts.map +1 -1
- package/lib/containerContext.js +5 -1
- package/lib/containerContext.js.map +1 -1
- package/lib/contracts.d.ts +8 -0
- package/lib/contracts.d.ts.map +1 -1
- package/lib/contracts.js.map +1 -1
- package/lib/deltaManager.d.ts +1 -1
- package/lib/deltaManager.d.ts.map +1 -1
- package/lib/deltaManager.js +4 -3
- package/lib/deltaManager.js.map +1 -1
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/package.json +22 -15
- package/prettier.config.cjs +8 -0
- package/src/audience.ts +6 -1
- package/src/collabWindowTracker.ts +5 -5
- package/src/connectionManager.ts +15 -3
- package/src/connectionStateHandler.ts +87 -39
- package/src/container.ts +26 -6
- package/src/containerContext.ts +8 -2
- package/src/contracts.ts +8 -0
- package/src/deltaManager.ts +4 -3
- package/src/packageVersion.ts +1 -1
|
@@ -7,14 +7,20 @@ import { ITelemetryLogger, ITelemetryProperties } from "@fluidframework/common-d
|
|
|
7
7
|
import { assert, Timer } from "@fluidframework/common-utils";
|
|
8
8
|
import { IConnectionDetails, IDeltaManager } from "@fluidframework/container-definitions";
|
|
9
9
|
import { ILocalSequencedClient } from "@fluidframework/protocol-base";
|
|
10
|
-
import {
|
|
10
|
+
import { ISequencedClient, IClient } from "@fluidframework/protocol-definitions";
|
|
11
11
|
import { PerformanceEvent, loggerToMonitoringContext } from "@fluidframework/telemetry-utils";
|
|
12
12
|
import { ConnectionState } from "./connectionState";
|
|
13
13
|
import { CatchUpMonitor, ICatchUpMonitor } from "./catchUpMonitor";
|
|
14
14
|
import { IProtocolHandler } from "./protocol";
|
|
15
15
|
|
|
16
|
+
// Based on recent data, it looks like majority of cases where we get stuck are due to really slow or
|
|
17
|
+
// timing out ops fetches. So attempt recovery infrequently. Also fetch uses 30 second timeout, so
|
|
18
|
+
// if retrying fixes the problem, we should not see these events.
|
|
16
19
|
const JoinOpTimeoutMs = 45000;
|
|
17
20
|
|
|
21
|
+
// Timeout waiting for "self" join signal, before giving up
|
|
22
|
+
const JoinSignalTimeoutMs = 5000;
|
|
23
|
+
|
|
18
24
|
/** Constructor parameter type for passing in dependencies needed by the ConnectionStateHandler */
|
|
19
25
|
export interface IConnectionStateHandlerInputs {
|
|
20
26
|
logger: ITelemetryLogger;
|
|
@@ -39,7 +45,7 @@ export interface IConnectionStateHandler {
|
|
|
39
45
|
containerSaved(): void;
|
|
40
46
|
dispose(): void;
|
|
41
47
|
initProtocol(protocol: IProtocolHandler): void;
|
|
42
|
-
receivedConnectEvent(
|
|
48
|
+
receivedConnectEvent(details: IConnectionDetails): void;
|
|
43
49
|
receivedDisconnectEvent(reason: string): void;
|
|
44
50
|
}
|
|
45
51
|
|
|
@@ -50,7 +56,8 @@ export function createConnectionStateHandler(
|
|
|
50
56
|
) {
|
|
51
57
|
const mc = loggerToMonitoringContext(inputs.logger);
|
|
52
58
|
return createConnectionStateHandlerCore(
|
|
53
|
-
mc.config.getBoolean("Fluid.Container.CatchUpBeforeDeclaringConnected") === true,
|
|
59
|
+
mc.config.getBoolean("Fluid.Container.CatchUpBeforeDeclaringConnected") === true, // connectedRaisedWhenCaughtUp
|
|
60
|
+
mc.config.getBoolean("Fluid.Container.DisableJoinSignalWait") !== true, // readClientsWaitForJoinSignal
|
|
54
61
|
inputs,
|
|
55
62
|
deltaManager,
|
|
56
63
|
clientId,
|
|
@@ -58,20 +65,34 @@ export function createConnectionStateHandler(
|
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
export function createConnectionStateHandlerCore(
|
|
61
|
-
|
|
68
|
+
connectedRaisedWhenCaughtUp: boolean,
|
|
69
|
+
readClientsWaitForJoinSignal: boolean,
|
|
62
70
|
inputs: IConnectionStateHandlerInputs,
|
|
63
71
|
deltaManager: IDeltaManager<any, any>,
|
|
64
72
|
clientId?: string,
|
|
65
73
|
) {
|
|
66
|
-
if (!
|
|
67
|
-
return new ConnectionStateHandler(inputs, clientId);
|
|
74
|
+
if (!connectedRaisedWhenCaughtUp) {
|
|
75
|
+
return new ConnectionStateHandler(inputs, readClientsWaitForJoinSignal, clientId);
|
|
68
76
|
}
|
|
69
77
|
return new ConnectionStateCatchup(
|
|
70
78
|
inputs,
|
|
71
|
-
(handler: IConnectionStateHandlerInputs) => new ConnectionStateHandler(
|
|
79
|
+
(handler: IConnectionStateHandlerInputs) => new ConnectionStateHandler(
|
|
80
|
+
handler,
|
|
81
|
+
readClientsWaitForJoinSignal,
|
|
82
|
+
clientId),
|
|
72
83
|
deltaManager);
|
|
73
84
|
}
|
|
74
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Helper internal interface to abstract away Audience & Quorum
|
|
88
|
+
*/
|
|
89
|
+
interface IMembership {
|
|
90
|
+
on(
|
|
91
|
+
eventName: "addMember" | "removeMember",
|
|
92
|
+
listener: (clientId: string, details: IClient | ISequencedClient) => void);
|
|
93
|
+
getMember(clientId: string): undefined | unknown;
|
|
94
|
+
}
|
|
95
|
+
|
|
75
96
|
/**
|
|
76
97
|
* Class that can be used as a base class for building IConnectionStateHandler adapters / pipeline.
|
|
77
98
|
* It implements both ends of communication interfaces and passes data back and forward
|
|
@@ -97,8 +118,8 @@ class ConnectionStateHandlerPassThrough implements IConnectionStateHandler, ICon
|
|
|
97
118
|
public initProtocol(protocol: IProtocolHandler) { return this.pimpl.initProtocol(protocol); }
|
|
98
119
|
public receivedDisconnectEvent(reason: string) { return this.pimpl.receivedDisconnectEvent(reason); }
|
|
99
120
|
|
|
100
|
-
public receivedConnectEvent(
|
|
101
|
-
return this.pimpl.receivedConnectEvent(
|
|
121
|
+
public receivedConnectEvent(details: IConnectionDetails) {
|
|
122
|
+
return this.pimpl.receivedConnectEvent(details);
|
|
102
123
|
}
|
|
103
124
|
|
|
104
125
|
/**
|
|
@@ -201,8 +222,8 @@ class ConnectionStateCatchup extends ConnectionStateHandlerPassThrough {
|
|
|
201
222
|
*
|
|
202
223
|
* For (a) we give up waiting after some time (same timeout as server uses), and go ahead and transition to Connected.
|
|
203
224
|
*
|
|
204
|
-
* For (b) we log telemetry if it takes too long, but still only transition to Connected when the Join op is
|
|
205
|
-
*
|
|
225
|
+
* For (b) we log telemetry if it takes too long, but still only transition to Connected when the Join op/signal is
|
|
226
|
+
* processed.
|
|
206
227
|
*
|
|
207
228
|
* For (c) this is optional behavior, controlled by the parameters of receivedConnectEvent
|
|
208
229
|
*/
|
|
@@ -212,6 +233,8 @@ class ConnectionStateHandler implements IConnectionStateHandler {
|
|
|
212
233
|
private readonly prevClientLeftTimer: Timer;
|
|
213
234
|
private readonly joinOpTimer: Timer;
|
|
214
235
|
private protocol?: IProtocolHandler;
|
|
236
|
+
private connection?: IConnectionDetails;
|
|
237
|
+
private _clientId?: string;
|
|
215
238
|
|
|
216
239
|
private waitEvent: PerformanceEvent | undefined;
|
|
217
240
|
|
|
@@ -229,8 +252,10 @@ class ConnectionStateHandler implements IConnectionStateHandler {
|
|
|
229
252
|
|
|
230
253
|
constructor(
|
|
231
254
|
private readonly handler: IConnectionStateHandlerInputs,
|
|
232
|
-
private
|
|
255
|
+
private readonly readClientsWaitForJoinSignal: boolean,
|
|
256
|
+
clientIdFromPausedSession?: string,
|
|
233
257
|
) {
|
|
258
|
+
this._clientId = clientIdFromPausedSession;
|
|
234
259
|
this.prevClientLeftTimer = new Timer(
|
|
235
260
|
// Default is 5 min for which we are going to wait for its own "leave" message. This is same as
|
|
236
261
|
// the max time on server after which leave op is sent.
|
|
@@ -242,11 +267,8 @@ class ConnectionStateHandler implements IConnectionStateHandler {
|
|
|
242
267
|
},
|
|
243
268
|
);
|
|
244
269
|
|
|
245
|
-
// Based on recent data, it looks like majority of cases where we get stuck are due to really slow or
|
|
246
|
-
// timing out ops fetches. So attempt recovery infrequently. Also fetch uses 30 second timeout, so
|
|
247
|
-
// if retrying fixes the problem, we should not see these events.
|
|
248
270
|
this.joinOpTimer = new Timer(
|
|
249
|
-
|
|
271
|
+
0, // default value is not used - startJoinOpTimer() explicitly provides timeout
|
|
250
272
|
() => {
|
|
251
273
|
// I've observed timer firing within couple ms from disconnect event, looks like
|
|
252
274
|
// queued timer callback is not cancelled if timer is cancelled while callback sits in the queue.
|
|
@@ -266,7 +288,10 @@ class ConnectionStateHandler implements IConnectionStateHandler {
|
|
|
266
288
|
|
|
267
289
|
private startJoinOpTimer() {
|
|
268
290
|
assert(!this.joinOpTimer.hasTimer, 0x234 /* "has joinOpTimer" */);
|
|
269
|
-
this.
|
|
291
|
+
assert(this.connection !== undefined, 0x4b3 /* have connection */);
|
|
292
|
+
this.joinOpTimer.start(
|
|
293
|
+
this.connection.mode === "write" ? JoinOpTimeoutMs : JoinSignalTimeoutMs,
|
|
294
|
+
);
|
|
270
295
|
}
|
|
271
296
|
|
|
272
297
|
private stopJoinOpTimer() {
|
|
@@ -297,8 +322,8 @@ class ConnectionStateHandler implements IConnectionStateHandler {
|
|
|
297
322
|
if (clientId === this.pendingClientId) {
|
|
298
323
|
if (this.joinOpTimer.hasTimer) {
|
|
299
324
|
this.stopJoinOpTimer();
|
|
300
|
-
} else {
|
|
301
|
-
// timer has already fired, meaning it took too long to get join
|
|
325
|
+
} else if (this.shouldWaitForJoinSignal()) {
|
|
326
|
+
// timer has already fired, meaning it took too long to get join op/signal.
|
|
302
327
|
// Record how long it actually took to recover.
|
|
303
328
|
this.handler.logConnectionIssue("ReceivedJoinOp");
|
|
304
329
|
}
|
|
@@ -333,9 +358,11 @@ class ConnectionStateHandler implements IConnectionStateHandler {
|
|
|
333
358
|
this.setConnectionState(ConnectionState.Connected);
|
|
334
359
|
} else {
|
|
335
360
|
// Adding this event temporarily so that we can get help debugging if something goes wrong.
|
|
361
|
+
// We may not see any ops due to being disconnected all that time - that's not an error!
|
|
362
|
+
const error = source === "timeout" && this.connectionState !== ConnectionState.Disconnected;
|
|
336
363
|
this.handler.logger.sendTelemetryEvent({
|
|
337
364
|
eventName: "connectedStateRejected",
|
|
338
|
-
category:
|
|
365
|
+
category: error ? "error" : "generic",
|
|
339
366
|
details: JSON.stringify({
|
|
340
367
|
source,
|
|
341
368
|
pendingClientId: this.pendingClientId,
|
|
@@ -356,29 +383,35 @@ class ConnectionStateHandler implements IConnectionStateHandler {
|
|
|
356
383
|
}
|
|
357
384
|
|
|
358
385
|
public receivedDisconnectEvent(reason: string) {
|
|
386
|
+
this.connection = undefined;
|
|
359
387
|
this.setConnectionState(ConnectionState.Disconnected, reason);
|
|
360
388
|
}
|
|
361
389
|
|
|
390
|
+
private shouldWaitForJoinSignal() {
|
|
391
|
+
assert(this.connection !== undefined, 0x4b4 /* all callers call here with active connection */);
|
|
392
|
+
return this.connection.mode === "write" || this.readClientsWaitForJoinSignal;
|
|
393
|
+
}
|
|
394
|
+
|
|
362
395
|
/**
|
|
363
396
|
* The "connect" event indicates the connection to the Relay Service is live.
|
|
364
397
|
* However, some additional conditions must be met before we can fully transition to
|
|
365
398
|
* "Connected" state. This function handles that interim period, known as "Connecting" state.
|
|
366
|
-
* @param connectionMode - Read or Write connection
|
|
367
399
|
* @param details - Connection details returned from the Relay Service
|
|
368
400
|
* @param deltaManager - DeltaManager to be used for delaying Connected transition until caught up.
|
|
369
401
|
* If it's undefined, then don't delay and transition to Connected as soon as Leave/Join op are accounted for
|
|
370
402
|
*/
|
|
371
403
|
public receivedConnectEvent(
|
|
372
|
-
connectionMode: ConnectionMode,
|
|
373
404
|
details: IConnectionDetails,
|
|
374
405
|
) {
|
|
406
|
+
this.connection = details;
|
|
407
|
+
|
|
375
408
|
const oldState = this._connectionState;
|
|
376
409
|
this._connectionState = ConnectionState.CatchingUp;
|
|
377
410
|
|
|
378
|
-
const writeConnection = connectionMode === "write";
|
|
379
|
-
|
|
380
411
|
// The following checks are wrong. They are only valid if user has write access to a file.
|
|
381
412
|
// If user lost such access mid-session, user will not be able to get "write" connection.
|
|
413
|
+
//
|
|
414
|
+
// const writeConnection = details.mode === "write";
|
|
382
415
|
// assert(!this.handler.shouldClientJoinWrite() || writeConnection,
|
|
383
416
|
// 0x30a /* shouldClientJoinWrite should imply this is a writeConnection */);
|
|
384
417
|
// assert(!this.waitingForLeaveOp || writeConnection,
|
|
@@ -395,16 +428,14 @@ class ConnectionStateHandler implements IConnectionStateHandler {
|
|
|
395
428
|
// IMPORTANT: Report telemetry after we set _pendingClientId, but before transitioning to Connected state
|
|
396
429
|
this.handler.connectionStateChanged(ConnectionState.CatchingUp, oldState);
|
|
397
430
|
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
// Previous client left, and we are waiting for our own join op. When it is processed we'll join the quorum
|
|
407
|
-
// and attempt to transition to Connected state via receivedAddMemberEvent.
|
|
431
|
+
// Check if we need to wait for join op/signal, and if we need to wait for leave op from previous connection.
|
|
432
|
+
// Pending clientId could have joined already (i.e. join op/signal already processed):
|
|
433
|
+
// We are fetching ops from storage in parallel to connecting to Relay Service,
|
|
434
|
+
// and given async processes, it's possible that we have already processed our own join message before
|
|
435
|
+
// connection was fully established.
|
|
436
|
+
if (!this.hasMember(this._pendingClientId) && this.shouldWaitForJoinSignal()) {
|
|
437
|
+
// We are waiting for our own join op / signal. When it is processed
|
|
438
|
+
// we'll attempt to transition to Connected state via receivedAddMemberEvent() flow.
|
|
408
439
|
this.startJoinOpTimer();
|
|
409
440
|
} else if (!this.waitingForLeaveOp) {
|
|
410
441
|
// We're not waiting for Join or Leave op (if read-only connection those don't even apply),
|
|
@@ -426,6 +457,10 @@ class ConnectionStateHandler implements IConnectionStateHandler {
|
|
|
426
457
|
|
|
427
458
|
const oldState = this._connectionState;
|
|
428
459
|
this._connectionState = value;
|
|
460
|
+
|
|
461
|
+
// This is the only place in code that deals with quorum. The rest works with audience
|
|
462
|
+
// The code below ensures that we do not send ops until we know that old "write" client's disconnect
|
|
463
|
+
// produced (and sequenced) leave op
|
|
429
464
|
let client: ILocalSequencedClient | undefined;
|
|
430
465
|
if (this._clientId !== undefined) {
|
|
431
466
|
client = this.protocol?.quorum?.getMember(this._clientId);
|
|
@@ -476,23 +511,36 @@ class ConnectionStateHandler implements IConnectionStateHandler {
|
|
|
476
511
|
// Helper method to switch between quorum and audience.
|
|
477
512
|
// Old design was checking only quorum for "write" clients.
|
|
478
513
|
// Latest change checks audience for all types of connections.
|
|
479
|
-
protected get membership() {
|
|
480
|
-
|
|
514
|
+
protected get membership(): IMembership | undefined {
|
|
515
|
+
// We could always use audience here, and in practice it will probably be correct.
|
|
516
|
+
// (including case when this.readClientsWaitForJoinSignal === false).
|
|
517
|
+
// But only if it's superset of quorum, i.e. when filtered to "write" clients, they are always identical!
|
|
518
|
+
// It's safer to assume that we have bugs and engaging kill-bit switch should bring us back to well-known
|
|
519
|
+
// and tested state!
|
|
520
|
+
return this.readClientsWaitForJoinSignal ? this.protocol?.audience : this.protocol?.quorum;
|
|
481
521
|
}
|
|
482
522
|
|
|
483
523
|
public initProtocol(protocol: IProtocolHandler) {
|
|
484
524
|
this.protocol = protocol;
|
|
485
525
|
|
|
486
|
-
this.membership?.on("addMember", (clientId) => {
|
|
526
|
+
this.membership?.on("addMember", (clientId, details) => {
|
|
527
|
+
assert((details as IClient).mode === "read" || protocol.quorum.getMember(clientId) !== undefined,
|
|
528
|
+
0x4b5 /* Audience is subset of quorum */);
|
|
487
529
|
this.receivedAddMemberEvent(clientId);
|
|
488
530
|
});
|
|
489
531
|
|
|
490
532
|
this.membership?.on("removeMember", (clientId) => {
|
|
533
|
+
assert(protocol.quorum.getMember(clientId) === undefined, 0x4b6 /* Audience is subset of quorum */);
|
|
491
534
|
this.receivedRemoveMemberEvent(clientId);
|
|
492
535
|
});
|
|
493
536
|
|
|
494
|
-
|
|
495
|
-
|
|
537
|
+
/* There is a tiny tiny race possible, where these events happen in this order:
|
|
538
|
+
1. A connection is established (no "cached" mode is used, so it happens in parallel / faster than other steps)
|
|
539
|
+
2. Some other client produces a summary
|
|
540
|
+
3. We get "lucky" and load from that summary as our initial snapshot
|
|
541
|
+
4. ConnectionStateHandler.initProtocol is called, "self" is already in the quorum.
|
|
542
|
+
We could avoid this sequence (and delete test case for it) if we move connection lower in Container.load()
|
|
543
|
+
*/
|
|
496
544
|
if (this.hasMember(this.pendingClientId)) {
|
|
497
545
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
498
546
|
this.receivedAddMemberEvent(this.pendingClientId!);
|
package/src/container.ts
CHANGED
|
@@ -241,14 +241,14 @@ const getCodeProposal =
|
|
|
241
241
|
* @param eventName - event name
|
|
242
242
|
* @param action - functor to call and measure
|
|
243
243
|
*/
|
|
244
|
-
async function ReportIfTooLong(
|
|
244
|
+
export async function ReportIfTooLong(
|
|
245
245
|
logger: ITelemetryLogger,
|
|
246
246
|
eventName: string,
|
|
247
247
|
action: () => Promise<ITelemetryProperties>,
|
|
248
248
|
) {
|
|
249
249
|
const event = PerformanceEvent.start(logger, { eventName });
|
|
250
250
|
const props = await action();
|
|
251
|
-
if (event.duration >
|
|
251
|
+
if (event.duration > 200) {
|
|
252
252
|
event.end(props);
|
|
253
253
|
}
|
|
254
254
|
}
|
|
@@ -646,13 +646,28 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
|
|
|
646
646
|
shouldClientJoinWrite: () => this._deltaManager.connectionManager.shouldJoinWrite(),
|
|
647
647
|
maxClientLeaveWaitTime: this.loader.services.options.maxClientLeaveWaitTime,
|
|
648
648
|
logConnectionIssue: (eventName: string, details?: ITelemetryProperties) => {
|
|
649
|
+
const mode = this.connectionMode;
|
|
649
650
|
// We get here when socket does not receive any ops on "write" connection, including
|
|
650
651
|
// its own join op. Attempt recovery option.
|
|
651
652
|
this._deltaManager.logConnectionIssue({
|
|
652
653
|
eventName,
|
|
654
|
+
mode,
|
|
653
655
|
duration: performance.now() - this.connectionTransitionTimes[ConnectionState.CatchingUp],
|
|
654
656
|
...(details === undefined ? {} : { details: JSON.stringify(details) }),
|
|
655
657
|
});
|
|
658
|
+
|
|
659
|
+
// If this is "write" connection, it took too long to receive join op. But in most cases that's due
|
|
660
|
+
// to very slow op fetches and we will eventually get there.
|
|
661
|
+
// For "read" connections, we get here due to self join signal not arriving on time. We will need to
|
|
662
|
+
// better understand when and why it may happen.
|
|
663
|
+
// For now, attempt to recover by reconnecting. In future, maybe we can query relay service for
|
|
664
|
+
// current state of audience.
|
|
665
|
+
// Other possible recovery path - move to connected state (i.e. ConnectionStateHandler.joinOpTimer
|
|
666
|
+
// to call this.applyForConnectedState("addMemberEvent") for "read" connections)
|
|
667
|
+
if (mode === "read") {
|
|
668
|
+
this.disconnect();
|
|
669
|
+
this.connect();
|
|
670
|
+
}
|
|
656
671
|
},
|
|
657
672
|
},
|
|
658
673
|
this.deltaManager,
|
|
@@ -1510,8 +1525,8 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
|
|
|
1510
1525
|
deltaManager.inboundSignal.pause();
|
|
1511
1526
|
|
|
1512
1527
|
deltaManager.on("connect", (details: IConnectionDetails, _opsBehind?: number) => {
|
|
1528
|
+
assert(this.connectionMode === details.mode, 0x4b7 /* mismatch */);
|
|
1513
1529
|
this.connectionStateHandler.receivedConnectEvent(
|
|
1514
|
-
this.connectionMode,
|
|
1515
1530
|
details,
|
|
1516
1531
|
);
|
|
1517
1532
|
});
|
|
@@ -1671,7 +1686,8 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
|
|
|
1671
1686
|
MessageType.Operation,
|
|
1672
1687
|
message.contents,
|
|
1673
1688
|
true, // batch
|
|
1674
|
-
message.metadata
|
|
1689
|
+
message.metadata,
|
|
1690
|
+
message.compression);
|
|
1675
1691
|
}
|
|
1676
1692
|
this._deltaManager.flush();
|
|
1677
1693
|
return clientSequenceNumber;
|
|
@@ -1690,7 +1706,11 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
|
|
|
1690
1706
|
return this.submitMessage(MessageType.Summarize, JSON.stringify(summary), false /* batch */);
|
|
1691
1707
|
}
|
|
1692
1708
|
|
|
1693
|
-
private submitMessage(type: MessageType,
|
|
1709
|
+
private submitMessage(type: MessageType,
|
|
1710
|
+
contents?: string,
|
|
1711
|
+
batch?: boolean,
|
|
1712
|
+
metadata?: any,
|
|
1713
|
+
compression?: string): number {
|
|
1694
1714
|
if (this.connectionState !== ConnectionState.Connected) {
|
|
1695
1715
|
this.mc.logger.sendErrorEvent({ eventName: "SubmitMessageWithNoConnection", type });
|
|
1696
1716
|
return -1;
|
|
@@ -1698,7 +1718,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
|
|
|
1698
1718
|
|
|
1699
1719
|
this.messageCountAfterDisconnection += 1;
|
|
1700
1720
|
this.collabWindowTracker?.stopSequenceNumberUpdate();
|
|
1701
|
-
return this._deltaManager.submit(type, contents, batch, metadata);
|
|
1721
|
+
return this._deltaManager.submit(type, contents, batch, metadata, compression);
|
|
1702
1722
|
}
|
|
1703
1723
|
|
|
1704
1724
|
private processRemoteMessage(message: ISequencedDocumentMessage) {
|
package/src/containerContext.ts
CHANGED
|
@@ -46,7 +46,7 @@ import {
|
|
|
46
46
|
ISummaryContent,
|
|
47
47
|
} from "@fluidframework/protocol-definitions";
|
|
48
48
|
import { PerformanceEvent } from "@fluidframework/telemetry-utils";
|
|
49
|
-
import { Container } from "./container";
|
|
49
|
+
import { Container, ReportIfTooLong } from "./container";
|
|
50
50
|
|
|
51
51
|
const PackageNotFactoryError = "Code package does not implement IRuntimeFactory";
|
|
52
52
|
|
|
@@ -324,7 +324,13 @@ export class ContainerContext implements IContainerContext {
|
|
|
324
324
|
|
|
325
325
|
private async instantiateRuntime(existing: boolean) {
|
|
326
326
|
const runtimeFactory = await this.getRuntimeFactory();
|
|
327
|
-
|
|
327
|
+
await ReportIfTooLong(
|
|
328
|
+
this.taggedLogger,
|
|
329
|
+
"instantiateRuntime",
|
|
330
|
+
async () => {
|
|
331
|
+
this._runtime = await runtimeFactory.instantiateRuntime(this, existing);
|
|
332
|
+
return {};
|
|
333
|
+
});
|
|
328
334
|
}
|
|
329
335
|
|
|
330
336
|
private attachListener() {
|
package/src/contracts.ts
CHANGED
|
@@ -148,15 +148,23 @@ export interface IConnectionManagerFactoryArgs {
|
|
|
148
148
|
|
|
149
149
|
/**
|
|
150
150
|
* Called whenever ping/pong messages are roundtripped on connection.
|
|
151
|
+
*
|
|
152
|
+
* @deprecated No replacement API intended.
|
|
151
153
|
*/
|
|
152
154
|
readonly pongHandler: (latency: number) => void;
|
|
153
155
|
|
|
154
156
|
/**
|
|
155
157
|
* Called whenever connection type changes from writable to read-only or vice versa.
|
|
158
|
+
*
|
|
159
|
+
* @remarks
|
|
160
|
+
*
|
|
156
161
|
* Connection can be read-only if user has no edit permissions, or if container forced
|
|
157
162
|
* connection to be read-only.
|
|
158
163
|
* This should not be confused with "read" / "write"connection mode which is internal
|
|
159
164
|
* optimization.
|
|
165
|
+
*
|
|
166
|
+
* @param readonly - Whether or not the container is now read-only.
|
|
167
|
+
* `undefined` indicates that user permissions are not yet known.
|
|
160
168
|
*/
|
|
161
169
|
readonly readonlyChangeHandler: (readonly?: boolean) => void;
|
|
162
170
|
}
|
package/src/deltaManager.ts
CHANGED
|
@@ -118,8 +118,8 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
|
|
|
118
118
|
// There are three numbers we track
|
|
119
119
|
// * lastQueuedSequenceNumber is the last queued sequence number. If there are gaps in seq numbers, then this number
|
|
120
120
|
// is not updated until we cover that gap, so it increases each time by 1.
|
|
121
|
-
// * lastObservedSeqNumber is
|
|
122
|
-
// populated at web socket connection time (if storage provides that info) and is
|
|
121
|
+
// * lastObservedSeqNumber is an estimation of last known sequence number for container in storage. It's initially
|
|
122
|
+
// populated at web socket connection time (if storage provides that info) and is updated once ops shows up.
|
|
123
123
|
// It's never less than lastQueuedSequenceNumber
|
|
124
124
|
// * lastProcessedSequenceNumber - last processed sequence number
|
|
125
125
|
private lastQueuedSequenceNumber: number = 0;
|
|
@@ -210,12 +210,13 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
|
|
|
210
210
|
public get readOnlyInfo() { return this.connectionManager.readOnlyInfo; }
|
|
211
211
|
public get clientDetails() { return this.connectionManager.clientDetails; }
|
|
212
212
|
|
|
213
|
-
public submit(type: MessageType, contents?: string, batch = false, metadata?: any) {
|
|
213
|
+
public submit(type: MessageType, contents?: string, batch = false, metadata?: any, compression?: string) {
|
|
214
214
|
const messagePartial: Omit<IDocumentMessage, "clientSequenceNumber"> = {
|
|
215
215
|
contents,
|
|
216
216
|
metadata,
|
|
217
217
|
referenceSequenceNumber: this.lastProcessedSequenceNumber,
|
|
218
218
|
type,
|
|
219
|
+
compression,
|
|
219
220
|
};
|
|
220
221
|
|
|
221
222
|
if (!batch) {
|
package/src/packageVersion.ts
CHANGED