@fluidframework/test-utils 1.4.0-115997 → 2.0.0-dev-rc.1.0.0.224419
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +8 -7
- package/.mocharc.js +12 -0
- package/CHANGELOG.md +177 -0
- package/README.md +44 -14
- package/api-extractor-lint.json +4 -0
- package/api-extractor.json +2 -2
- package/api-report/test-utils.api.md +395 -0
- package/dist/DriverWrappers.d.ts +32 -0
- package/dist/DriverWrappers.d.ts.map +1 -0
- package/dist/DriverWrappers.js +57 -0
- package/dist/DriverWrappers.js.map +1 -0
- package/dist/TestConfigs.d.ts +10 -0
- package/dist/TestConfigs.d.ts.map +1 -0
- package/dist/TestConfigs.js +17 -0
- package/dist/TestConfigs.js.map +1 -0
- package/dist/TestSummaryUtils.d.ts +61 -0
- package/dist/TestSummaryUtils.d.ts.map +1 -0
- package/dist/TestSummaryUtils.js +135 -0
- package/dist/TestSummaryUtils.js.map +1 -0
- package/dist/containerUtils.d.ts +46 -0
- package/dist/containerUtils.d.ts.map +1 -0
- package/dist/containerUtils.js +85 -0
- package/dist/containerUtils.js.map +1 -0
- package/dist/index.d.ts +14 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +43 -19
- package/dist/index.js.map +1 -1
- package/dist/interfaces.d.ts +6 -0
- package/dist/interfaces.d.ts.map +1 -1
- package/dist/interfaces.js.map +1 -1
- package/dist/loaderContainerTracker.d.ts +39 -15
- package/dist/loaderContainerTracker.d.ts.map +1 -1
- package/dist/loaderContainerTracker.js +214 -86
- package/dist/loaderContainerTracker.js.map +1 -1
- package/dist/localCodeLoader.d.ts +9 -2
- package/dist/localCodeLoader.d.ts.map +1 -1
- package/dist/localCodeLoader.js +18 -17
- package/dist/localCodeLoader.js.map +1 -1
- package/dist/localLoader.d.ts +3 -2
- package/dist/localLoader.d.ts.map +1 -1
- package/dist/localLoader.js +2 -0
- package/dist/localLoader.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/retry.d.ts +1 -0
- package/dist/retry.d.ts.map +1 -1
- package/dist/retry.js +3 -2
- package/dist/retry.js.map +1 -1
- package/dist/test-utils-alpha.d.ts +305 -0
- package/dist/test-utils-beta.d.ts +202 -0
- package/dist/test-utils-public.d.ts +202 -0
- package/dist/test-utils-untrimmed.d.ts +1001 -0
- package/dist/testContainerRuntimeFactory.d.ts +4 -2
- package/dist/testContainerRuntimeFactory.d.ts.map +1 -1
- package/dist/testContainerRuntimeFactory.js +67 -12
- package/dist/testContainerRuntimeFactory.js.map +1 -1
- package/dist/testContainerRuntimeFactoryWithDefaultDataStore.d.ts +23 -0
- package/dist/testContainerRuntimeFactoryWithDefaultDataStore.d.ts.map +1 -0
- package/dist/testContainerRuntimeFactoryWithDefaultDataStore.js +32 -0
- package/dist/testContainerRuntimeFactoryWithDefaultDataStore.js.map +1 -0
- package/dist/testFluidObject.d.ts +30 -12
- package/dist/testFluidObject.d.ts.map +1 -1
- package/dist/testFluidObject.js +74 -43
- package/dist/testFluidObject.js.map +1 -1
- package/dist/testObjectProvider.d.ts +298 -41
- package/dist/testObjectProvider.d.ts.map +1 -1
- package/dist/testObjectProvider.js +377 -61
- package/dist/testObjectProvider.js.map +1 -1
- package/dist/timeoutUtils.d.ts +42 -0
- package/dist/timeoutUtils.d.ts.map +1 -1
- package/dist/timeoutUtils.js +142 -9
- package/dist/timeoutUtils.js.map +1 -1
- package/dist/tsdoc-metadata.json +11 -0
- package/lib/test-utils-alpha.d.ts +305 -0
- package/lib/test-utils-beta.d.ts +202 -0
- package/lib/test-utils-public.d.ts +202 -0
- package/lib/test-utils-untrimmed.d.ts +1001 -0
- package/package.json +106 -72
- package/prettier.config.cjs +8 -0
- package/src/DriverWrappers.ts +83 -0
- package/src/TestConfigs.ts +17 -0
- package/src/TestSummaryUtils.ts +210 -0
- package/src/containerUtils.ts +90 -0
- package/src/index.ts +47 -9
- package/src/interfaces.ts +16 -7
- package/src/loaderContainerTracker.ts +754 -534
- package/src/localCodeLoader.ts +87 -86
- package/src/localLoader.ts +27 -26
- package/src/packageVersion.ts +1 -1
- package/src/retry.ts +33 -26
- package/src/testContainerRuntimeFactory.ts +152 -59
- package/src/testContainerRuntimeFactoryWithDefaultDataStore.ts +61 -0
- package/src/testFluidObject.ts +190 -147
- package/src/testObjectProvider.ts +931 -372
- package/src/timeoutUtils.ts +226 -37
- package/tsconfig.json +9 -12
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LoaderContainerTracker = void 0;
|
|
2
4
|
/*!
|
|
3
5
|
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
|
4
6
|
* Licensed under the MIT License.
|
|
5
7
|
*/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
8
|
+
const core_utils_1 = require("@fluidframework/core-utils");
|
|
9
|
+
const container_loader_1 = require("@fluidframework/container-loader");
|
|
10
|
+
const driver_utils_1 = require("@fluidframework/driver-utils");
|
|
9
11
|
const protocol_definitions_1 = require("@fluidframework/protocol-definitions");
|
|
12
|
+
const containerUtils_1 = require("./containerUtils");
|
|
10
13
|
const debug_1 = require("./debug");
|
|
14
|
+
const timeoutUtils_1 = require("./timeoutUtils");
|
|
11
15
|
const debugOp = debug_1.debug.extend("ops");
|
|
12
16
|
const debugWait = debug_1.debug.extend("wait");
|
|
17
|
+
/**
|
|
18
|
+
* @alpha
|
|
19
|
+
*/
|
|
13
20
|
class LoaderContainerTracker {
|
|
14
21
|
constructor(syncSummarizerClients = false) {
|
|
15
22
|
this.syncSummarizerClients = syncSummarizerClients;
|
|
@@ -42,14 +49,28 @@ class LoaderContainerTracker {
|
|
|
42
49
|
* @param container - container to add
|
|
43
50
|
*/
|
|
44
51
|
addContainer(container) {
|
|
45
|
-
// ignore summarizer
|
|
46
|
-
if (!container.deltaManager.clientDetails.capabilities.interactive && !this.syncSummarizerClients) {
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
52
|
// don't add container that is already tracked
|
|
50
53
|
if (this.containers.has(container)) {
|
|
51
54
|
return;
|
|
52
55
|
}
|
|
56
|
+
const containerWithClone = container;
|
|
57
|
+
// back-compat: Check for undefined because this function was added recently and older containers won't have it.
|
|
58
|
+
if (containerWithClone.clone !== undefined) {
|
|
59
|
+
const patch = (fn) => {
|
|
60
|
+
const boundFn = fn.bind(containerWithClone);
|
|
61
|
+
return async (...args) => {
|
|
62
|
+
const newContainer = await boundFn(...args);
|
|
63
|
+
this.addContainer(newContainer);
|
|
64
|
+
return newContainer;
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
containerWithClone.clone = patch(containerWithClone.clone);
|
|
68
|
+
}
|
|
69
|
+
// ignore summarizer
|
|
70
|
+
if (!container.deltaManager.clientDetails.capabilities.interactive &&
|
|
71
|
+
!this.syncSummarizerClients) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
53
74
|
const record = {
|
|
54
75
|
index: this.containers.size,
|
|
55
76
|
paused: false,
|
|
@@ -72,7 +93,7 @@ class LoaderContainerTracker {
|
|
|
72
93
|
trackTrailingNoOps(container, record) {
|
|
73
94
|
container.deltaManager.outbound.on("op", (messages) => {
|
|
74
95
|
for (const msg of messages) {
|
|
75
|
-
if (
|
|
96
|
+
if ((0, driver_utils_1.canBeCoalescedByService)(msg)) {
|
|
76
97
|
// Track the NoOp that was sent.
|
|
77
98
|
if (record.trailingNoOps === 0) {
|
|
78
99
|
// record the starting sequence number of the trailing no ops if we haven't been tracking yet.
|
|
@@ -88,15 +109,15 @@ class LoaderContainerTracker {
|
|
|
88
109
|
});
|
|
89
110
|
container.deltaManager.inbound.on("push", (message) => {
|
|
90
111
|
// Received the no op back, update the record if we are tracking
|
|
91
|
-
if (message
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
112
|
+
if ((0, driver_utils_1.canBeCoalescedByService)(message) &&
|
|
113
|
+
message.clientId === container.clientId &&
|
|
114
|
+
record.trailingNoOps !== 0 &&
|
|
115
|
+
record.startTrailingNoOps <= message.clientSequenceNumber) {
|
|
95
116
|
// NoOp might have coalesced and skipped ahead some sequence number
|
|
96
117
|
// update the record and skip ahead as well
|
|
97
118
|
const oldStartTrailingNoOps = record.startTrailingNoOps;
|
|
98
119
|
record.startTrailingNoOps = message.clientSequenceNumber + 1;
|
|
99
|
-
record.trailingNoOps -=
|
|
120
|
+
record.trailingNoOps -= record.startTrailingNoOps - oldStartTrailingNoOps;
|
|
100
121
|
}
|
|
101
122
|
});
|
|
102
123
|
container.on("disconnected", () => {
|
|
@@ -124,25 +145,34 @@ class LoaderContainerTracker {
|
|
|
124
145
|
}
|
|
125
146
|
/**
|
|
126
147
|
* Make sure all the tracked containers are synchronized.
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* -
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
148
|
+
*
|
|
149
|
+
* No isDirty (non-readonly) containers
|
|
150
|
+
* No extra clientId in quorum of any container that is not tracked and still opened.
|
|
151
|
+
* - i.e. no pending Join/Leave message.
|
|
152
|
+
* No unresolved proposal (minSeqNum \>= lastProposalSeqNum)
|
|
153
|
+
* lastSequenceNumber of all container is the same
|
|
154
|
+
* clientSequenceNumberObserved is the same as clientSequenceNumber sent
|
|
155
|
+
* - this overlaps with !isDirty, but include task scheduler ops.
|
|
156
|
+
* - Trailing NoOp is tracked and don't count as pending ops.
|
|
157
|
+
*
|
|
158
|
+
* Containers that are already pause will resume process and paused again once
|
|
159
|
+
* everything is synchronized. Containers that aren't paused will remain unpaused when this
|
|
160
|
+
* function returns.
|
|
135
161
|
*/
|
|
136
162
|
async ensureSynchronized(...containers) {
|
|
137
163
|
const resumed = this.resumeProcessing(...containers);
|
|
138
|
-
let waitingSequenceNumberSynchronized
|
|
164
|
+
let waitingSequenceNumberSynchronized;
|
|
139
165
|
// eslint-disable-next-line no-constant-condition
|
|
140
166
|
while (true) {
|
|
167
|
+
// yield a turn to allow side effect of resuming or the ops we just processed execute before we check
|
|
168
|
+
await new Promise((resolve) => {
|
|
169
|
+
setTimeout(resolve, 0);
|
|
170
|
+
});
|
|
141
171
|
const containersToApply = this.getContainers(containers);
|
|
142
172
|
if (containersToApply.length === 0) {
|
|
143
173
|
break;
|
|
144
174
|
}
|
|
145
|
-
// Ignore readonly dirty containers, because it can't sent
|
|
175
|
+
// Ignore readonly dirty containers, because it can't sent ops and nothing can be done about it being dirty
|
|
146
176
|
const dirtyContainers = containersToApply.filter((c) => {
|
|
147
177
|
const { deltaManager, isDirty } = c;
|
|
148
178
|
return deltaManager.readOnlyInfo.readonly !== true && isDirty;
|
|
@@ -151,37 +181,46 @@ class LoaderContainerTracker {
|
|
|
151
181
|
// Wait for all the leave messages
|
|
152
182
|
const pendingClients = this.getPendingClients(containersToApply);
|
|
153
183
|
if (pendingClients.length === 0) {
|
|
154
|
-
|
|
184
|
+
const needSync = this.needSequenceNumberSynchronize(containersToApply);
|
|
185
|
+
if (needSync === undefined) {
|
|
155
186
|
// done, we are in sync
|
|
156
187
|
break;
|
|
157
188
|
}
|
|
158
|
-
if (
|
|
159
|
-
//
|
|
160
|
-
waitingSequenceNumberSynchronized =
|
|
161
|
-
debugWait(
|
|
162
|
-
await this.waitForAnyInboundOps(containersToApply);
|
|
189
|
+
if (waitingSequenceNumberSynchronized !== needSync.reason) {
|
|
190
|
+
// Don't repeat writing to console if it is the same reason
|
|
191
|
+
waitingSequenceNumberSynchronized = needSync.reason;
|
|
192
|
+
debugWait(needSync.message);
|
|
163
193
|
}
|
|
194
|
+
// Wait for one inbounds ops which might change the state of things
|
|
195
|
+
await (0, timeoutUtils_1.timeoutAwait)(this.waitForAnyInboundOps(containersToApply), {
|
|
196
|
+
errorMsg: `Timeout on ${needSync.message}`,
|
|
197
|
+
});
|
|
164
198
|
}
|
|
165
199
|
else {
|
|
166
|
-
waitingSequenceNumberSynchronized =
|
|
167
|
-
await this.waitForPendingClients(pendingClients)
|
|
200
|
+
waitingSequenceNumberSynchronized = undefined;
|
|
201
|
+
await (0, timeoutUtils_1.timeoutAwait)(this.waitForPendingClients(pendingClients), {
|
|
202
|
+
errorMsg: "Timeout on waiting for pending join or leave op",
|
|
203
|
+
});
|
|
168
204
|
}
|
|
169
205
|
}
|
|
170
206
|
else {
|
|
171
207
|
// Wait for all the containers to be saved
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
208
|
+
debugWait(`Waiting container to be saved ${this.containerIndexStrings(dirtyContainers)}`);
|
|
209
|
+
waitingSequenceNumberSynchronized = undefined;
|
|
210
|
+
await Promise.all(dirtyContainers.map(async (c) => Promise.race([
|
|
211
|
+
(0, timeoutUtils_1.timeoutPromise)((resolve) => c.once("saved", () => resolve()), {
|
|
212
|
+
errorMsg: "Timeout on waiting a container to be saved",
|
|
213
|
+
}),
|
|
214
|
+
new Promise((resolve) => c.once("closed", resolve)),
|
|
215
|
+
])));
|
|
177
216
|
}
|
|
178
|
-
// yield a turn to allow side effect of the ops we just processed execute before we check again
|
|
179
|
-
await new Promise((resolve) => { setTimeout(resolve, 0); });
|
|
180
217
|
}
|
|
181
218
|
// Pause all container that was resumed
|
|
182
219
|
// don't call pause if resumed is empty and pause everything, which is not what we want
|
|
183
220
|
if (resumed.length !== 0) {
|
|
184
|
-
await this.pauseProcessing(...resumed)
|
|
221
|
+
await (0, timeoutUtils_1.timeoutAwait)(this.pauseProcessing(...resumed), {
|
|
222
|
+
errorMsg: "Timeout on waiting for pausing all resumed containers",
|
|
223
|
+
});
|
|
185
224
|
}
|
|
186
225
|
debugWait("Synchronized");
|
|
187
226
|
}
|
|
@@ -202,7 +241,8 @@ class LoaderContainerTracker {
|
|
|
202
241
|
const quorum = container.getQuorum();
|
|
203
242
|
quorum.getMembers().forEach((client, clientId) => {
|
|
204
243
|
// ignore summarizer
|
|
205
|
-
if (!client.client.details.capabilities.interactive &&
|
|
244
|
+
if (!client.client.details.capabilities.interactive &&
|
|
245
|
+
!this.syncSummarizerClients) {
|
|
206
246
|
return;
|
|
207
247
|
}
|
|
208
248
|
if (!openedClientId.includes(clientId)) {
|
|
@@ -221,42 +261,62 @@ class LoaderContainerTracker {
|
|
|
221
261
|
*
|
|
222
262
|
* @param containersToApply - the set of containers to check
|
|
223
263
|
*/
|
|
224
|
-
|
|
264
|
+
needSequenceNumberSynchronize(containersToApply) {
|
|
265
|
+
// If there is a pending proposal, wait for it to be accepted
|
|
266
|
+
const minSeqNum = containersToApply[0].deltaManager.minimumSequenceNumber;
|
|
267
|
+
if (minSeqNum < this.lastProposalSeqNum) {
|
|
268
|
+
return {
|
|
269
|
+
reason: "Proposal",
|
|
270
|
+
message: `waiting for MSN to advance to proposal at sequence number ${this.lastProposalSeqNum}`,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
225
273
|
// clientSequenceNumber check detects ops in flight, both on the wire and in the outbound queue
|
|
226
274
|
// We need both client sequence number and isDirty check because:
|
|
227
275
|
// - Currently isDirty flag ignores ops for task scheduler, so we need the client sequence number check
|
|
228
276
|
// - But isDirty flags include ops during forceReadonly and disconnected, because we don't submit
|
|
229
277
|
// the ops in the first place, clientSequenceNumber is not assigned
|
|
230
|
-
const
|
|
278
|
+
const containerWithInflightOps = containersToApply.filter((container) => {
|
|
231
279
|
if (container.deltaManager.readOnlyInfo.readonly === true) {
|
|
232
280
|
// Ignore readonly container. the clientSeqNum and clientSeqNumObserved might be out of sync
|
|
233
281
|
// because we transition to readonly when outbound is not empty or the in transit op got lost
|
|
234
|
-
return
|
|
282
|
+
return false;
|
|
235
283
|
}
|
|
236
284
|
// Note that in read only mode, the op won't be submitted
|
|
237
285
|
let deltaManager = container.deltaManager;
|
|
238
286
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
239
287
|
const { trailingNoOps } = this.containers.get(container);
|
|
240
|
-
// Back-compat: clientSequenceNumber
|
|
288
|
+
// Back-compat: lastSubmittedClientId/clientSequenceNumber/clientSequenceNumberObserved moved to ConnectionManager in 0.53
|
|
241
289
|
if (!("clientSequenceNumber" in deltaManager)) {
|
|
242
290
|
deltaManager = deltaManager.connectionManager;
|
|
243
291
|
}
|
|
244
|
-
(0,
|
|
245
|
-
(0,
|
|
246
|
-
|
|
247
|
-
|
|
292
|
+
(0, core_utils_1.assert)("clientSequenceNumber" in deltaManager, "no clientSequenceNumber");
|
|
293
|
+
(0, core_utils_1.assert)("clientSequenceNumberObserved" in deltaManager, "no clientSequenceNumber");
|
|
294
|
+
// If last submittedClientId isn't the current clientId, then we haven't send any ops
|
|
295
|
+
return (deltaManager.lastSubmittedClientId === container.clientId &&
|
|
296
|
+
deltaManager.clientSequenceNumber !==
|
|
297
|
+
deltaManager.clientSequenceNumberObserved + trailingNoOps);
|
|
248
298
|
});
|
|
249
|
-
if (
|
|
250
|
-
return
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
// There is an unresolved proposal
|
|
255
|
-
return false;
|
|
299
|
+
if (containerWithInflightOps.length !== 0) {
|
|
300
|
+
return {
|
|
301
|
+
reason: "InflightOps",
|
|
302
|
+
message: `waiting for containers with inflight ops: ${this.containerIndexStrings(containerWithInflightOps)}`,
|
|
303
|
+
};
|
|
256
304
|
}
|
|
257
305
|
// Check to see if all the container has process the same number of ops.
|
|
258
|
-
const
|
|
259
|
-
|
|
306
|
+
const maxSeqNum = Math.max(...containersToApply.map((c) => c.deltaManager.lastSequenceNumber));
|
|
307
|
+
const containerWithPendingIncoming = containersToApply.filter((c) => c.deltaManager.lastSequenceNumber !== maxSeqNum);
|
|
308
|
+
if (containerWithPendingIncoming.length !== 0) {
|
|
309
|
+
return {
|
|
310
|
+
reason: "Pending",
|
|
311
|
+
message: `waiting for containers with pending incoming ops up to sequence number ${maxSeqNum}: ${this.containerIndexStrings(containerWithPendingIncoming)}`,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
return undefined;
|
|
315
|
+
}
|
|
316
|
+
containerIndexStrings(containers) {
|
|
317
|
+
return containers.map(
|
|
318
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
319
|
+
(c) => this.containers.get(c).index);
|
|
260
320
|
}
|
|
261
321
|
/**
|
|
262
322
|
* Utility to wait for any clientId in quorum that is NOT associated with any container we
|
|
@@ -267,7 +327,7 @@ class LoaderContainerTracker {
|
|
|
267
327
|
* @param containersToApply - the set of containers to wait for any inbound ops for
|
|
268
328
|
*/
|
|
269
329
|
async waitForPendingClients(pendingClients) {
|
|
270
|
-
const unconnectedClients = Array.from(this.containers.keys()).filter((c) => !c.closed &&
|
|
330
|
+
const unconnectedClients = Array.from(this.containers.keys()).filter((c) => !c.closed && c.connectionState !== container_loader_1.ConnectionState.Connected);
|
|
271
331
|
return Promise.all(pendingClients.map(async ([container, pendingClientId]) => {
|
|
272
332
|
return new Promise((resolve) => {
|
|
273
333
|
const cleanup = () => {
|
|
@@ -318,7 +378,8 @@ class LoaderContainerTracker {
|
|
|
318
378
|
const containersToApply = this.getContainers(containers);
|
|
319
379
|
for (const container of containersToApply) {
|
|
320
380
|
const record = this.containers.get(container);
|
|
321
|
-
|
|
381
|
+
(0, core_utils_1.assert)(record?.pauseP === undefined, "Cannot resume container while pausing is in progress");
|
|
382
|
+
if (record?.paused === true) {
|
|
322
383
|
debugWait(`${record.index}: container resumed`);
|
|
323
384
|
container.deltaManager.inbound.resume();
|
|
324
385
|
container.deltaManager.outbound.resume();
|
|
@@ -331,25 +392,86 @@ class LoaderContainerTracker {
|
|
|
331
392
|
/**
|
|
332
393
|
* Pause all queue activities on the containers given, or all tracked containers
|
|
333
394
|
* Any containers given that is not tracked will be ignored.
|
|
395
|
+
*
|
|
396
|
+
* When a container is paused, it is assumed that we want fine grain control over op
|
|
397
|
+
* sequencing. This function will prepare the container and force it into write mode to
|
|
398
|
+
* avoid missing join messages or change the sequence of event when switching from read to
|
|
399
|
+
* write mode.
|
|
334
400
|
*/
|
|
335
401
|
async pauseProcessing(...containers) {
|
|
336
|
-
const
|
|
402
|
+
const waitP = [];
|
|
337
403
|
const containersToApply = this.getContainers(containers);
|
|
338
404
|
for (const container of containersToApply) {
|
|
339
405
|
const record = this.containers.get(container);
|
|
340
406
|
if (record !== undefined && !record.paused) {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
record.
|
|
407
|
+
if (record.pauseP === undefined) {
|
|
408
|
+
record.pauseP = this.pauseContainer(container, record);
|
|
409
|
+
}
|
|
410
|
+
waitP.push(record.pauseP);
|
|
345
411
|
}
|
|
346
412
|
}
|
|
347
|
-
await Promise.all(
|
|
413
|
+
await Promise.all(waitP);
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* When a container is paused, it is assumed that we want fine grain control over op
|
|
417
|
+
* sequencing. This function will prepare the container and force it into write mode to
|
|
418
|
+
* avoid missing join messages or change the sequence of event when switching from read to
|
|
419
|
+
* write mode.
|
|
420
|
+
*
|
|
421
|
+
* @param container - the container to pause
|
|
422
|
+
* @param record - the record for the container
|
|
423
|
+
*/
|
|
424
|
+
async pauseContainer(container, record) {
|
|
425
|
+
debugWait(`${record.index}: pausing container`);
|
|
426
|
+
(0, core_utils_1.assert)(!container.deltaManager.outbound.paused, "Container should not be paused yet");
|
|
427
|
+
(0, core_utils_1.assert)(!container.deltaManager.inbound.paused, "Container should not be paused yet");
|
|
428
|
+
// Pause outbound
|
|
429
|
+
debugWait(`${record.index}: pausing container outbound queues`);
|
|
430
|
+
await container.deltaManager.outbound.pause();
|
|
431
|
+
// Ensure the container is connected first.
|
|
432
|
+
if (container.connectionState !== container_loader_1.ConnectionState.Connected) {
|
|
433
|
+
debugWait(`${record.index}: Wait for container connection`);
|
|
434
|
+
await (0, containerUtils_1.waitForContainerConnection)(container);
|
|
435
|
+
}
|
|
436
|
+
// Check if the container is in write mode
|
|
437
|
+
if (!container.deltaManager.active) {
|
|
438
|
+
let proposalP;
|
|
439
|
+
if (container.deltaManager.outbound.idle) {
|
|
440
|
+
// Need to generate an op to force write mode
|
|
441
|
+
debugWait(`${record.index}: container force write connection`);
|
|
442
|
+
const maybeContainer = container;
|
|
443
|
+
const codeProposal = maybeContainer.getLoadedCodeDetails
|
|
444
|
+
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
445
|
+
container.getLoadedCodeDetails()
|
|
446
|
+
: container.chaincodePackage;
|
|
447
|
+
proposalP = container.proposeCodeDetails(codeProposal);
|
|
448
|
+
}
|
|
449
|
+
// Wait for nack
|
|
450
|
+
debugWait(`${record.index}: Wait for container disconnect`);
|
|
451
|
+
container.deltaManager.outbound.resume();
|
|
452
|
+
await new Promise((resolve) => container.once("disconnected", resolve));
|
|
453
|
+
const accepted = proposalP ? await proposalP : false;
|
|
454
|
+
(0, core_utils_1.assert)(!accepted, "A proposal in read mode should be rejected");
|
|
455
|
+
await container.deltaManager.outbound.pause();
|
|
456
|
+
// Ensure the container is reconnect.
|
|
457
|
+
if (container.connectionState !== container_loader_1.ConnectionState.Connected) {
|
|
458
|
+
debugWait(`${record.index}: Wait for container reconnection`);
|
|
459
|
+
await (0, containerUtils_1.waitForContainerConnection)(container);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
debugWait(`${record.index}: pausing container inbound queues`);
|
|
463
|
+
// Pause inbound
|
|
464
|
+
await container.deltaManager.inbound.pause();
|
|
465
|
+
debugWait(`${record.index}: container paused`);
|
|
466
|
+
record.pauseP = undefined;
|
|
467
|
+
record.paused = true;
|
|
348
468
|
}
|
|
349
469
|
/**
|
|
350
470
|
* Pause all queue activities on all tracked containers, and resume only
|
|
351
471
|
* inbound to process ops until it is idle. All queues are left in the paused state
|
|
352
|
-
* after the function
|
|
472
|
+
* after the function.
|
|
473
|
+
*
|
|
474
|
+
* Pausing will switch the container to write mode. See `pauseProcessing`
|
|
353
475
|
*/
|
|
354
476
|
async processIncoming(...containers) {
|
|
355
477
|
return this.processQueue(containers, (container) => container.deltaManager.inbound);
|
|
@@ -357,7 +479,9 @@ class LoaderContainerTracker {
|
|
|
357
479
|
/**
|
|
358
480
|
* Pause all queue activities on all tracked containers, and resume only
|
|
359
481
|
* outbound to process ops until it is idle. All queues are left in the paused state
|
|
360
|
-
* after the function
|
|
482
|
+
* after the function.
|
|
483
|
+
*
|
|
484
|
+
* Pausing will switch the container to write mode. See `pauseProcessing`
|
|
361
485
|
*/
|
|
362
486
|
async processOutgoing(...containers) {
|
|
363
487
|
return this.processQueue(containers, (container) => container.deltaManager.outbound);
|
|
@@ -372,6 +496,7 @@ class LoaderContainerTracker {
|
|
|
372
496
|
const inflightTracker = new Map();
|
|
373
497
|
const cleanup = [];
|
|
374
498
|
for (const container of containersToApply) {
|
|
499
|
+
(0, core_utils_1.assert)(container.deltaManager.active, "Container should be connected in write mode already");
|
|
375
500
|
const queue = getQueue(container);
|
|
376
501
|
// track the outgoing ops (if any) to make sure they make the round trip to at least to the same client
|
|
377
502
|
// to make sure they are sequenced.
|
|
@@ -381,7 +506,9 @@ class LoaderContainerTracker {
|
|
|
381
506
|
}
|
|
382
507
|
while (resumed.some((queue) => !queue.idle)) {
|
|
383
508
|
debugWait("Wait until queue is idle");
|
|
384
|
-
await new Promise((resolve) => {
|
|
509
|
+
await new Promise((resolve) => {
|
|
510
|
+
setTimeout(resolve, 0);
|
|
511
|
+
});
|
|
385
512
|
}
|
|
386
513
|
// Make sure all the op that we sent out are acked first
|
|
387
514
|
// This is no op if we are processing incoming
|
|
@@ -405,15 +532,15 @@ class LoaderContainerTracker {
|
|
|
405
532
|
setupInOutTracker(container, inflightTracker) {
|
|
406
533
|
const outHandler = (messages) => {
|
|
407
534
|
for (const message of messages) {
|
|
408
|
-
if (
|
|
535
|
+
if (!(0, driver_utils_1.canBeCoalescedByService)(message)) {
|
|
409
536
|
inflightTracker.set(container, message.clientSequenceNumber);
|
|
410
537
|
}
|
|
411
538
|
}
|
|
412
539
|
};
|
|
413
540
|
const inHandler = (message) => {
|
|
414
|
-
if (message
|
|
415
|
-
|
|
416
|
-
|
|
541
|
+
if (!(0, driver_utils_1.canBeCoalescedByService)(message) &&
|
|
542
|
+
message.clientId === container.clientId &&
|
|
543
|
+
inflightTracker.get(container) === message.clientSequenceNumber) {
|
|
417
544
|
inflightTracker.delete(container);
|
|
418
545
|
}
|
|
419
546
|
};
|
|
@@ -430,7 +557,6 @@ class LoaderContainerTracker {
|
|
|
430
557
|
setupTrace(container, index) {
|
|
431
558
|
if (debugOp.enabled) {
|
|
432
559
|
const getContentsString = (type, msgContents) => {
|
|
433
|
-
var _a, _b;
|
|
434
560
|
try {
|
|
435
561
|
if (type !== protocol_definitions_1.MessageType.Operation) {
|
|
436
562
|
if (typeof msgContents === "string") {
|
|
@@ -441,14 +567,13 @@ class LoaderContainerTracker {
|
|
|
441
567
|
let address = "";
|
|
442
568
|
// contents comes in the wire as JSON string ("push" event)
|
|
443
569
|
// But already parsed when apply ("op" event)
|
|
444
|
-
let contents = typeof msgContents === "string" ?
|
|
445
|
-
JSON.parse(msgContents) : msgContents;
|
|
570
|
+
let contents = typeof msgContents === "string" ? JSON.parse(msgContents) : msgContents;
|
|
446
571
|
while (contents !== undefined && contents !== null) {
|
|
447
|
-
if (
|
|
572
|
+
if (contents.contents?.address !== undefined) {
|
|
448
573
|
address += `/${contents.contents.address}`;
|
|
449
574
|
contents = contents.contents.contents;
|
|
450
575
|
}
|
|
451
|
-
else if (
|
|
576
|
+
else if (contents.content?.address !== undefined) {
|
|
452
577
|
address += `/${contents.content.address}`;
|
|
453
578
|
contents = contents.content.contents;
|
|
454
579
|
}
|
|
@@ -468,19 +593,22 @@ class LoaderContainerTracker {
|
|
|
468
593
|
debugOp(`${index}: ADD: clientId: ${container.clientId}`);
|
|
469
594
|
container.deltaManager.outbound.on("op", (messages) => {
|
|
470
595
|
for (const msg of messages) {
|
|
471
|
-
debugOp(`${index}: OUT: `
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
596
|
+
debugOp(`${index}: OUT: ` +
|
|
597
|
+
`cli: ${msg.clientSequenceNumber.toString().padStart(3)} ` +
|
|
598
|
+
`rsq: ${msg.referenceSequenceNumber.toString().padStart(3)} ` +
|
|
599
|
+
`${msg.type} ${getContentsString(msg.type, msg.contents)}`);
|
|
475
600
|
}
|
|
476
601
|
});
|
|
477
602
|
const getInboundHandler = (type) => {
|
|
478
603
|
return (msg) => {
|
|
479
|
-
const clientSeq = msg.clientId === container.clientId
|
|
480
|
-
`cli: ${msg.clientSequenceNumber.toString().padStart(3)}`
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
604
|
+
const clientSeq = msg.clientId === container.clientId
|
|
605
|
+
? `cli: ${msg.clientSequenceNumber.toString().padStart(3)}`
|
|
606
|
+
: " ";
|
|
607
|
+
debugOp(`${index}: ${type}: seq: ${msg.sequenceNumber.toString().padStart(3)} ` +
|
|
608
|
+
`${clientSeq} min: ${msg.minimumSequenceNumber
|
|
609
|
+
.toString()
|
|
610
|
+
.padStart(3)} ` +
|
|
611
|
+
`${msg.type} ${getContentsString(msg.type, msg.contents)}`);
|
|
484
612
|
};
|
|
485
613
|
};
|
|
486
614
|
container.deltaManager.inbound.on("push", getInboundHandler("IN "));
|