@fluidframework/container-loader 0.52.0 → 0.54.0-47413
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/connectionManager.d.ts +153 -0
- package/dist/connectionManager.d.ts.map +1 -0
- package/dist/connectionManager.js +664 -0
- package/dist/connectionManager.js.map +1 -0
- package/dist/connectionStateHandler.d.ts +1 -0
- package/dist/connectionStateHandler.d.ts.map +1 -1
- package/dist/connectionStateHandler.js +6 -0
- package/dist/connectionStateHandler.js.map +1 -1
- package/dist/container.d.ts +2 -22
- package/dist/container.d.ts.map +1 -1
- package/dist/container.js +121 -151
- package/dist/container.js.map +1 -1
- package/dist/containerContext.d.ts +1 -0
- package/dist/containerContext.d.ts.map +1 -1
- package/dist/containerContext.js +4 -0
- package/dist/containerContext.js.map +1 -1
- package/dist/contracts.d.ts +112 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +14 -0
- package/dist/contracts.js.map +1 -0
- package/dist/deltaManager.d.ts +26 -142
- package/dist/deltaManager.d.ts.map +1 -1
- package/dist/deltaManager.js +143 -770
- package/dist/deltaManager.js.map +1 -1
- package/dist/loader.d.ts +14 -4
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +10 -4
- 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/protocolTreeDocumentStorageService.d.ts +2 -2
- package/dist/protocolTreeDocumentStorageService.d.ts.map +1 -1
- package/lib/connectionManager.d.ts +153 -0
- package/lib/connectionManager.d.ts.map +1 -0
- package/lib/connectionManager.js +660 -0
- package/lib/connectionManager.js.map +1 -0
- package/lib/connectionStateHandler.d.ts +1 -0
- package/lib/connectionStateHandler.d.ts.map +1 -1
- package/lib/connectionStateHandler.js +6 -0
- package/lib/connectionStateHandler.js.map +1 -1
- package/lib/container.d.ts +2 -22
- package/lib/container.d.ts.map +1 -1
- package/lib/container.js +122 -152
- package/lib/container.js.map +1 -1
- package/lib/containerContext.d.ts +1 -0
- package/lib/containerContext.d.ts.map +1 -1
- package/lib/containerContext.js +4 -0
- package/lib/containerContext.js.map +1 -1
- package/lib/contracts.d.ts +112 -0
- package/lib/contracts.d.ts.map +1 -0
- package/lib/contracts.js +11 -0
- package/lib/contracts.js.map +1 -0
- package/lib/deltaManager.d.ts +26 -142
- package/lib/deltaManager.d.ts.map +1 -1
- package/lib/deltaManager.js +147 -774
- package/lib/deltaManager.js.map +1 -1
- package/lib/loader.d.ts +14 -4
- package/lib/loader.d.ts.map +1 -1
- package/lib/loader.js +11 -5
- 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/protocolTreeDocumentStorageService.d.ts +2 -2
- package/lib/protocolTreeDocumentStorageService.d.ts.map +1 -1
- package/package.json +9 -9
- package/src/connectionManager.ts +892 -0
- package/src/connectionStateHandler.ts +8 -0
- package/src/container.ts +165 -187
- package/src/containerContext.ts +4 -0
- package/src/contracts.ts +156 -0
- package/src/deltaManager.ts +181 -978
- package/src/loader.ts +59 -27
- package/src/packageVersion.ts +1 -1
package/dist/deltaManager.js
CHANGED
|
@@ -7,7 +7,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
7
7
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
8
|
};
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
-
exports.DeltaManager =
|
|
10
|
+
exports.DeltaManager = void 0;
|
|
11
11
|
const abort_controller_1 = __importDefault(require("abort-controller"));
|
|
12
12
|
const uuid_1 = require("uuid");
|
|
13
13
|
const common_utils_1 = require("@fluidframework/common-utils");
|
|
@@ -18,78 +18,16 @@ const protocol_definitions_1 = require("@fluidframework/protocol-definitions");
|
|
|
18
18
|
const driver_utils_1 = require("@fluidframework/driver-utils");
|
|
19
19
|
const container_utils_1 = require("@fluidframework/container-utils");
|
|
20
20
|
const deltaQueue_1 = require("./deltaQueue");
|
|
21
|
-
const MaxReconnectDelayInMs = 8000;
|
|
22
|
-
const InitialReconnectDelayInMs = 1000;
|
|
23
|
-
const DefaultChunkSize = 16 * 1024;
|
|
24
|
-
function getNackReconnectInfo(nackContent) {
|
|
25
|
-
const message = `Nack (${nackContent.type}): ${nackContent.message}`;
|
|
26
|
-
const canRetry = nackContent.code !== 403;
|
|
27
|
-
const retryAfterMs = nackContent.retryAfter !== undefined ? nackContent.retryAfter * 1000 : undefined;
|
|
28
|
-
return driver_utils_1.createGenericNetworkError(`nack [${nackContent.code}]`, message, canRetry, retryAfterMs, { statusCode: nackContent.code });
|
|
29
|
-
}
|
|
30
|
-
const createReconnectError = (fluidErrorCode, err) => telemetry_utils_1.wrapError(err, (errorMessage) => new driver_utils_1.GenericNetworkError(fluidErrorCode, errorMessage, true /* canRetry */));
|
|
31
|
-
var ReconnectMode;
|
|
32
|
-
(function (ReconnectMode) {
|
|
33
|
-
ReconnectMode["Never"] = "Never";
|
|
34
|
-
ReconnectMode["Disabled"] = "Disabled";
|
|
35
|
-
ReconnectMode["Enabled"] = "Enabled";
|
|
36
|
-
})(ReconnectMode = exports.ReconnectMode || (exports.ReconnectMode = {}));
|
|
37
|
-
/**
|
|
38
|
-
* Implementation of IDocumentDeltaConnection that does not support submitting
|
|
39
|
-
* or receiving ops. Used in storage-only mode.
|
|
40
|
-
*/
|
|
41
|
-
class NoDeltaStream extends common_utils_1.TypedEventEmitter {
|
|
42
|
-
constructor() {
|
|
43
|
-
super(...arguments);
|
|
44
|
-
this.clientId = "storage-only client";
|
|
45
|
-
this.claims = {
|
|
46
|
-
scopes: [protocol_definitions_1.ScopeType.DocRead],
|
|
47
|
-
};
|
|
48
|
-
this.mode = "read";
|
|
49
|
-
this.existing = true;
|
|
50
|
-
this.maxMessageSize = 0;
|
|
51
|
-
this.version = "";
|
|
52
|
-
this.initialMessages = [];
|
|
53
|
-
this.initialSignals = [];
|
|
54
|
-
this.initialClients = [];
|
|
55
|
-
this.serviceConfiguration = {
|
|
56
|
-
maxMessageSize: 0,
|
|
57
|
-
blockSize: 0,
|
|
58
|
-
summary: undefined,
|
|
59
|
-
};
|
|
60
|
-
this.checkpointSequenceNumber = undefined;
|
|
61
|
-
this._disposed = false;
|
|
62
|
-
}
|
|
63
|
-
submit(messages) {
|
|
64
|
-
this.emit("nack", this.clientId, messages.map((operation) => {
|
|
65
|
-
return {
|
|
66
|
-
operation,
|
|
67
|
-
content: { message: "Cannot submit with storage-only connection", code: 403 },
|
|
68
|
-
};
|
|
69
|
-
}));
|
|
70
|
-
}
|
|
71
|
-
submitSignal(message) {
|
|
72
|
-
this.emit("nack", this.clientId, {
|
|
73
|
-
operation: message,
|
|
74
|
-
content: { message: "Cannot submit signal with storage-only connection", code: 403 },
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
get disposed() { return this._disposed; }
|
|
78
|
-
dispose() { this._disposed = true; }
|
|
79
|
-
}
|
|
80
21
|
/**
|
|
81
22
|
* Manages the flow of both inbound and outbound messages. This class ensures that shared objects receive delta
|
|
82
23
|
* messages in order regardless of possible network conditions or timings causing out of order delivery.
|
|
83
24
|
*/
|
|
84
25
|
class DeltaManager extends common_utils_1.TypedEventEmitter {
|
|
85
|
-
constructor(serviceProvider,
|
|
26
|
+
constructor(serviceProvider, logger, _active, createConnectionManager) {
|
|
86
27
|
super();
|
|
87
28
|
this.serviceProvider = serviceProvider;
|
|
88
|
-
this.client = client;
|
|
89
29
|
this.logger = logger;
|
|
90
30
|
this._active = _active;
|
|
91
|
-
// tracks host requiring read-only mode.
|
|
92
|
-
this._forceReadonly = false;
|
|
93
31
|
this.pending = [];
|
|
94
32
|
// The minimum sequence number and last sequence number received from the server
|
|
95
33
|
this.minSequenceNumber = 0;
|
|
@@ -106,82 +44,30 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
|
|
|
106
44
|
this.baseTerm = 0;
|
|
107
45
|
// The sequence number we initially loaded from
|
|
108
46
|
this.initSequenceNumber = 0;
|
|
109
|
-
this.clientSequenceNumber = 0;
|
|
110
|
-
this.clientSequenceNumberObserved = 0;
|
|
111
|
-
// Counts the number of noops sent by the client which may not be acked.
|
|
112
|
-
this.trailingNoopCount = 0;
|
|
113
47
|
this.closed = false;
|
|
114
|
-
this.deltaStreamDelayId = uuid_1.v4();
|
|
115
|
-
this.deltaStorageDelayId = uuid_1.v4();
|
|
116
|
-
this.messageBuffer = [];
|
|
117
|
-
this.connectFirstConnection = true;
|
|
118
48
|
this.throttlingIdSet = new Set();
|
|
119
49
|
this.timeTillThrottling = 0;
|
|
120
|
-
this.connectionStateProps = {};
|
|
121
|
-
// True if current connection has checkpoint information
|
|
122
|
-
// I.e. we know how far behind the client was at the time of establishing connection
|
|
123
|
-
this._hasCheckpointSequenceNumber = false;
|
|
124
50
|
this.closeAbortController = new abort_controller_1.default();
|
|
125
|
-
|
|
126
|
-
this.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
this.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
this.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
this.nackHandler = (documentId, messages) => {
|
|
138
|
-
const message = messages[0];
|
|
139
|
-
// TODO: we should remove this check when service updates?
|
|
140
|
-
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
141
|
-
if (this._readonlyPermissions) {
|
|
142
|
-
this.close(driver_utils_1.createWriteError("writeOnReadOnlyDocument"));
|
|
143
|
-
}
|
|
144
|
-
// check message.content for Back-compat with old service.
|
|
145
|
-
const reconnectInfo = message.content !== undefined
|
|
146
|
-
? getNackReconnectInfo(message.content) :
|
|
147
|
-
driver_utils_1.createGenericNetworkError("nackReasonUnknown", undefined, true);
|
|
148
|
-
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
149
|
-
this.reconnectOnError("write", reconnectInfo);
|
|
150
|
-
};
|
|
151
|
-
// Connection mode is always read on disconnect/error unless the system mode was write.
|
|
152
|
-
this.disconnectHandler = (disconnectReason) => {
|
|
153
|
-
// Note: we might get multiple disconnect calls on same socket, as early disconnect notification
|
|
154
|
-
// ("server_disconnect", ODSP-specific) is mapped to "disconnect"
|
|
155
|
-
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
156
|
-
this.reconnectOnError(this.defaultReconnectionMode, createReconnectError("dmDocumentDeltaConnectionDisconnected", disconnectReason));
|
|
157
|
-
};
|
|
158
|
-
this.errorHandler = (error) => {
|
|
159
|
-
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
160
|
-
this.reconnectOnError(this.defaultReconnectionMode, createReconnectError("dmDocumentDeltaConnectionError", error));
|
|
161
|
-
};
|
|
162
|
-
this.pongHandler = (latency) => {
|
|
163
|
-
this.emit("pong", latency);
|
|
51
|
+
this.deltaStorageDelayId = uuid_1.v4();
|
|
52
|
+
this.deltaStreamDelayId = uuid_1.v4();
|
|
53
|
+
this.messageBuffer = [];
|
|
54
|
+
const props = {
|
|
55
|
+
incomingOpHandler: (messages, reason) => this.enqueueMessages(messages, reason),
|
|
56
|
+
signalHandler: (message) => this._inboundSignal.push(message),
|
|
57
|
+
reconnectionDelayHandler: (delayMs, error) => this.emitDelayInfo(this.deltaStreamDelayId, delayMs, error),
|
|
58
|
+
closeHandler: (error) => this.close(error),
|
|
59
|
+
disconnectHandler: (reason) => this.disconnectHandler(reason),
|
|
60
|
+
connectHandler: (connection) => this.connectHandler(connection),
|
|
61
|
+
pongHandler: (latency) => this.emit("pong", latency),
|
|
62
|
+
readonlyChangeHandler: (readonly) => telemetry_utils_1.safeRaiseEvent(this, this.logger, "readonly", readonly),
|
|
164
63
|
};
|
|
165
|
-
this.
|
|
166
|
-
this.defaultReconnectionMode = this.client.mode;
|
|
167
|
-
this._reconnectMode = reconnectAllowed ? ReconnectMode.Enabled : ReconnectMode.Never;
|
|
64
|
+
this.connectionManager = createConnectionManager(props);
|
|
168
65
|
this._inbound = new deltaQueue_1.DeltaQueue((op) => {
|
|
169
66
|
this.processInboundMessage(op);
|
|
170
67
|
});
|
|
171
68
|
this._inbound.on("error", (error) => {
|
|
172
69
|
this.close(container_utils_1.CreateProcessingError(error, "deltaManagerInboundErrorHandler", this.lastMessage));
|
|
173
70
|
});
|
|
174
|
-
// Outbound message queue. The outbound queue is represented as a queue of an array of ops. Ops contained
|
|
175
|
-
// within an array *must* fit within the maxMessageSize and are guaranteed to be ordered sequentially.
|
|
176
|
-
this._outbound = new deltaQueue_1.DeltaQueue((messages) => {
|
|
177
|
-
if (this.connection === undefined) {
|
|
178
|
-
throw new Error("Attempted to submit an outbound message without connection");
|
|
179
|
-
}
|
|
180
|
-
this.connection.submit(messages);
|
|
181
|
-
});
|
|
182
|
-
this._outbound.on("error", (error) => {
|
|
183
|
-
this.close(telemetry_utils_1.normalizeError(error));
|
|
184
|
-
});
|
|
185
71
|
// Inbound signal queue
|
|
186
72
|
this._inboundSignal = new deltaQueue_1.DeltaQueue((message) => {
|
|
187
73
|
if (this.handler === undefined) {
|
|
@@ -202,21 +88,9 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
|
|
|
202
88
|
get active() { return this._active(); }
|
|
203
89
|
get disposed() { return this.closed; }
|
|
204
90
|
get IDeltaSender() { return this; }
|
|
205
|
-
/**
|
|
206
|
-
* Tells if current connection has checkpoint information.
|
|
207
|
-
* I.e. we know how far behind the client was at the time of establishing connection
|
|
208
|
-
*/
|
|
209
|
-
get hasCheckpointSequenceNumber() {
|
|
210
|
-
// Valid to be called only if we have active connection.
|
|
211
|
-
common_utils_1.assert(this.connection !== undefined, 0x0df /* "Missing active connection" */);
|
|
212
|
-
return this._hasCheckpointSequenceNumber;
|
|
213
|
-
}
|
|
214
91
|
get inbound() {
|
|
215
92
|
return this._inbound;
|
|
216
93
|
}
|
|
217
|
-
get outbound() {
|
|
218
|
-
return this._outbound;
|
|
219
|
-
}
|
|
220
94
|
get inboundSignal() {
|
|
221
95
|
return this._inboundSignal;
|
|
222
96
|
}
|
|
@@ -238,39 +112,22 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
|
|
|
238
112
|
get minimumSequenceNumber() {
|
|
239
113
|
return this.minSequenceNumber;
|
|
240
114
|
}
|
|
241
|
-
get maxMessageSize() {
|
|
242
|
-
var _a, _b, _c;
|
|
243
|
-
return (_c = (_b = (_a = this.connection) === null || _a === void 0 ? void 0 : _a.serviceConfiguration) === null || _b === void 0 ? void 0 : _b.maxMessageSize) !== null && _c !== void 0 ? _c : DefaultChunkSize;
|
|
244
|
-
}
|
|
245
|
-
get version() {
|
|
246
|
-
if (this.connection === undefined) {
|
|
247
|
-
throw new Error("Cannot check version without a connection");
|
|
248
|
-
}
|
|
249
|
-
return this.connection.version;
|
|
250
|
-
}
|
|
251
|
-
get serviceConfiguration() {
|
|
252
|
-
var _a;
|
|
253
|
-
return (_a = this.connection) === null || _a === void 0 ? void 0 : _a.serviceConfiguration;
|
|
254
|
-
}
|
|
255
|
-
get scopes() {
|
|
256
|
-
var _a;
|
|
257
|
-
return (_a = this.connection) === null || _a === void 0 ? void 0 : _a.claims.scopes;
|
|
258
|
-
}
|
|
259
|
-
get socketDocumentId() {
|
|
260
|
-
var _a;
|
|
261
|
-
return (_a = this.connection) === null || _a === void 0 ? void 0 : _a.claims.documentId;
|
|
262
|
-
}
|
|
263
115
|
/**
|
|
264
|
-
*
|
|
116
|
+
* Tells if current connection has checkpoint information.
|
|
117
|
+
* I.e. we know how far behind the client was at the time of establishing connection
|
|
265
118
|
*/
|
|
266
|
-
get
|
|
267
|
-
|
|
268
|
-
common_utils_1.assert(
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}
|
|
119
|
+
get hasCheckpointSequenceNumber() {
|
|
120
|
+
// Valid to be called only if we have active connection.
|
|
121
|
+
common_utils_1.assert(this.connectionManager.connected, 0x0df /* "Missing active connection" */);
|
|
122
|
+
return this._checkpointSequenceNumber !== undefined;
|
|
123
|
+
}
|
|
124
|
+
// Forwarding connection manager properties / IDeltaManager implementation
|
|
125
|
+
get maxMessageSize() { return this.connectionManager.maxMessageSize; }
|
|
126
|
+
get version() { return this.connectionManager.version; }
|
|
127
|
+
get serviceConfiguration() { return this.connectionManager.serviceConfiguration; }
|
|
128
|
+
get outbound() { return this.connectionManager.outbound; }
|
|
129
|
+
get readOnlyInfo() { return this.connectionManager.readOnlyInfo; }
|
|
130
|
+
get clientDetails() { return this.connectionManager.clientDetails; }
|
|
274
131
|
/**
|
|
275
132
|
* Tells if container is in read-only mode.
|
|
276
133
|
* Data stores should listen for "readonly" notifications and disallow user
|
|
@@ -282,118 +139,50 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
|
|
|
282
139
|
* @deprecated - use readOnlyInfo
|
|
283
140
|
*/
|
|
284
141
|
get readonly() {
|
|
285
|
-
|
|
286
|
-
return true;
|
|
287
|
-
}
|
|
288
|
-
return this._readonlyPermissions;
|
|
289
|
-
}
|
|
290
|
-
/**
|
|
291
|
-
* Tells if user has no write permissions for file in storage
|
|
292
|
-
* It is undefined if we have not yet established websocket connection
|
|
293
|
-
* and do not know if user has write access to a file.
|
|
294
|
-
* @deprecated - use readOnlyInfo
|
|
295
|
-
*/
|
|
296
|
-
get readonlyPermissions() {
|
|
297
|
-
return this._readonlyPermissions;
|
|
298
|
-
}
|
|
299
|
-
get readOnlyInfo() {
|
|
300
|
-
const storageOnly = this.connection !== undefined && this.connection instanceof NoDeltaStream;
|
|
301
|
-
if (storageOnly || this._forceReadonly || this._readonlyPermissions === true) {
|
|
302
|
-
return {
|
|
303
|
-
readonly: true,
|
|
304
|
-
forced: this._forceReadonly,
|
|
305
|
-
permissions: this._readonlyPermissions,
|
|
306
|
-
storageOnly,
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
return { readonly: this._readonlyPermissions };
|
|
310
|
-
}
|
|
311
|
-
/**
|
|
312
|
-
* Automatic reconnecting enabled or disabled.
|
|
313
|
-
* If set to Never, then reconnecting will never be allowed.
|
|
314
|
-
*/
|
|
315
|
-
get reconnectMode() {
|
|
316
|
-
return this._reconnectMode;
|
|
142
|
+
return this.readOnlyInfo.readonly;
|
|
317
143
|
}
|
|
318
|
-
|
|
319
|
-
//
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
const
|
|
328
|
-
|
|
144
|
+
submit(type, contents, batch = false, metadata) {
|
|
145
|
+
// Start adding trace for the op.
|
|
146
|
+
const traces = [
|
|
147
|
+
{
|
|
148
|
+
action: "start",
|
|
149
|
+
service: "client",
|
|
150
|
+
timestamp: Date.now(),
|
|
151
|
+
}
|
|
152
|
+
];
|
|
153
|
+
const messagePartial = {
|
|
154
|
+
contents: JSON.stringify(contents),
|
|
155
|
+
metadata,
|
|
156
|
+
referenceSequenceNumber: this.lastProcessedSequenceNumber,
|
|
157
|
+
traces,
|
|
158
|
+
type,
|
|
329
159
|
};
|
|
330
|
-
if (
|
|
331
|
-
|
|
160
|
+
if (!batch) {
|
|
161
|
+
this.flush();
|
|
332
162
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
sentOps: this.clientSequenceNumber });
|
|
163
|
+
const message = this.connectionManager.prepareMessageToSend(messagePartial);
|
|
164
|
+
if (message === undefined) {
|
|
165
|
+
return -1;
|
|
337
166
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
* Will throw an error if reconnectMode set to Never.
|
|
342
|
-
*/
|
|
343
|
-
setAutoReconnect(mode) {
|
|
344
|
-
common_utils_1.assert(mode !== ReconnectMode.Never && this._reconnectMode !== ReconnectMode.Never, 0x278 /* "API is not supported for non-connecting or closed container" */);
|
|
345
|
-
this._reconnectMode = mode;
|
|
346
|
-
if (mode !== ReconnectMode.Enabled) {
|
|
347
|
-
// immediately disconnect - do not rely on service eventually dropping connection.
|
|
348
|
-
this.disconnectFromDeltaStream("setAutoReconnect");
|
|
167
|
+
this.messageBuffer.push(message);
|
|
168
|
+
if (!batch) {
|
|
169
|
+
this.flush();
|
|
349
170
|
}
|
|
171
|
+
this.emit("submitOp", message);
|
|
172
|
+
return message.clientSequenceNumber;
|
|
350
173
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
* (server can return "write" mode even when asked for "read")
|
|
356
|
-
* Leveraging same "readonly" event as runtime & data stores should behave the same in such case
|
|
357
|
-
* as in read-only permissions.
|
|
358
|
-
* But this.active can be used by some DDSes to figure out if ops can be sent
|
|
359
|
-
* (for example, read-only view still participates in code proposals / upgrades decisions)
|
|
360
|
-
*
|
|
361
|
-
* Forcing Readonly does not prevent DDS from generating ops. It is up to user code to honour
|
|
362
|
-
* the readonly flag. If ops are generated, they will accumulate locally and not be sent. If
|
|
363
|
-
* there are pending in the outbound queue, it will stop sending until force readonly is
|
|
364
|
-
* cleared.
|
|
365
|
-
*
|
|
366
|
-
* @param readonly - set or clear force readonly.
|
|
367
|
-
*/
|
|
368
|
-
forceReadonly(readonly) {
|
|
369
|
-
if (readonly !== this._forceReadonly) {
|
|
370
|
-
this.logger.sendTelemetryEvent({
|
|
371
|
-
eventName: "ForceReadOnly",
|
|
372
|
-
value: readonly,
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
const oldValue = this.readonly;
|
|
376
|
-
this._forceReadonly = readonly;
|
|
377
|
-
if (oldValue !== this.readonly) {
|
|
378
|
-
common_utils_1.assert(this._reconnectMode !== ReconnectMode.Never, 0x279 /* "API is not supported for non-connecting or closed container" */);
|
|
379
|
-
let reconnect = false;
|
|
380
|
-
if (this.readonly === true) {
|
|
381
|
-
// If we switch to readonly while connected, we should disconnect first
|
|
382
|
-
// See comment in the "readonly" event handler to deltaManager set up by
|
|
383
|
-
// the ContainerRuntime constructor
|
|
384
|
-
if (this.shouldJoinWrite()) {
|
|
385
|
-
// If we have pending changes, then we will never send them - it smells like
|
|
386
|
-
// host logic error.
|
|
387
|
-
this.logger.sendErrorEvent({ eventName: "ForceReadonlyPendingChanged" });
|
|
388
|
-
}
|
|
389
|
-
reconnect = this.disconnectFromDeltaStream("Force readonly");
|
|
390
|
-
}
|
|
391
|
-
telemetry_utils_1.safeRaiseEvent(this, this.logger, "readonly", this.readonly);
|
|
392
|
-
if (reconnect) {
|
|
393
|
-
// reconnect if we disconnected from before.
|
|
394
|
-
this.triggerConnect({ reason: "forceReadonly", mode: "read", fetchOpsFromStorage: false });
|
|
395
|
-
}
|
|
174
|
+
submitSignal(content) { return this.connectionManager.submitSignal(content); }
|
|
175
|
+
flush() {
|
|
176
|
+
if (this.messageBuffer.length === 0) {
|
|
177
|
+
return;
|
|
396
178
|
}
|
|
179
|
+
// The prepareFlush event allows listeners to append metadata to the batch prior to submission.
|
|
180
|
+
this.emit("prepareSend", this.messageBuffer);
|
|
181
|
+
this.connectionManager.sendMessages(this.messageBuffer);
|
|
182
|
+
this.messageBuffer = [];
|
|
183
|
+
}
|
|
184
|
+
get connectionProps() {
|
|
185
|
+
return Object.assign({ sequenceNumber: this.lastSequenceNumber }, this.connectionManager.connectionProps);
|
|
397
186
|
}
|
|
398
187
|
/**
|
|
399
188
|
* Log error event with a bunch of internal to DeltaManager information about state of op processing
|
|
@@ -403,20 +192,46 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
|
|
|
403
192
|
*/
|
|
404
193
|
logConnectionIssue(event) {
|
|
405
194
|
var _a;
|
|
406
|
-
common_utils_1.assert(this.
|
|
195
|
+
common_utils_1.assert(this.connectionManager.connected, 0x238 /* "called only in connected state" */);
|
|
407
196
|
const pendingSorted = this.pending.sort((a, b) => a.sequenceNumber - b.sequenceNumber);
|
|
408
197
|
this.logger.sendErrorEvent(Object.assign(Object.assign(Object.assign(Object.assign({}, event), {
|
|
409
198
|
// This directly tells us if fetching ops is in flight, and thus likely the reason of
|
|
410
199
|
// stalled op processing
|
|
411
200
|
fetchReason: this.fetchReason,
|
|
412
201
|
// A bunch of useful sequence numbers to understand if we are holding some ops from processing
|
|
413
|
-
lastQueuedSequenceNumber: this.lastQueuedSequenceNumber, lastProcessedSequenceNumber: this.lastProcessedSequenceNumber, lastObserved: this.lastObservedSeqNumber }), this.
|
|
202
|
+
lastQueuedSequenceNumber: this.lastQueuedSequenceNumber, lastProcessedSequenceNumber: this.lastProcessedSequenceNumber, lastObserved: this.lastObservedSeqNumber }), this.connectionManager.connectionVerboseProps), { pendingOps: this.pending.length, pendingFirst: (_a = pendingSorted[0]) === null || _a === void 0 ? void 0 : _a.sequenceNumber, haveHandler: this.handler !== undefined, inboundLength: this.inbound.length, inboundPaused: this.inbound.paused }));
|
|
414
203
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
204
|
+
connectHandler(connection) {
|
|
205
|
+
this.refreshDelayInfo(this.deltaStreamDelayId);
|
|
206
|
+
const props = this.connectionManager.connectionVerboseProps;
|
|
207
|
+
props.connectionLastQueuedSequenceNumber = this.lastQueuedSequenceNumber;
|
|
208
|
+
props.connectionLastObservedSeqNumber = this.lastObservedSeqNumber;
|
|
209
|
+
const checkpointSequenceNumber = connection.checkpointSequenceNumber;
|
|
210
|
+
this._checkpointSequenceNumber = checkpointSequenceNumber;
|
|
211
|
+
if (checkpointSequenceNumber !== undefined) {
|
|
212
|
+
this.updateLatestKnownOpSeqNumber(checkpointSequenceNumber);
|
|
213
|
+
}
|
|
214
|
+
// We cancel all ops on lost of connectivity, and rely on DDSes to resubmit them.
|
|
215
|
+
// Semantics are not well defined for batches (and they are broken right now on disconnects anyway),
|
|
216
|
+
// but it's safe to assume (until better design is put into place) that batches should not exist
|
|
217
|
+
// across multiple connections. Right now we assume runtime will not submit any ops in disconnected
|
|
218
|
+
// state. As requirements change, so should these checks.
|
|
219
|
+
common_utils_1.assert(this.messageBuffer.length === 0, 0x0e9 /* "messageBuffer is not empty on new connection" */);
|
|
220
|
+
this.emit("connect", connection, checkpointSequenceNumber !== undefined ?
|
|
221
|
+
this.lastObservedSeqNumber - this.lastSequenceNumber : undefined);
|
|
222
|
+
// If we got some initial ops, then we know the gap and call above fetched ops to fill it.
|
|
223
|
+
// Same is true for "write" mode even if we have no ops - we will get "join" own op very very soon.
|
|
224
|
+
// However if we are connecting as view-only, then there is no good signal to realize if client is behind.
|
|
225
|
+
// Thus we have to hit storage to see if any ops are there.
|
|
226
|
+
if (checkpointSequenceNumber !== undefined) {
|
|
227
|
+
// We know how far we are behind (roughly). If it's non-zero gap, fetch ops right away.
|
|
228
|
+
if (checkpointSequenceNumber > this.lastQueuedSequenceNumber) {
|
|
229
|
+
this.fetchMissingDeltas("AfterConnection");
|
|
230
|
+
}
|
|
231
|
+
// we do not know the gap, and we will not learn about it if socket is quite - have to ask.
|
|
232
|
+
}
|
|
233
|
+
else if (connection.mode === "read") {
|
|
234
|
+
this.fetchMissingDeltas("AfterReadConnection");
|
|
420
235
|
}
|
|
421
236
|
}
|
|
422
237
|
dispose() {
|
|
@@ -457,66 +272,16 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
|
|
|
457
272
|
// (which in most cases will happen when we are done processing cached ops)
|
|
458
273
|
if (cacheOnly) {
|
|
459
274
|
// fire and forget
|
|
460
|
-
this.fetchMissingDeltas("DocumentOpen"
|
|
275
|
+
this.fetchMissingDeltas("DocumentOpen");
|
|
461
276
|
}
|
|
462
277
|
}
|
|
463
278
|
// Ensure there is no need to call this.processPendingOps() at the end of boot sequence
|
|
464
279
|
common_utils_1.assert(this.fetchReason !== undefined || this.pending.length === 0, 0x269 /* "pending ops are not dropped" */);
|
|
465
280
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
claims: connection.claims,
|
|
469
|
-
clientId: connection.clientId,
|
|
470
|
-
existing: connection.existing,
|
|
471
|
-
checkpointSequenceNumber: connection.checkpointSequenceNumber,
|
|
472
|
-
get initialClients() { return connection.initialClients; },
|
|
473
|
-
maxMessageSize: connection.serviceConfiguration.maxMessageSize,
|
|
474
|
-
mode: connection.mode,
|
|
475
|
-
serviceConfiguration: connection.serviceConfiguration,
|
|
476
|
-
version: connection.version,
|
|
477
|
-
};
|
|
478
|
-
}
|
|
479
|
-
async connect(args) {
|
|
480
|
-
const connection = await this.connectCore(args);
|
|
481
|
-
return DeltaManager.detailsFromConnection(connection);
|
|
482
|
-
}
|
|
483
|
-
/**
|
|
484
|
-
* Start the connection. Any error should result in container being close.
|
|
485
|
-
* And report the error if it excape for any reason.
|
|
486
|
-
* @param args - The connection arguments
|
|
487
|
-
*/
|
|
488
|
-
triggerConnect(args) {
|
|
489
|
-
common_utils_1.assert(this.connection === undefined, 0x239 /* "called only in disconnected state" */);
|
|
490
|
-
if (this.reconnectMode !== ReconnectMode.Enabled) {
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
this.connectCore(args).catch((err) => {
|
|
494
|
-
// Errors are raised as "error" event and close container.
|
|
495
|
-
// Have a catch-all case in case we missed something
|
|
496
|
-
if (!this.closed) {
|
|
497
|
-
this.logger.sendErrorEvent({ eventName: "ConnectException" }, err);
|
|
498
|
-
}
|
|
499
|
-
});
|
|
500
|
-
}
|
|
501
|
-
async connectCore(args) {
|
|
502
|
-
var _a, _b, _c;
|
|
503
|
-
common_utils_1.assert(!this.closed, 0x26a /* "not closed" */);
|
|
504
|
-
if (this.connection !== undefined) {
|
|
505
|
-
return this.connection;
|
|
506
|
-
}
|
|
507
|
-
if (this.connectionP !== undefined) {
|
|
508
|
-
return this.connectionP;
|
|
509
|
-
}
|
|
281
|
+
connect(args) {
|
|
282
|
+
var _a;
|
|
510
283
|
const fetchOpsFromStorage = (_a = args.fetchOpsFromStorage) !== null && _a !== void 0 ? _a : true;
|
|
511
|
-
|
|
512
|
-
// if we have any non-acked ops from last connection, reconnect as "write".
|
|
513
|
-
// without that we would connect in view-only mode, which will result in immediate
|
|
514
|
-
// firing of "connected" event from Container and switch of current clientId (as tracked
|
|
515
|
-
// by all DDSes). This will make it impossible to figure out if ops actually made it through,
|
|
516
|
-
// so DDSes will immediately resubmit all pending ops, and some of them will be duplicates, corrupting document
|
|
517
|
-
if (this.shouldJoinWrite()) {
|
|
518
|
-
requestedMode = "write";
|
|
519
|
-
}
|
|
284
|
+
telemetry_utils_1.logIfFalse(this.handler !== undefined || !fetchOpsFromStorage, this.logger, "CantFetchWithoutBaseline"); // can't fetch if no baseline
|
|
520
285
|
// Note: There is race condition here.
|
|
521
286
|
// We want to issue request to storage as soon as possible, to
|
|
522
287
|
// reduce latency of becoming current, thus this code here.
|
|
@@ -526,214 +291,11 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
|
|
|
526
291
|
// own "join" message and realize any gap client has in ops.
|
|
527
292
|
// But for view-only connection, we have no such signal, and with no traffic
|
|
528
293
|
// on the wire, we might be always behind.
|
|
529
|
-
// See comment at the end of
|
|
530
|
-
telemetry_utils_1.logIfFalse(this.handler !== undefined || !fetchOpsFromStorage, this.logger, "CantFetchWithoutBaseline"); // can't fetch if no baseline
|
|
294
|
+
// See comment at the end of "connect" handler
|
|
531
295
|
if (fetchOpsFromStorage) {
|
|
532
|
-
this.fetchMissingDeltas(args.reason
|
|
533
|
-
}
|
|
534
|
-
const docService = this.serviceProvider();
|
|
535
|
-
if (docService === undefined) {
|
|
536
|
-
throw new Error("Container is not attached");
|
|
537
|
-
}
|
|
538
|
-
if (((_c = docService.policies) === null || _c === void 0 ? void 0 : _c.storageOnly) === true) {
|
|
539
|
-
const connection = new NoDeltaStream();
|
|
540
|
-
this.connectionP = Promise.resolve(connection); // to keep setupNewSuccessfulConnection happy
|
|
541
|
-
this.setupNewSuccessfulConnection(connection, "read");
|
|
542
|
-
return connection;
|
|
543
|
-
}
|
|
544
|
-
// The promise returned from connectCore will settle with a resolved connection or reject with error
|
|
545
|
-
const connectCore = async () => {
|
|
546
|
-
let connection;
|
|
547
|
-
let delayMs = InitialReconnectDelayInMs;
|
|
548
|
-
let connectRepeatCount = 0;
|
|
549
|
-
const connectStartTime = common_utils_1.performance.now();
|
|
550
|
-
let lastError;
|
|
551
|
-
// This loop will keep trying to connect until successful, with a delay between each iteration.
|
|
552
|
-
while (connection === undefined) {
|
|
553
|
-
if (this.closed) {
|
|
554
|
-
throw new Error("Attempting to connect a closed DeltaManager");
|
|
555
|
-
}
|
|
556
|
-
connectRepeatCount++;
|
|
557
|
-
try {
|
|
558
|
-
this.client.mode = requestedMode;
|
|
559
|
-
connection = await docService.connectToDeltaStream(this.client);
|
|
560
|
-
if (connection.disposed) {
|
|
561
|
-
// Nobody observed this connection, so drop it on the floor and retry.
|
|
562
|
-
this.logger.sendTelemetryEvent({ eventName: "ReceivedClosedConnection" });
|
|
563
|
-
connection = undefined;
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
catch (origError) {
|
|
567
|
-
if (typeof origError === "object" && origError !== null &&
|
|
568
|
-
(origError === null || origError === void 0 ? void 0 : origError.errorType) === driver_utils_1.DeltaStreamConnectionForbiddenError.errorType) {
|
|
569
|
-
connection = new NoDeltaStream();
|
|
570
|
-
requestedMode = "read";
|
|
571
|
-
break;
|
|
572
|
-
}
|
|
573
|
-
// Socket.io error when we connect to wrong socket, or hit some multiplexing bug
|
|
574
|
-
if (!driver_utils_1.canRetryOnError(origError)) {
|
|
575
|
-
const error = telemetry_utils_1.normalizeError(origError);
|
|
576
|
-
this.close(error);
|
|
577
|
-
throw error;
|
|
578
|
-
}
|
|
579
|
-
// Log error once - we get too many errors in logs when we are offline,
|
|
580
|
-
// and unfortunately there is no reliable way to detect that.
|
|
581
|
-
if (connectRepeatCount === 1) {
|
|
582
|
-
driver_utils_1.logNetworkFailure(this.logger, {
|
|
583
|
-
delay: delayMs,
|
|
584
|
-
eventName: "DeltaConnectionFailureToConnect",
|
|
585
|
-
duration: telemetry_utils_1.TelemetryLogger.formatTick(common_utils_1.performance.now() - connectStartTime),
|
|
586
|
-
}, origError);
|
|
587
|
-
}
|
|
588
|
-
lastError = origError;
|
|
589
|
-
const retryDelayFromError = driver_utils_1.getRetryDelayFromError(origError);
|
|
590
|
-
delayMs = retryDelayFromError !== null && retryDelayFromError !== void 0 ? retryDelayFromError : Math.min(delayMs * 2, MaxReconnectDelayInMs);
|
|
591
|
-
if (retryDelayFromError !== undefined) {
|
|
592
|
-
this.emitDelayInfo(this.deltaStreamDelayId, retryDelayFromError, origError);
|
|
593
|
-
}
|
|
594
|
-
await driver_utils_1.waitForConnectedState(delayMs);
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
// If we retried more than once, log an event about how long it took
|
|
598
|
-
if (connectRepeatCount > 1) {
|
|
599
|
-
this.logger.sendTelemetryEvent({
|
|
600
|
-
eventName: "MultipleDeltaConnectionFailures",
|
|
601
|
-
attempts: connectRepeatCount,
|
|
602
|
-
duration: telemetry_utils_1.TelemetryLogger.formatTick(common_utils_1.performance.now() - connectStartTime),
|
|
603
|
-
}, lastError);
|
|
604
|
-
}
|
|
605
|
-
this.setupNewSuccessfulConnection(connection, requestedMode);
|
|
606
|
-
return connection;
|
|
607
|
-
};
|
|
608
|
-
// This promise settles as soon as we know the outcome of the connection attempt
|
|
609
|
-
// Set it upfront, such that if connection is established (NoDeltaConnection) or rejected (bug in
|
|
610
|
-
// connectToDeltaStream() implementation - throwing exception vs. returning rejected promise) in
|
|
611
|
-
// synchronous way, we have this.connectionP setup for all the code to assert correctness of the flow.
|
|
612
|
-
const deferred = new common_utils_1.Deferred();
|
|
613
|
-
this.connectionP = deferred.promise;
|
|
614
|
-
// Regardless of how the connection attempt concludes, we'll clear the promise and remove the listener
|
|
615
|
-
// Reject the connection promise if the DeltaManager gets closed during connection
|
|
616
|
-
const cleanupAndReject = (error) => {
|
|
617
|
-
this.connectionP = undefined;
|
|
618
|
-
this.removeListener("closed", cleanupAndReject);
|
|
619
|
-
// This error came from some logic error in this file. Fail-fast to learn and fix the issue faster
|
|
620
|
-
this.close(error);
|
|
621
|
-
deferred.reject(error);
|
|
622
|
-
};
|
|
623
|
-
this.on("closed", cleanupAndReject);
|
|
624
|
-
// Attempt the connection
|
|
625
|
-
connectCore().then((connection) => {
|
|
626
|
-
this.removeListener("closed", cleanupAndReject);
|
|
627
|
-
deferred.resolve(connection);
|
|
628
|
-
}).catch(cleanupAndReject);
|
|
629
|
-
return this.connectionP;
|
|
630
|
-
}
|
|
631
|
-
flush() {
|
|
632
|
-
if (this.messageBuffer.length === 0) {
|
|
633
|
-
return;
|
|
634
|
-
}
|
|
635
|
-
// The prepareFlush event allows listeners to append metadata to the batch prior to submission.
|
|
636
|
-
this.emit("prepareSend", this.messageBuffer);
|
|
637
|
-
this._outbound.push(this.messageBuffer);
|
|
638
|
-
this.messageBuffer = [];
|
|
639
|
-
}
|
|
640
|
-
/**
|
|
641
|
-
* Submits the given delta returning the client sequence number for the message. Contents is the actual
|
|
642
|
-
* contents of the message. appData is optional metadata that can be attached to the op by the app.
|
|
643
|
-
*
|
|
644
|
-
* If batch is set to true then the submit will be batched - and as a result guaranteed to be ordered sequentially
|
|
645
|
-
* in the global sequencing space. The batch will be flushed either when flush is called or when a non-batched
|
|
646
|
-
* op is submitted.
|
|
647
|
-
*/
|
|
648
|
-
submit(type, contents, batch = false, metadata) {
|
|
649
|
-
// TODO need to fail if gets too large
|
|
650
|
-
// const serializedContent = JSON.stringify(this.messageBuffer);
|
|
651
|
-
// const maxOpSize = this.context.deltaManager.maxMessageSize;
|
|
652
|
-
var _a, _b;
|
|
653
|
-
if (this.readonly === true) {
|
|
654
|
-
common_utils_1.assert(this.readOnlyInfo.readonly === true, 0x1f0 /* "Unexpected mismatch in readonly" */);
|
|
655
|
-
const error = new container_utils_1.GenericError("deltaManagerReadonlySubmit", undefined /* error */, {
|
|
656
|
-
readonly: this.readOnlyInfo.readonly,
|
|
657
|
-
forcedReadonly: this.readOnlyInfo.forced,
|
|
658
|
-
readonlyPermissions: this.readOnlyInfo.permissions,
|
|
659
|
-
storageOnly: this.readOnlyInfo.storageOnly,
|
|
660
|
-
});
|
|
661
|
-
this.close(error);
|
|
662
|
-
return -1;
|
|
663
|
-
}
|
|
664
|
-
// reset clientSequenceNumber if we are using new clientId.
|
|
665
|
-
// we keep info about old connection as long as possible to be able to account for all non-acked ops
|
|
666
|
-
// that we pick up on next connection.
|
|
667
|
-
common_utils_1.assert(!!this.connection, 0x0e4 /* "Lost old connection!" */);
|
|
668
|
-
if (this.lastSubmittedClientId !== ((_a = this.connection) === null || _a === void 0 ? void 0 : _a.clientId)) {
|
|
669
|
-
this.lastSubmittedClientId = (_b = this.connection) === null || _b === void 0 ? void 0 : _b.clientId;
|
|
670
|
-
this.clientSequenceNumber = 0;
|
|
671
|
-
this.clientSequenceNumberObserved = 0;
|
|
672
|
-
}
|
|
673
|
-
// If connection is "read" or implicit "read" (got leave op for "write" connection),
|
|
674
|
-
// then op can't make it through - we will get a nack if op is sent.
|
|
675
|
-
// We can short-circuit this process.
|
|
676
|
-
// Note that we also want nacks to be rare and be treated as catastrophic failures.
|
|
677
|
-
// Be careful with reentrancy though - disconnected event should not be be raised in the
|
|
678
|
-
// middle of the current workflow, but rather on clean stack!
|
|
679
|
-
if (this.connectionMode === "read") {
|
|
680
|
-
if (!this.pendingReconnect) {
|
|
681
|
-
this.pendingReconnect = true;
|
|
682
|
-
Promise.resolve().then(async () => {
|
|
683
|
-
if (this.pendingReconnect) { // still valid?
|
|
684
|
-
return this.reconnectOnErrorCore("write", // connectionMode
|
|
685
|
-
"Switch to write");
|
|
686
|
-
}
|
|
687
|
-
})
|
|
688
|
-
.catch(() => { });
|
|
689
|
-
}
|
|
690
|
-
// Can return -1 here, but no other path does it (other than error path in Container),
|
|
691
|
-
// so it's better not to introduce new states.
|
|
692
|
-
return ++this.clientSequenceNumber;
|
|
693
|
-
}
|
|
694
|
-
const service = this.clientDetails.type === undefined || this.clientDetails.type === ""
|
|
695
|
-
? "unknown"
|
|
696
|
-
: this.clientDetails.type;
|
|
697
|
-
// Start adding trace for the op.
|
|
698
|
-
const traces = [
|
|
699
|
-
{
|
|
700
|
-
action: "start",
|
|
701
|
-
service,
|
|
702
|
-
timestamp: Date.now(),
|
|
703
|
-
}
|
|
704
|
-
];
|
|
705
|
-
const message = {
|
|
706
|
-
clientSequenceNumber: ++this.clientSequenceNumber,
|
|
707
|
-
contents: JSON.stringify(contents),
|
|
708
|
-
metadata,
|
|
709
|
-
referenceSequenceNumber: this.lastProcessedSequenceNumber,
|
|
710
|
-
traces,
|
|
711
|
-
type,
|
|
712
|
-
};
|
|
713
|
-
if (type === protocol_definitions_1.MessageType.NoOp) {
|
|
714
|
-
this.trailingNoopCount++;
|
|
715
|
-
}
|
|
716
|
-
else {
|
|
717
|
-
this.trailingNoopCount = 0;
|
|
718
|
-
}
|
|
719
|
-
this.emit("submitOp", message);
|
|
720
|
-
if (!batch) {
|
|
721
|
-
this.flush();
|
|
722
|
-
this.messageBuffer.push(message);
|
|
723
|
-
this.flush();
|
|
724
|
-
}
|
|
725
|
-
else {
|
|
726
|
-
this.messageBuffer.push(message);
|
|
727
|
-
}
|
|
728
|
-
return message.clientSequenceNumber;
|
|
729
|
-
}
|
|
730
|
-
submitSignal(content) {
|
|
731
|
-
if (this.connection !== undefined) {
|
|
732
|
-
this.connection.submitSignal(content);
|
|
733
|
-
}
|
|
734
|
-
else {
|
|
735
|
-
this.logger.sendErrorEvent({ eventName: "submitSignalDisconnected" });
|
|
296
|
+
this.fetchMissingDeltas(args.reason);
|
|
736
297
|
}
|
|
298
|
+
this.connectionManager.connect(args.mode);
|
|
737
299
|
}
|
|
738
300
|
async getDeltas(from, // inclusive
|
|
739
301
|
to, // exclusive
|
|
@@ -745,9 +307,6 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
|
|
|
745
307
|
if (this.deltaStorage === undefined) {
|
|
746
308
|
this.deltaStorage = await docService.connectToDeltaStorage();
|
|
747
309
|
}
|
|
748
|
-
common_utils_1.assert(this.closeAbortController.signal.onabort === null, 0x1e8 /* "reentrancy" */);
|
|
749
|
-
const controller = new abort_controller_1.default();
|
|
750
|
-
this.closeAbortController.signal.onabort = () => controller.abort();
|
|
751
310
|
let cancelFetch;
|
|
752
311
|
if (to !== undefined) {
|
|
753
312
|
const lastExpectedOp = to - 1; // make it inclusive!
|
|
@@ -755,7 +314,7 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
|
|
|
755
314
|
// received through delta stream. Validate that before moving forward.
|
|
756
315
|
if (this.lastQueuedSequenceNumber >= lastExpectedOp) {
|
|
757
316
|
this.logger.sendPerformanceEvent(Object.assign({ reason: this.fetchReason, eventName: "ExtraStorageCall", early: true, from,
|
|
758
|
-
to }, this.
|
|
317
|
+
to }, this.connectionManager.connectionVerboseProps));
|
|
759
318
|
return;
|
|
760
319
|
}
|
|
761
320
|
// Be prepared for the case where webSocket would receive the ops that we are trying to fill through
|
|
@@ -772,6 +331,7 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
|
|
|
772
331
|
// That said, if we have socket connection, make sure we got ops up to checkpointSequenceNumber!
|
|
773
332
|
cancelFetch = (op) => op.sequenceNumber >= this.lastObservedSeqNumber;
|
|
774
333
|
}
|
|
334
|
+
const controller = new abort_controller_1.default();
|
|
775
335
|
let opsFromFetch = false;
|
|
776
336
|
const opListener = (op) => {
|
|
777
337
|
common_utils_1.assert(op.sequenceNumber === this.lastQueuedSequenceNumber, 0x23a /* "seq#'s" */);
|
|
@@ -783,11 +343,13 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
|
|
|
783
343
|
this._inbound.off("push", opListener);
|
|
784
344
|
}
|
|
785
345
|
};
|
|
786
|
-
this._inbound.on("push", opListener);
|
|
787
346
|
try {
|
|
347
|
+
this._inbound.on("push", opListener);
|
|
348
|
+
common_utils_1.assert(this.closeAbortController.signal.onabort === null, 0x1e8 /* "reentrancy" */);
|
|
349
|
+
this.closeAbortController.signal.onabort = () => controller.abort();
|
|
788
350
|
const stream = this.deltaStorage.fetchMessages(from, // inclusive
|
|
789
351
|
to, // exclusive
|
|
790
|
-
controller.signal, cacheOnly);
|
|
352
|
+
controller.signal, cacheOnly, this.fetchReason);
|
|
791
353
|
// eslint-disable-next-line no-constant-condition
|
|
792
354
|
while (true) {
|
|
793
355
|
const result = await stream.read();
|
|
@@ -804,9 +366,9 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
|
|
|
804
366
|
}
|
|
805
367
|
}
|
|
806
368
|
finally {
|
|
807
|
-
common_utils_1.assert(!opsFromFetch, 0x289 /* "logic error" */);
|
|
808
369
|
this.closeAbortController.signal.onabort = null;
|
|
809
370
|
this._inbound.off("push", opListener);
|
|
371
|
+
common_utils_1.assert(!opsFromFetch, 0x289 /* "logic error" */);
|
|
810
372
|
}
|
|
811
373
|
}
|
|
812
374
|
/**
|
|
@@ -817,13 +379,9 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
|
|
|
817
379
|
return;
|
|
818
380
|
}
|
|
819
381
|
this.closed = true;
|
|
820
|
-
|
|
821
|
-
this._reconnectMode = ReconnectMode.Never;
|
|
382
|
+
this.connectionManager.dispose(error);
|
|
822
383
|
this.closeAbortController.abort();
|
|
823
|
-
// This raises "disconnect" event if we have active connection.
|
|
824
|
-
this.disconnectFromDeltaStream(error !== undefined ? `${error.message}` : "Container closed");
|
|
825
384
|
this._inbound.clear();
|
|
826
|
-
this._outbound.clear();
|
|
827
385
|
this._inboundSignal.clear();
|
|
828
386
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
829
387
|
this._inbound.pause();
|
|
@@ -831,10 +389,6 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
|
|
|
831
389
|
this._inboundSignal.pause();
|
|
832
390
|
// Drop pending messages - this will ensure catchUp() does not go into infinite loop
|
|
833
391
|
this.pending = [];
|
|
834
|
-
// Notify everyone we are in read-only state.
|
|
835
|
-
// Useful for data stores in case we hit some critical error,
|
|
836
|
-
// to switch to a mode where user edits are not accepted
|
|
837
|
-
this.set_readonlyPermissions(true);
|
|
838
392
|
// This needs to be the last thing we do (before removing listeners), as it causes
|
|
839
393
|
// Container to dispose context and break ability of data stores / runtime to "hear"
|
|
840
394
|
// from delta manager, including notification (above) about readonly state.
|
|
@@ -847,6 +401,20 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
|
|
|
847
401
|
this.timeTillThrottling = 0;
|
|
848
402
|
}
|
|
849
403
|
}
|
|
404
|
+
disconnectHandler(reason) {
|
|
405
|
+
if (this.messageBuffer.length > 0) {
|
|
406
|
+
// Behavior is not well defined here RE batches across connections / disconnect.
|
|
407
|
+
// DeltaManager overall policy - drop all ops on disconnection and rely on
|
|
408
|
+
// container runtime to deal with resubmitting any ops that did not make it through.
|
|
409
|
+
// So drop them, but also raise error event to look into details.
|
|
410
|
+
this.logger.sendErrorEvent({
|
|
411
|
+
eventName: "OpenBatchOnDisconnect",
|
|
412
|
+
length: this.messageBuffer.length,
|
|
413
|
+
});
|
|
414
|
+
this.messageBuffer.length = 0;
|
|
415
|
+
}
|
|
416
|
+
this.emit("disconnect", reason);
|
|
417
|
+
}
|
|
850
418
|
/**
|
|
851
419
|
* Emit info about a delay in service communication on account of throttling.
|
|
852
420
|
* @param id - Id of the connection that is delayed
|
|
@@ -862,177 +430,6 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
|
|
|
862
430
|
this.emit("throttled", throttlingWarning);
|
|
863
431
|
}
|
|
864
432
|
}
|
|
865
|
-
/**
|
|
866
|
-
* Once we've successfully gotten a connection, we need to set up state, attach event listeners, and process
|
|
867
|
-
* initial messages.
|
|
868
|
-
* @param connection - The newly established connection
|
|
869
|
-
*/
|
|
870
|
-
setupNewSuccessfulConnection(connection, requestedMode) {
|
|
871
|
-
// Old connection should have been cleaned up before establishing a new one
|
|
872
|
-
common_utils_1.assert(this.connection === undefined, 0x0e6 /* "old connection exists on new connection setup" */);
|
|
873
|
-
common_utils_1.assert(this.connectionP !== undefined || this.closed, 0x27f /* "reentrancy may result in incorrect behavior" */);
|
|
874
|
-
common_utils_1.assert(!connection.disposed, 0x28a /* "can't be disposed - Callers need to ensure that!" */);
|
|
875
|
-
this.connectionP = undefined;
|
|
876
|
-
this.connection = connection;
|
|
877
|
-
// Does information in scopes & mode matches?
|
|
878
|
-
// If we asked for "write" and got "read", then file is read-only
|
|
879
|
-
// But if we ask read, server can still give us write.
|
|
880
|
-
const readonly = !connection.claims.scopes.includes(protocol_definitions_1.ScopeType.DocWrite);
|
|
881
|
-
// This connection mode validation logic is moving to the driver layer in 0.44. These two asserts can be
|
|
882
|
-
// removed after those packages have released and become ubiquitous.
|
|
883
|
-
common_utils_1.assert(requestedMode === "read" || readonly === (this.connectionMode === "read"), 0x0e7 /* "claims/connectionMode mismatch" */);
|
|
884
|
-
common_utils_1.assert(!readonly || this.connectionMode === "read", 0x0e8 /* "readonly perf with write connection" */);
|
|
885
|
-
this.set_readonlyPermissions(readonly);
|
|
886
|
-
this.refreshDelayInfo(this.deltaStreamDelayId);
|
|
887
|
-
if (this.closed) {
|
|
888
|
-
// Raise proper events, Log telemetry event and close connection.
|
|
889
|
-
this.disconnectFromDeltaStream(`Disconnect on close`);
|
|
890
|
-
return;
|
|
891
|
-
}
|
|
892
|
-
// We cancel all ops on lost of connectivity, and rely on DDSes to resubmit them.
|
|
893
|
-
// Semantics are not well defined for batches (and they are broken right now on disconnects anyway),
|
|
894
|
-
// but it's safe to assume (until better design is put into place) that batches should not exist
|
|
895
|
-
// across multiple connections. Right now we assume runtime will not submit any ops in disconnected
|
|
896
|
-
// state. As requirements change, so should these checks.
|
|
897
|
-
common_utils_1.assert(this.messageBuffer.length === 0, 0x0e9 /* "messageBuffer is not empty on new connection" */);
|
|
898
|
-
this._outbound.resume();
|
|
899
|
-
connection.on("op", this.opHandler);
|
|
900
|
-
connection.on("signal", this.signalHandler);
|
|
901
|
-
connection.on("nack", this.nackHandler);
|
|
902
|
-
connection.on("disconnect", this.disconnectHandler);
|
|
903
|
-
connection.on("error", this.errorHandler);
|
|
904
|
-
connection.on("pong", this.pongHandler);
|
|
905
|
-
// Initial messages are always sorted. However, due to early op handler installed by drivers and appending those
|
|
906
|
-
// ops to initialMessages, resulting set is no longer sorted, which would result in client hitting storage to
|
|
907
|
-
// fill in gap. We will recover by cancelling this request once we process remaining ops, but it's a waste that
|
|
908
|
-
// we could avoid
|
|
909
|
-
const initialMessages = connection.initialMessages.sort((a, b) => a.sequenceNumber - b.sequenceNumber);
|
|
910
|
-
this.connectionStateProps = {
|
|
911
|
-
connectionLastQueuedSequenceNumber: this.lastQueuedSequenceNumber,
|
|
912
|
-
connectionLastObservedSeqNumber: this.lastObservedSeqNumber,
|
|
913
|
-
clientId: connection.clientId,
|
|
914
|
-
mode: connection.mode,
|
|
915
|
-
};
|
|
916
|
-
this._hasCheckpointSequenceNumber = false;
|
|
917
|
-
// Some storages may provide checkpointSequenceNumber to identify how far client is behind.
|
|
918
|
-
const checkpointSequenceNumber = connection.checkpointSequenceNumber;
|
|
919
|
-
if (checkpointSequenceNumber !== undefined) {
|
|
920
|
-
this._hasCheckpointSequenceNumber = true;
|
|
921
|
-
this.updateLatestKnownOpSeqNumber(checkpointSequenceNumber);
|
|
922
|
-
}
|
|
923
|
-
// Update knowledge of how far we are behind, before raising "connect" event
|
|
924
|
-
// This is duplication of what enqueueMessages() does, but we have to raise event before we get there,
|
|
925
|
-
// so duplicating update logic here as well.
|
|
926
|
-
const last = initialMessages.length > 0 ? initialMessages[initialMessages.length - 1].sequenceNumber : -1;
|
|
927
|
-
if (initialMessages.length > 0) {
|
|
928
|
-
this._hasCheckpointSequenceNumber = true;
|
|
929
|
-
this.updateLatestKnownOpSeqNumber(last);
|
|
930
|
-
}
|
|
931
|
-
// Notify of the connection
|
|
932
|
-
// WARNING: This has to happen before processInitialMessages() call below.
|
|
933
|
-
// If not, we may not update Container.pendingClientId in time before seeing our own join session op.
|
|
934
|
-
this.emit("connect", DeltaManager.detailsFromConnection(connection), this._hasCheckpointSequenceNumber ? this.lastObservedSeqNumber - this.lastSequenceNumber : undefined);
|
|
935
|
-
this.enqueueMessages(initialMessages, this.connectFirstConnection ? "InitialOps" : "ReconnectOps");
|
|
936
|
-
if (connection.initialSignals !== undefined) {
|
|
937
|
-
for (const signal of connection.initialSignals) {
|
|
938
|
-
this._inboundSignal.push(signal);
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
// If we got some initial ops, then we know the gap and call above fetched ops to fill it.
|
|
942
|
-
// Same is true for "write" mode even if we have no ops - we will get self "join" ops very very soon.
|
|
943
|
-
// However if we are connecting as view-only, then there is no good signal to realize if client is behind.
|
|
944
|
-
// Thus we have to hit storage to see if any ops are there.
|
|
945
|
-
if (initialMessages.length === 0) {
|
|
946
|
-
if (checkpointSequenceNumber !== undefined) {
|
|
947
|
-
// We know how far we are behind (roughly). If it's non-zero gap, fetch ops right away.
|
|
948
|
-
if (checkpointSequenceNumber > this.lastQueuedSequenceNumber) {
|
|
949
|
-
this.fetchMissingDeltas("AfterConnection", this.lastQueuedSequenceNumber);
|
|
950
|
-
}
|
|
951
|
-
// we do not know the gap, and we will not learn about it if socket is quite - have to ask.
|
|
952
|
-
}
|
|
953
|
-
else if (connection.mode === "read") {
|
|
954
|
-
this.fetchMissingDeltas("AfterReadConnection", this.lastQueuedSequenceNumber);
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
else {
|
|
958
|
-
this.connectionStateProps.connectionInitialOpsFrom = initialMessages[0].sequenceNumber;
|
|
959
|
-
this.connectionStateProps.connectionInitialOpsTo = last + 1;
|
|
960
|
-
}
|
|
961
|
-
this.connectFirstConnection = false;
|
|
962
|
-
}
|
|
963
|
-
/**
|
|
964
|
-
* Disconnect the current connection.
|
|
965
|
-
* @param reason - Text description of disconnect reason to emit with disconnect event
|
|
966
|
-
*/
|
|
967
|
-
disconnectFromDeltaStream(reason) {
|
|
968
|
-
this.pendingReconnect = false;
|
|
969
|
-
this.downgradedConnection = false;
|
|
970
|
-
if (this.connection === undefined) {
|
|
971
|
-
return false;
|
|
972
|
-
}
|
|
973
|
-
common_utils_1.assert(this.connectionP === undefined, 0x27b /* "reentrancy may result in incorrect behavior" */);
|
|
974
|
-
const connection = this.connection;
|
|
975
|
-
// Avoid any re-entrancy - clear object reference
|
|
976
|
-
this.connection = undefined;
|
|
977
|
-
// Remove listeners first so we don't try to retrigger this flow accidentally through reconnectOnError
|
|
978
|
-
connection.off("op", this.opHandler);
|
|
979
|
-
connection.off("signal", this.signalHandler);
|
|
980
|
-
connection.off("nack", this.nackHandler);
|
|
981
|
-
connection.off("disconnect", this.disconnectHandler);
|
|
982
|
-
connection.off("error", this.errorHandler);
|
|
983
|
-
connection.off("pong", this.pongHandler);
|
|
984
|
-
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
985
|
-
this._outbound.pause();
|
|
986
|
-
this._outbound.clear();
|
|
987
|
-
this.emit("disconnect", reason);
|
|
988
|
-
connection.dispose();
|
|
989
|
-
this.connectionStateProps = {};
|
|
990
|
-
return true;
|
|
991
|
-
}
|
|
992
|
-
/**
|
|
993
|
-
* Disconnect the current connection and reconnect.
|
|
994
|
-
* @param connection - The connection that wants to reconnect - no-op if it's different from this.connection
|
|
995
|
-
* @param requestedMode - Read or write
|
|
996
|
-
* @param error - Error reconnect information including whether or not to reconnect
|
|
997
|
-
* @returns A promise that resolves when the connection is reestablished or we stop trying
|
|
998
|
-
*/
|
|
999
|
-
async reconnectOnError(requestedMode, error) {
|
|
1000
|
-
return this.reconnectOnErrorCore(requestedMode, error.message, error);
|
|
1001
|
-
}
|
|
1002
|
-
/**
|
|
1003
|
-
* Disconnect the current connection and reconnect.
|
|
1004
|
-
* @param connection - The connection that wants to reconnect - no-op if it's different from this.connection
|
|
1005
|
-
* @param requestedMode - Read or write
|
|
1006
|
-
* @param error - Error reconnect information including whether or not to reconnect
|
|
1007
|
-
* @returns A promise that resolves when the connection is reestablished or we stop trying
|
|
1008
|
-
*/
|
|
1009
|
-
async reconnectOnErrorCore(requestedMode, disconnectMessage, error) {
|
|
1010
|
-
// We quite often get protocol errors before / after observing nack/disconnect
|
|
1011
|
-
// we do not want to run through same sequence twice.
|
|
1012
|
-
// If we're already disconnected/disconnecting it's not appropriate to call this again.
|
|
1013
|
-
common_utils_1.assert(this.connection !== undefined, 0x0eb /* "Missing connection for reconnect" */);
|
|
1014
|
-
this.disconnectFromDeltaStream(disconnectMessage);
|
|
1015
|
-
const canRetry = error !== undefined ? driver_utils_1.canRetryOnError(error) : true;
|
|
1016
|
-
// If reconnection is not an option, close the DeltaManager
|
|
1017
|
-
if (this.reconnectMode === ReconnectMode.Never || !canRetry) {
|
|
1018
|
-
// Do not raise container error if we are closing just because we lost connection.
|
|
1019
|
-
// Those errors (like IdleDisconnect) would show up in telemetry dashboards and
|
|
1020
|
-
// are very misleading, as first initial reaction - some logic is broken.
|
|
1021
|
-
this.close(canRetry ? undefined : error);
|
|
1022
|
-
}
|
|
1023
|
-
// If closed then we can't reconnect
|
|
1024
|
-
if (this.closed) {
|
|
1025
|
-
return;
|
|
1026
|
-
}
|
|
1027
|
-
if (this.reconnectMode === ReconnectMode.Enabled) {
|
|
1028
|
-
const delayMs = error !== undefined ? driver_utils_1.getRetryDelayFromError(error) : undefined;
|
|
1029
|
-
if (delayMs !== undefined) {
|
|
1030
|
-
this.emitDelayInfo(this.deltaStreamDelayId, delayMs, error);
|
|
1031
|
-
await driver_utils_1.waitForConnectedState(delayMs);
|
|
1032
|
-
}
|
|
1033
|
-
this.triggerConnect({ reason: "reconnect", mode: requestedMode, fetchOpsFromStorage: false });
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
433
|
// returns parts of message (in string format) that should never change for a given message.
|
|
1037
434
|
// Used for message comparison. It attempts to avoid comparing fields that potentially may differ.
|
|
1038
435
|
// for example, it's not clear if serverMetadata or timestamp property is a property of message or server state.
|
|
@@ -1044,7 +441,7 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
|
|
|
1044
441
|
return `${m.clientId}-${m.type}-${m.minimumSequenceNumber}-${m.referenceSequenceNumber}-${m.timestamp}`;
|
|
1045
442
|
}
|
|
1046
443
|
enqueueMessages(messages, reason, allowGaps = false) {
|
|
1047
|
-
var _a, _b
|
|
444
|
+
var _a, _b;
|
|
1048
445
|
if (this.handler === undefined) {
|
|
1049
446
|
// We did not setup handler yet.
|
|
1050
447
|
// This happens when we connect to web socket faster than we get attributes for container
|
|
@@ -1104,7 +501,7 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
|
|
|
1104
501
|
// correctly take into account pending ops.
|
|
1105
502
|
if (eventName !== undefined) {
|
|
1106
503
|
this.logger.sendPerformanceEvent(Object.assign({ eventName,
|
|
1107
|
-
reason, previousReason: this.prevEnqueueMessagesReason, from, to: last + 1, length: messages.length, fetchReason: this.fetchReason, duplicate: duplicate > 0 ? duplicate : undefined, initialGap: initialGap !== 0 ? initialGap : undefined, gap: gap > 0 ? gap : undefined, firstMissing, dmInitialSeqNumber: this.initialSequenceNumber }, this.
|
|
504
|
+
reason, previousReason: this.prevEnqueueMessagesReason, from, to: last + 1, length: messages.length, fetchReason: this.fetchReason, duplicate: duplicate > 0 ? duplicate : undefined, initialGap: initialGap !== 0 ? initialGap : undefined, gap: gap > 0 ? gap : undefined, firstMissing, dmInitialSeqNumber: this.initialSequenceNumber }, this.connectionManager.connectionVerboseProps));
|
|
1108
505
|
}
|
|
1109
506
|
}
|
|
1110
507
|
this.updateLatestKnownOpSeqNumber(messages[messages.length - 1].sequenceNumber);
|
|
@@ -1120,7 +517,7 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
|
|
|
1120
517
|
const message2 = this.comparableMessagePayload(message);
|
|
1121
518
|
if (message1 !== message2) {
|
|
1122
519
|
const error = new driver_utils_1.NonRetryableError("twoMessagesWithSameSeqNumAndDifferentPayload", undefined, driver_definitions_1.DriverErrorType.fileOverwrittenInStorage, {
|
|
1123
|
-
clientId:
|
|
520
|
+
clientId: this.connectionManager.clientId,
|
|
1124
521
|
sequenceNumber: message.sequenceNumber,
|
|
1125
522
|
message1,
|
|
1126
523
|
message2,
|
|
@@ -1131,7 +528,7 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
|
|
|
1131
528
|
}
|
|
1132
529
|
else if (message.sequenceNumber !== this.lastQueuedSequenceNumber + 1) {
|
|
1133
530
|
this.pending.push(message);
|
|
1134
|
-
this.fetchMissingDeltas(reason,
|
|
531
|
+
this.fetchMissingDeltas(reason, message.sequenceNumber);
|
|
1135
532
|
}
|
|
1136
533
|
else {
|
|
1137
534
|
this.lastQueuedSequenceNumber = message.sequenceNumber;
|
|
@@ -1145,57 +542,34 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
|
|
|
1145
542
|
this.prevEnqueueMessagesReason = this.pending.length > 0 ? "unknown" : reason;
|
|
1146
543
|
}
|
|
1147
544
|
processInboundMessage(message) {
|
|
1148
|
-
var _a, _b, _c;
|
|
1149
545
|
const startTime = Date.now();
|
|
1150
546
|
this.lastProcessedMessage = message;
|
|
1151
547
|
// All non-system messages are coming from some client, and should have clientId
|
|
1152
548
|
// System messages may have no clientId (but some do, like propose, noop, summarize)
|
|
1153
549
|
common_utils_1.assert(message.clientId !== undefined
|
|
1154
550
|
|| protocol_base_1.isSystemMessage(message), 0x0ed /* "non-system message have to have clientId" */);
|
|
1155
|
-
// if we have connection, and message is local, then we better treat is as local!
|
|
1156
|
-
common_utils_1.assert(this.connection === undefined
|
|
1157
|
-
|| this.connection.clientId !== message.clientId
|
|
1158
|
-
|| this.lastSubmittedClientId === message.clientId, 0x0ee /* "Not accounting local messages correctly" */);
|
|
1159
|
-
if (this.lastSubmittedClientId !== undefined && this.lastSubmittedClientId === message.clientId) {
|
|
1160
|
-
const clientSequenceNumber = message.clientSequenceNumber;
|
|
1161
|
-
common_utils_1.assert(this.clientSequenceNumberObserved < clientSequenceNumber, 0x0ef /* "client seq# not growing" */);
|
|
1162
|
-
common_utils_1.assert(clientSequenceNumber <= this.clientSequenceNumber, 0x0f0 /* "Incoming local client seq# > generated by this client" */);
|
|
1163
|
-
this.clientSequenceNumberObserved = clientSequenceNumber;
|
|
1164
|
-
}
|
|
1165
551
|
// TODO Remove after SPO picks up the latest build.
|
|
1166
552
|
if (typeof message.contents === "string"
|
|
1167
553
|
&& message.contents !== ""
|
|
1168
554
|
&& message.type !== protocol_definitions_1.MessageType.ClientLeave) {
|
|
1169
555
|
message.contents = JSON.parse(message.contents);
|
|
1170
556
|
}
|
|
1171
|
-
|
|
1172
|
-
const systemLeaveMessage = message;
|
|
1173
|
-
const clientId = JSON.parse(systemLeaveMessage.data);
|
|
1174
|
-
if (clientId === ((_a = this.connection) === null || _a === void 0 ? void 0 : _a.clientId)) {
|
|
1175
|
-
// We have been kicked out from quorum
|
|
1176
|
-
this.logger.sendPerformanceEvent({ eventName: "ReadConnectionTransition" });
|
|
1177
|
-
this.downgradedConnection = true;
|
|
1178
|
-
common_utils_1.assert(this.connectionMode === "read", 0x27c /* "effective connectionMode should be 'read' after downgrade" */);
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
557
|
+
this.connectionManager.beforeProcessingIncomingOp(message);
|
|
1181
558
|
// Add final ack trace.
|
|
1182
559
|
if (message.traces !== undefined && message.traces.length > 0) {
|
|
1183
|
-
const service = this.clientDetails.type === undefined || this.clientDetails.type === ""
|
|
1184
|
-
? "unknown"
|
|
1185
|
-
: this.clientDetails.type;
|
|
1186
560
|
message.traces.push({
|
|
1187
561
|
action: "end",
|
|
1188
|
-
service,
|
|
562
|
+
service: "client",
|
|
1189
563
|
timestamp: Date.now(),
|
|
1190
564
|
});
|
|
1191
565
|
}
|
|
1192
566
|
// Watch the minimum sequence number and be ready to update as needed
|
|
1193
567
|
if (this.minSequenceNumber > message.minimumSequenceNumber) {
|
|
1194
|
-
throw new container_utils_1.DataCorruptionError("msnMovesBackwards", Object.assign(Object.assign({}, extractLogSafeMessageProperties(message)), { clientId:
|
|
568
|
+
throw new container_utils_1.DataCorruptionError("msnMovesBackwards", Object.assign(Object.assign({}, extractLogSafeMessageProperties(message)), { clientId: this.connectionManager.clientId }));
|
|
1195
569
|
}
|
|
1196
570
|
this.minSequenceNumber = message.minimumSequenceNumber;
|
|
1197
571
|
if (message.sequenceNumber !== this.lastProcessedSequenceNumber + 1) {
|
|
1198
|
-
throw new container_utils_1.DataCorruptionError("nonSequentialSequenceNumber", Object.assign(Object.assign({}, extractLogSafeMessageProperties(message)), { clientId:
|
|
572
|
+
throw new container_utils_1.DataCorruptionError("nonSequentialSequenceNumber", Object.assign(Object.assign({}, extractLogSafeMessageProperties(message)), { clientId: this.connectionManager.clientId }));
|
|
1199
573
|
}
|
|
1200
574
|
this.lastProcessedSequenceNumber = message.sequenceNumber;
|
|
1201
575
|
// a bunch of code assumes that this is true
|
|
@@ -1217,15 +591,15 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
|
|
|
1217
591
|
/**
|
|
1218
592
|
* Retrieves the missing deltas between the given sequence numbers
|
|
1219
593
|
*/
|
|
1220
|
-
fetchMissingDeltas(reasonArg,
|
|
1221
|
-
this.fetchMissingDeltasCore(reasonArg, false /* cacheOnly */,
|
|
594
|
+
fetchMissingDeltas(reasonArg, to) {
|
|
595
|
+
this.fetchMissingDeltasCore(reasonArg, false /* cacheOnly */, to).catch((error) => {
|
|
1222
596
|
this.logger.sendErrorEvent({ eventName: "fetchMissingDeltasException" }, error);
|
|
1223
597
|
});
|
|
1224
598
|
}
|
|
1225
599
|
/**
|
|
1226
600
|
* Retrieves the missing deltas between the given sequence numbers
|
|
1227
601
|
*/
|
|
1228
|
-
async fetchMissingDeltasCore(reason, cacheOnly,
|
|
602
|
+
async fetchMissingDeltasCore(reason, cacheOnly, to) {
|
|
1229
603
|
var _a;
|
|
1230
604
|
// Exit out early if we're already fetching deltas
|
|
1231
605
|
if (this.fetchReason !== undefined) {
|
|
@@ -1237,12 +611,11 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
|
|
|
1237
611
|
}
|
|
1238
612
|
if (this.handler === undefined) {
|
|
1239
613
|
// We do not poses yet any information
|
|
1240
|
-
common_utils_1.assert(
|
|
614
|
+
common_utils_1.assert(this.lastQueuedSequenceNumber === 0, 0x26b /* "initial state" */);
|
|
1241
615
|
return;
|
|
1242
616
|
}
|
|
1243
617
|
try {
|
|
1244
|
-
|
|
1245
|
-
let from = lastKnowOp + 1;
|
|
618
|
+
let from = this.lastQueuedSequenceNumber + 1;
|
|
1246
619
|
const n = (_a = this.previouslyProcessedMessage) === null || _a === void 0 ? void 0 : _a.sequenceNumber;
|
|
1247
620
|
if (n !== undefined) {
|
|
1248
621
|
// If we already processed at least one op, then we have this.previouslyProcessedMessage populated
|
|
@@ -1250,7 +623,7 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
|
|
|
1250
623
|
// Knowing about this mechanism, we could ask for op we already observed to increase validation.
|
|
1251
624
|
// This is especially useful when coming out of offline mode or loading from
|
|
1252
625
|
// very old cached (by client / driver) snapshot.
|
|
1253
|
-
common_utils_1.assert(n ===
|
|
626
|
+
common_utils_1.assert(n === this.lastQueuedSequenceNumber, 0x0f2 /* "previouslyProcessedMessage" */);
|
|
1254
627
|
common_utils_1.assert(from > 1, 0x0f3 /* "not positive" */);
|
|
1255
628
|
from--;
|
|
1256
629
|
}
|
|
@@ -1298,7 +671,7 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
|
|
|
1298
671
|
// (the other 50%), and thus these errors below should be looked at even if code below results in
|
|
1299
672
|
// recovery.
|
|
1300
673
|
if (this.lastQueuedSequenceNumber < this.lastObservedSeqNumber) {
|
|
1301
|
-
this.fetchMissingDeltas("OpsBehind"
|
|
674
|
+
this.fetchMissingDeltas("OpsBehind");
|
|
1302
675
|
}
|
|
1303
676
|
}
|
|
1304
677
|
}
|