@fluidframework/test-utils 2.0.0-dev.3.1.0.125672 → 2.0.0-dev.4.2.0.153917
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/CHANGELOG.md +5 -0
- package/dist/TestConfigs.d.ts.map +1 -1
- package/dist/TestConfigs.js +0 -2
- package/dist/TestConfigs.js.map +1 -1
- package/dist/TestSummaryUtils.d.ts +14 -3
- package/dist/TestSummaryUtils.d.ts.map +1 -1
- package/dist/TestSummaryUtils.js +24 -16
- package/dist/TestSummaryUtils.js.map +1 -1
- package/dist/containerUtils.d.ts +10 -19
- package/dist/containerUtils.d.ts.map +1 -1
- package/dist/containerUtils.js +12 -25
- package/dist/containerUtils.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -3
- package/dist/index.js.map +1 -1
- package/dist/loaderContainerTracker.d.ts +30 -15
- package/dist/loaderContainerTracker.d.ts.map +1 -1
- package/dist/loaderContainerTracker.js +138 -58
- package/dist/loaderContainerTracker.js.map +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/dist/testObjectProvider.d.ts +7 -1
- package/dist/testObjectProvider.d.ts.map +1 -1
- package/dist/testObjectProvider.js +44 -15
- package/dist/testObjectProvider.js.map +1 -1
- package/dist/timeoutUtils.d.ts +0 -2
- package/dist/timeoutUtils.d.ts.map +1 -1
- package/dist/timeoutUtils.js +1 -7
- package/dist/timeoutUtils.js.map +1 -1
- package/package.json +62 -63
- package/src/TestConfigs.ts +0 -2
- package/src/TestSummaryUtils.ts +23 -30
- package/src/containerUtils.ts +12 -25
- package/src/index.ts +2 -7
- package/src/loaderContainerTracker.ts +174 -72
- package/src/packageVersion.ts +1 -1
- package/src/testObjectProvider.ts +72 -16
- package/src/timeoutUtils.ts +0 -7
package/src/TestSummaryUtils.ts
CHANGED
|
@@ -29,10 +29,11 @@ import { timeoutAwait } from "./timeoutUtils";
|
|
|
29
29
|
const summarizerClientType = "summarizer";
|
|
30
30
|
|
|
31
31
|
async function createSummarizerCore(
|
|
32
|
-
|
|
32
|
+
container: IContainer,
|
|
33
33
|
loader: IHostLoader,
|
|
34
34
|
summaryVersion?: string,
|
|
35
35
|
) {
|
|
36
|
+
const absoluteUrl = await container.getAbsoluteUrl("");
|
|
36
37
|
if (absoluteUrl === undefined) {
|
|
37
38
|
throw new Error("URL could not be resolved");
|
|
38
39
|
}
|
|
@@ -52,10 +53,12 @@ async function createSummarizerCore(
|
|
|
52
53
|
const summarizerContainer = await loader.resolve(request);
|
|
53
54
|
await waitForContainerConnection(summarizerContainer);
|
|
54
55
|
|
|
55
|
-
const fluidObject
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
const fluidObject: FluidObject<ISummarizer> | undefined = summarizerContainer.getEntryPoint
|
|
57
|
+
? await summarizerContainer.getEntryPoint?.()
|
|
58
|
+
: await requestFluidObject<FluidObject<ISummarizer>>(summarizerContainer, {
|
|
59
|
+
url: "_summarizer",
|
|
60
|
+
});
|
|
61
|
+
if (fluidObject?.ISummarizer === undefined) {
|
|
59
62
|
throw new Error("Fluid object does not implement ISummarizer");
|
|
60
63
|
}
|
|
61
64
|
|
|
@@ -74,6 +77,11 @@ const defaultSummaryOptions: ISummaryRuntimeOptions = {
|
|
|
74
77
|
},
|
|
75
78
|
};
|
|
76
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Creates a summarizer client from the given container and data store factory, and returns the summarizer client's
|
|
82
|
+
* IContainer and ISummarizer.
|
|
83
|
+
* The ISummarizer can be used to generate on-demand summaries. The IContainer can be used to fetch data stores, etc.
|
|
84
|
+
*/
|
|
77
85
|
export async function createSummarizerFromFactory(
|
|
78
86
|
provider: ITestObjectProvider,
|
|
79
87
|
container: IContainer,
|
|
@@ -81,7 +89,8 @@ export async function createSummarizerFromFactory(
|
|
|
81
89
|
summaryVersion?: string,
|
|
82
90
|
containerRuntimeFactoryType = ContainerRuntimeFactoryWithDefaultDataStore,
|
|
83
91
|
registryEntries?: NamedFluidDataStoreRegistryEntries,
|
|
84
|
-
|
|
92
|
+
logger?: ITelemetryBaseLogger,
|
|
93
|
+
): Promise<{ container: IContainer; summarizer: ISummarizer }> {
|
|
85
94
|
const innerRequestHandler = async (request: IRequest, runtime: IContainerRuntimeBase) =>
|
|
86
95
|
runtime.IFluidHandleContext.resolveHandle(request);
|
|
87
96
|
const runtimeFactory = new containerRuntimeFactoryType(
|
|
@@ -94,11 +103,15 @@ export async function createSummarizerFromFactory(
|
|
|
94
103
|
|
|
95
104
|
const loader = provider.createLoader([[provider.defaultCodeDetails, runtimeFactory]], {
|
|
96
105
|
configProvider: mockConfigProvider(),
|
|
106
|
+
logger,
|
|
97
107
|
});
|
|
98
|
-
|
|
99
|
-
return (await createSummarizerCore(absoluteUrl, loader, summaryVersion)).summarizer;
|
|
108
|
+
return createSummarizerCore(container, loader, summaryVersion);
|
|
100
109
|
}
|
|
101
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Creates a summarizer client from the given container and returns the summarizer client's IContainer and ISummarizer.
|
|
113
|
+
* The ISummarizer can be used to generate on-demand summaries. The IContainer can be used to fetch data stores, etc.
|
|
114
|
+
*/
|
|
102
115
|
export async function createSummarizer(
|
|
103
116
|
provider: ITestObjectProvider,
|
|
104
117
|
container: IContainer,
|
|
@@ -106,27 +119,6 @@ export async function createSummarizer(
|
|
|
106
119
|
gcOptions?: IGCRuntimeOptions,
|
|
107
120
|
configProvider: IConfigProviderBase = mockConfigProvider(),
|
|
108
121
|
logger?: ITelemetryBaseLogger,
|
|
109
|
-
): Promise<ISummarizer> {
|
|
110
|
-
const absoluteUrl = await container.getAbsoluteUrl("");
|
|
111
|
-
return (
|
|
112
|
-
await createSummarizerWithContainer(
|
|
113
|
-
provider,
|
|
114
|
-
absoluteUrl,
|
|
115
|
-
summaryVersion,
|
|
116
|
-
gcOptions,
|
|
117
|
-
configProvider,
|
|
118
|
-
logger,
|
|
119
|
-
)
|
|
120
|
-
).summarizer;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export async function createSummarizerWithContainer(
|
|
124
|
-
provider: ITestObjectProvider,
|
|
125
|
-
absoluteUrl: string | undefined,
|
|
126
|
-
summaryVersion?: string,
|
|
127
|
-
gcOptions?: IGCRuntimeOptions,
|
|
128
|
-
configProvider: IConfigProviderBase = mockConfigProvider(),
|
|
129
|
-
logger?: ITelemetryBaseLogger,
|
|
130
122
|
): Promise<{ container: IContainer; summarizer: ISummarizer }> {
|
|
131
123
|
const testContainerConfig: ITestContainerConfig = {
|
|
132
124
|
runtimeOptions: {
|
|
@@ -136,8 +128,9 @@ export async function createSummarizerWithContainer(
|
|
|
136
128
|
loaderProps: { configProvider, logger },
|
|
137
129
|
};
|
|
138
130
|
const loader = provider.makeTestLoader(testContainerConfig);
|
|
139
|
-
return createSummarizerCore(
|
|
131
|
+
return createSummarizerCore(container, loader, summaryVersion);
|
|
140
132
|
}
|
|
133
|
+
|
|
141
134
|
/**
|
|
142
135
|
* Summarizes on demand and returns the summary tree, the version number and the reference sequence number of the
|
|
143
136
|
* submitted summary.
|
package/src/containerUtils.ts
CHANGED
|
@@ -4,23 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { IContainer } from "@fluidframework/container-definitions";
|
|
7
|
-
import { ConnectionState
|
|
7
|
+
import { ConnectionState } from "@fluidframework/container-loader";
|
|
8
8
|
import { PromiseExecutor, timeoutPromise, TimeoutWithError } from "./timeoutUtils";
|
|
9
9
|
|
|
10
|
-
/**
|
|
11
|
-
* Waits for the specified container to emit a 'connected' event.
|
|
12
|
-
*
|
|
13
|
-
* @deprecated Use waitForContainerConnection instead.
|
|
14
|
-
* Note that an upcoming release will change the default parameters on that function to:
|
|
15
|
-
* - failOnContainerClose = true
|
|
16
|
-
* - timeoutOptions.durationMs = 1s
|
|
17
|
-
*/
|
|
18
|
-
export async function ensureContainerConnected(container: Container): Promise<void> {
|
|
19
|
-
if (!container.connected) {
|
|
20
|
-
return timeoutPromise((resolve) => container.once("connected", () => resolve()));
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
10
|
/**
|
|
25
11
|
* Utility function to wait for the specified Container to be in Connected state.
|
|
26
12
|
* If the Container is already connected, the Promise returns immediately; otherwise it resolves when the Container emits
|
|
@@ -30,20 +16,21 @@ export async function ensureContainerConnected(container: Container): Promise<vo
|
|
|
30
16
|
* @param container - The container to wait for.
|
|
31
17
|
* @param failOnContainerClose - If true, the returned Promise will be rejected if the container emits a 'closed' event
|
|
32
18
|
* before a 'connected' event.
|
|
33
|
-
* Defaults to
|
|
19
|
+
* Defaults to true.
|
|
34
20
|
* @param timeoutOptions - Options related to the behavior of the timeout.
|
|
35
|
-
* If
|
|
36
|
-
* 'connected' (or 'closed, if
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* timeoutOptions.durationMs
|
|
21
|
+
* If provided, the returned Promise will reject if the container hasn't emitted relevant events in timeoutOptions.durationMs.
|
|
22
|
+
* If not provided, the Promise will wait indefinitely for the Container to emit its 'connected' (or 'closed', if
|
|
23
|
+
* failOnContainerClose === true) event.
|
|
24
|
+
*
|
|
25
|
+
* @returns A Promise that either:
|
|
26
|
+
* - Resolves when the specified container emits a 'connected' event (or immediately if the Container is already connected).
|
|
27
|
+
* - Rejects if failOnContainerClose === true and the container emits a 'closed' event before a 'connected' event.
|
|
28
|
+
* - Rejects after timeoutOptions.durationMs if timeoutOptions !== undefined and the container does not emit relevant
|
|
29
|
+
* events, within that timeframe.
|
|
43
30
|
*/
|
|
44
31
|
export async function waitForContainerConnection(
|
|
45
32
|
container: IContainer,
|
|
46
|
-
failOnContainerClose: boolean =
|
|
33
|
+
failOnContainerClose: boolean = true,
|
|
47
34
|
timeoutOptions?: TimeoutWithError,
|
|
48
35
|
): Promise<void> {
|
|
49
36
|
if (container.connectionState !== ConnectionState.Connected) {
|
package/src/index.ts
CHANGED
|
@@ -29,12 +29,7 @@ export {
|
|
|
29
29
|
ITestObjectProvider,
|
|
30
30
|
TestObjectProvider,
|
|
31
31
|
} from "./testObjectProvider";
|
|
32
|
-
export {
|
|
33
|
-
createSummarizer,
|
|
34
|
-
createSummarizerFromFactory,
|
|
35
|
-
createSummarizerWithContainer,
|
|
36
|
-
summarizeNow,
|
|
37
|
-
} from "./TestSummaryUtils";
|
|
32
|
+
export { createSummarizer, createSummarizerFromFactory, summarizeNow } from "./TestSummaryUtils";
|
|
38
33
|
export {
|
|
39
34
|
defaultTimeoutDurationMs,
|
|
40
35
|
timeoutAwait,
|
|
@@ -42,4 +37,4 @@ export {
|
|
|
42
37
|
TimeoutWithError,
|
|
43
38
|
TimeoutWithValue,
|
|
44
39
|
} from "./timeoutUtils";
|
|
45
|
-
export {
|
|
40
|
+
export { waitForContainerConnection } from "./containerUtils";
|
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { assert } from "@fluidframework/common-utils";
|
|
6
6
|
import { IContainer, IDeltaQueue, IHostLoader } from "@fluidframework/container-definitions";
|
|
7
|
-
import {
|
|
7
|
+
import { ConnectionState } from "@fluidframework/container-loader";
|
|
8
8
|
import { canBeCoalescedByService } from "@fluidframework/driver-utils";
|
|
9
9
|
import {
|
|
10
10
|
IDocumentMessage,
|
|
11
11
|
ISequencedDocumentMessage,
|
|
12
12
|
MessageType,
|
|
13
13
|
} from "@fluidframework/protocol-definitions";
|
|
14
|
+
import { waitForContainerConnection } from "./containerUtils";
|
|
14
15
|
import { debug } from "./debug";
|
|
15
16
|
import { IOpProcessingController } from "./testObjectProvider";
|
|
16
17
|
import { timeoutAwait, timeoutPromise } from "./timeoutUtils";
|
|
@@ -24,6 +25,7 @@ interface ContainerRecord {
|
|
|
24
25
|
|
|
25
26
|
// LoaderContainerTracker paused state
|
|
26
27
|
paused: boolean;
|
|
28
|
+
pauseP?: Promise<void>; // promise for for the pause that is in progress
|
|
27
29
|
|
|
28
30
|
// Tracking trailing no-op that may or may be acked by the server so we can discount them
|
|
29
31
|
// See issue #5629
|
|
@@ -123,7 +125,7 @@ export class LoaderContainerTracker implements IOpProcessingController {
|
|
|
123
125
|
// Received the no op back, update the record if we are tracking
|
|
124
126
|
if (
|
|
125
127
|
canBeCoalescedByService(message) &&
|
|
126
|
-
message.clientId ===
|
|
128
|
+
message.clientId === container.clientId &&
|
|
127
129
|
record.trailingNoOps !== 0 &&
|
|
128
130
|
record.startTrailingNoOps <= message.clientSequenceNumber
|
|
129
131
|
) {
|
|
@@ -162,57 +164,51 @@ export class LoaderContainerTracker implements IOpProcessingController {
|
|
|
162
164
|
// REVIEW: do we need to unpatch the loaders?
|
|
163
165
|
}
|
|
164
166
|
|
|
165
|
-
/**
|
|
166
|
-
* Ensure all tracked containers are synchronized
|
|
167
|
-
*/
|
|
168
|
-
public async ensureSynchronized(...containers: IContainer[]): Promise<void> {
|
|
169
|
-
await this.processSynchronized(undefined, ...containers);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
167
|
/**
|
|
173
168
|
* Ensure all tracked containers are synchronized with a time limit
|
|
169
|
+
*
|
|
170
|
+
* @deprecated - this method is equivalent to @see {@link LoaderContainerTracker.ensureSynchronized}, please configure the test timeout instead
|
|
174
171
|
*/
|
|
175
172
|
public async ensureSynchronizedWithTimeout?(
|
|
176
173
|
timeoutDuration: number | undefined,
|
|
177
174
|
...containers: IContainer[]
|
|
178
175
|
) {
|
|
179
|
-
await this.
|
|
176
|
+
await this.ensureSynchronized(...containers);
|
|
180
177
|
}
|
|
181
178
|
|
|
182
179
|
/**
|
|
183
180
|
* Make sure all the tracked containers are synchronized.
|
|
184
181
|
*
|
|
185
182
|
* No isDirty (non-readonly) containers
|
|
186
|
-
*
|
|
187
183
|
* No extra clientId in quorum of any container that is not tracked and still opened.
|
|
188
|
-
*
|
|
189
184
|
* - i.e. no pending Join/Leave message.
|
|
190
|
-
*
|
|
191
185
|
* No unresolved proposal (minSeqNum \>= lastProposalSeqNum)
|
|
192
|
-
*
|
|
193
186
|
* lastSequenceNumber of all container is the same
|
|
194
|
-
*
|
|
195
187
|
* clientSequenceNumberObserved is the same as clientSequenceNumber sent
|
|
196
|
-
*
|
|
197
188
|
* - this overlaps with !isDirty, but include task scheduler ops.
|
|
198
|
-
*
|
|
199
189
|
* - Trailing NoOp is tracked and don't count as pending ops.
|
|
190
|
+
*
|
|
191
|
+
* Containers that are already pause will resume process and paused again once
|
|
192
|
+
* everything is synchronized. Containers that aren't paused will remain unpaused when this
|
|
193
|
+
* function returns.
|
|
200
194
|
*/
|
|
201
|
-
|
|
202
|
-
timeoutDuration: number | undefined,
|
|
203
|
-
...containers: IContainer[]
|
|
204
|
-
) {
|
|
195
|
+
public async ensureSynchronized(...containers: IContainer[]): Promise<void> {
|
|
205
196
|
const resumed = this.resumeProcessing(...containers);
|
|
206
197
|
|
|
207
|
-
let waitingSequenceNumberSynchronized
|
|
198
|
+
let waitingSequenceNumberSynchronized: string | undefined;
|
|
208
199
|
// eslint-disable-next-line no-constant-condition
|
|
209
200
|
while (true) {
|
|
201
|
+
// yield a turn to allow side effect of resuming or the ops we just processed execute before we check
|
|
202
|
+
await new Promise<void>((resolve) => {
|
|
203
|
+
setTimeout(resolve, 0);
|
|
204
|
+
});
|
|
205
|
+
|
|
210
206
|
const containersToApply = this.getContainers(containers);
|
|
211
207
|
if (containersToApply.length === 0) {
|
|
212
208
|
break;
|
|
213
209
|
}
|
|
214
210
|
|
|
215
|
-
// Ignore readonly dirty containers, because it can't sent
|
|
211
|
+
// Ignore readonly dirty containers, because it can't sent ops and nothing can be done about it being dirty
|
|
216
212
|
const dirtyContainers = containersToApply.filter((c) => {
|
|
217
213
|
const { deltaManager, isDirty } = c;
|
|
218
214
|
return deltaManager.readOnlyInfo.readonly !== true && isDirty;
|
|
@@ -221,20 +217,22 @@ export class LoaderContainerTracker implements IOpProcessingController {
|
|
|
221
217
|
// Wait for all the leave messages
|
|
222
218
|
const pendingClients = this.getPendingClients(containersToApply);
|
|
223
219
|
if (pendingClients.length === 0) {
|
|
224
|
-
|
|
220
|
+
const needSync = this.needSequenceNumberSynchronize(containersToApply);
|
|
221
|
+
if (needSync === undefined) {
|
|
225
222
|
// done, we are in sync
|
|
226
223
|
break;
|
|
227
224
|
}
|
|
228
|
-
if (
|
|
229
|
-
//
|
|
230
|
-
waitingSequenceNumberSynchronized =
|
|
231
|
-
debugWait(
|
|
232
|
-
await timeoutAwait(this.waitForAnyInboundOps(containersToApply), {
|
|
233
|
-
errorMsg: "Timeout on waiting for sequence number synchronized",
|
|
234
|
-
});
|
|
225
|
+
if (waitingSequenceNumberSynchronized !== needSync.reason) {
|
|
226
|
+
// Don't repeat writing to console if it is the same reason
|
|
227
|
+
waitingSequenceNumberSynchronized = needSync.reason;
|
|
228
|
+
debugWait(needSync.message);
|
|
235
229
|
}
|
|
230
|
+
// Wait for one inbounds ops which might change the state of things
|
|
231
|
+
await timeoutAwait(this.waitForAnyInboundOps(containersToApply), {
|
|
232
|
+
errorMsg: `Timeout on ${needSync.message}`,
|
|
233
|
+
});
|
|
236
234
|
} else {
|
|
237
|
-
waitingSequenceNumberSynchronized =
|
|
235
|
+
waitingSequenceNumberSynchronized = undefined;
|
|
238
236
|
await timeoutAwait(this.waitForPendingClients(pendingClients), {
|
|
239
237
|
errorMsg: "Timeout on waiting for pending join or leave op",
|
|
240
238
|
});
|
|
@@ -242,12 +240,9 @@ export class LoaderContainerTracker implements IOpProcessingController {
|
|
|
242
240
|
} else {
|
|
243
241
|
// Wait for all the containers to be saved
|
|
244
242
|
debugWait(
|
|
245
|
-
`Waiting container to be saved ${
|
|
246
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
247
|
-
(c) => this.containers.get(c)!.index,
|
|
248
|
-
)}`,
|
|
243
|
+
`Waiting container to be saved ${this.containerIndexStrings(dirtyContainers)}`,
|
|
249
244
|
);
|
|
250
|
-
waitingSequenceNumberSynchronized =
|
|
245
|
+
waitingSequenceNumberSynchronized = undefined;
|
|
251
246
|
await Promise.all(
|
|
252
247
|
dirtyContainers.map(async (c) =>
|
|
253
248
|
Promise.race([
|
|
@@ -259,11 +254,6 @@ export class LoaderContainerTracker implements IOpProcessingController {
|
|
|
259
254
|
),
|
|
260
255
|
);
|
|
261
256
|
}
|
|
262
|
-
|
|
263
|
-
// yield a turn to allow side effect of the ops we just processed execute before we check again
|
|
264
|
-
await new Promise<void>((resolve) => {
|
|
265
|
-
setTimeout(resolve, 0);
|
|
266
|
-
});
|
|
267
257
|
}
|
|
268
258
|
|
|
269
259
|
// Pause all container that was resumed
|
|
@@ -287,9 +277,7 @@ export class LoaderContainerTracker implements IOpProcessingController {
|
|
|
287
277
|
// All the clientId we track should be a superset of the quorum, otherwise, we are missing
|
|
288
278
|
// leave messages
|
|
289
279
|
const openedDocuments = Array.from(this.containers.keys()).filter((c) => !c.closed);
|
|
290
|
-
const openedClientId = openedDocuments.map(
|
|
291
|
-
(container) => (container as Container).clientId,
|
|
292
|
-
);
|
|
280
|
+
const openedClientId = openedDocuments.map((container) => container.clientId);
|
|
293
281
|
|
|
294
282
|
const pendingClients: [IContainer, Set<string>][] = [];
|
|
295
283
|
containersToApply.forEach((container) => {
|
|
@@ -321,48 +309,78 @@ export class LoaderContainerTracker implements IOpProcessingController {
|
|
|
321
309
|
*
|
|
322
310
|
* @param containersToApply - the set of containers to check
|
|
323
311
|
*/
|
|
324
|
-
private
|
|
312
|
+
private needSequenceNumberSynchronize(containersToApply: IContainer[]) {
|
|
313
|
+
// If there is a pending proposal, wait for it to be accepted
|
|
314
|
+
const minSeqNum = containersToApply[0].deltaManager.minimumSequenceNumber;
|
|
315
|
+
if (minSeqNum < this.lastProposalSeqNum) {
|
|
316
|
+
return {
|
|
317
|
+
reason: "Proposal",
|
|
318
|
+
message: `waiting for MSN to advance to proposal at sequence number ${this.lastProposalSeqNum}`,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
325
322
|
// clientSequenceNumber check detects ops in flight, both on the wire and in the outbound queue
|
|
326
323
|
// We need both client sequence number and isDirty check because:
|
|
327
324
|
// - Currently isDirty flag ignores ops for task scheduler, so we need the client sequence number check
|
|
328
325
|
// - But isDirty flags include ops during forceReadonly and disconnected, because we don't submit
|
|
329
326
|
// the ops in the first place, clientSequenceNumber is not assigned
|
|
330
327
|
|
|
331
|
-
const
|
|
328
|
+
const containerWithInflightOps = containersToApply.filter((container) => {
|
|
332
329
|
if (container.deltaManager.readOnlyInfo.readonly === true) {
|
|
333
330
|
// Ignore readonly container. the clientSeqNum and clientSeqNumObserved might be out of sync
|
|
334
331
|
// because we transition to readonly when outbound is not empty or the in transit op got lost
|
|
335
|
-
return
|
|
332
|
+
return false;
|
|
336
333
|
}
|
|
337
334
|
// Note that in read only mode, the op won't be submitted
|
|
338
335
|
let deltaManager = container.deltaManager as any;
|
|
339
336
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
340
337
|
const { trailingNoOps } = this.containers.get(container)!;
|
|
341
|
-
// Back-compat: clientSequenceNumber
|
|
338
|
+
// Back-compat: lastSubmittedClientId/clientSequenceNumber/clientSequenceNumberObserved moved to ConnectionManager in 0.53
|
|
342
339
|
if (!("clientSequenceNumber" in deltaManager)) {
|
|
343
340
|
deltaManager = deltaManager.connectionManager;
|
|
344
341
|
}
|
|
345
342
|
assert("clientSequenceNumber" in deltaManager, "no clientSequenceNumber");
|
|
346
343
|
assert("clientSequenceNumberObserved" in deltaManager, "no clientSequenceNumber");
|
|
344
|
+
// If last submittedClientId isn't the current clientId, then we haven't send any ops
|
|
347
345
|
return (
|
|
348
|
-
deltaManager.
|
|
349
|
-
|
|
346
|
+
deltaManager.lastSubmittedClientId === container.clientId &&
|
|
347
|
+
deltaManager.clientSequenceNumber !==
|
|
348
|
+
(deltaManager.clientSequenceNumberObserved as number) + trailingNoOps
|
|
350
349
|
);
|
|
351
350
|
});
|
|
352
351
|
|
|
353
|
-
if (
|
|
354
|
-
return
|
|
352
|
+
if (containerWithInflightOps.length !== 0) {
|
|
353
|
+
return {
|
|
354
|
+
reason: "InflightOps",
|
|
355
|
+
message: `waiting for containers with inflight ops: ${this.containerIndexStrings(
|
|
356
|
+
containerWithInflightOps,
|
|
357
|
+
)}`,
|
|
358
|
+
};
|
|
355
359
|
}
|
|
356
360
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
+
// Check to see if all the container has process the same number of ops.
|
|
362
|
+
const maxSeqNum = Math.max(
|
|
363
|
+
...containersToApply.map((c) => c.deltaManager.lastSequenceNumber),
|
|
364
|
+
);
|
|
365
|
+
const containerWithPendingIncoming = containersToApply.filter(
|
|
366
|
+
(c) => c.deltaManager.lastSequenceNumber !== maxSeqNum,
|
|
367
|
+
);
|
|
368
|
+
if (containerWithPendingIncoming.length !== 0) {
|
|
369
|
+
return {
|
|
370
|
+
reason: "Pending",
|
|
371
|
+
message: `waiting for containers with pending incoming ops up to sequence number ${maxSeqNum}: ${this.containerIndexStrings(
|
|
372
|
+
containerWithPendingIncoming,
|
|
373
|
+
)}`,
|
|
374
|
+
};
|
|
361
375
|
}
|
|
376
|
+
return undefined;
|
|
377
|
+
}
|
|
362
378
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
379
|
+
private containerIndexStrings(containers: IContainer[]) {
|
|
380
|
+
return containers.map(
|
|
381
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
382
|
+
(c) => this.containers.get(c)!.index,
|
|
383
|
+
);
|
|
366
384
|
}
|
|
367
385
|
|
|
368
386
|
/**
|
|
@@ -375,7 +393,7 @@ export class LoaderContainerTracker implements IOpProcessingController {
|
|
|
375
393
|
*/
|
|
376
394
|
private async waitForPendingClients(pendingClients: [IContainer, Set<string>][]) {
|
|
377
395
|
const unconnectedClients = Array.from(this.containers.keys()).filter(
|
|
378
|
-
(c) => !c.closed &&
|
|
396
|
+
(c) => !c.closed && c.connectionState !== ConnectionState.Connected,
|
|
379
397
|
);
|
|
380
398
|
return Promise.all(
|
|
381
399
|
pendingClients.map(async ([container, pendingClientId]) => {
|
|
@@ -435,6 +453,10 @@ export class LoaderContainerTracker implements IOpProcessingController {
|
|
|
435
453
|
const containersToApply = this.getContainers(containers);
|
|
436
454
|
for (const container of containersToApply) {
|
|
437
455
|
const record = this.containers.get(container);
|
|
456
|
+
assert(
|
|
457
|
+
record?.pauseP === undefined,
|
|
458
|
+
"Cannot resume container while pausing is in progress",
|
|
459
|
+
);
|
|
438
460
|
if (record?.paused === true) {
|
|
439
461
|
debugWait(`${record.index}: container resumed`);
|
|
440
462
|
container.deltaManager.inbound.resume();
|
|
@@ -449,26 +471,98 @@ export class LoaderContainerTracker implements IOpProcessingController {
|
|
|
449
471
|
/**
|
|
450
472
|
* Pause all queue activities on the containers given, or all tracked containers
|
|
451
473
|
* Any containers given that is not tracked will be ignored.
|
|
474
|
+
*
|
|
475
|
+
* When a container is paused, it is assumed that we want fine grain control over op
|
|
476
|
+
* sequencing. This function will prepare the container and force it into write mode to
|
|
477
|
+
* avoid missing join messages or change the sequence of event when switching from read to
|
|
478
|
+
* write mode.
|
|
452
479
|
*/
|
|
453
480
|
public async pauseProcessing(...containers: IContainer[]) {
|
|
454
|
-
const
|
|
481
|
+
const waitP: Promise<void>[] = [];
|
|
455
482
|
const containersToApply = this.getContainers(containers);
|
|
456
483
|
for (const container of containersToApply) {
|
|
457
484
|
const record = this.containers.get(container);
|
|
458
485
|
if (record !== undefined && !record.paused) {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
record.
|
|
486
|
+
if (record.pauseP === undefined) {
|
|
487
|
+
record.pauseP = this.pauseContainer(container, record);
|
|
488
|
+
}
|
|
489
|
+
waitP.push(record.pauseP);
|
|
463
490
|
}
|
|
464
491
|
}
|
|
465
|
-
await Promise.all(
|
|
492
|
+
await Promise.all(waitP);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* When a container is paused, it is assumed that we want fine grain control over op
|
|
497
|
+
* sequencing. This function will prepare the container and force it into write mode to
|
|
498
|
+
* avoid missing join messages or change the sequence of event when switching from read to
|
|
499
|
+
* write mode.
|
|
500
|
+
*
|
|
501
|
+
* @param container - the container to pause
|
|
502
|
+
* @param record - the record for the container
|
|
503
|
+
*/
|
|
504
|
+
private async pauseContainer(container: IContainer, record: ContainerRecord) {
|
|
505
|
+
debugWait(`${record.index}: pausing container`);
|
|
506
|
+
assert(!container.deltaManager.outbound.paused, "Container should not be paused yet");
|
|
507
|
+
assert(!container.deltaManager.inbound.paused, "Container should not be paused yet");
|
|
508
|
+
|
|
509
|
+
// Pause outbound
|
|
510
|
+
debugWait(`${record.index}: pausing container outbound queues`);
|
|
511
|
+
await container.deltaManager.outbound.pause();
|
|
512
|
+
|
|
513
|
+
// Ensure the container is connected first.
|
|
514
|
+
if (container.connectionState !== ConnectionState.Connected) {
|
|
515
|
+
debugWait(`${record.index}: Wait for container connection`);
|
|
516
|
+
await waitForContainerConnection(container);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Check if the container is in write mode
|
|
520
|
+
if (!container.deltaManager.active) {
|
|
521
|
+
let proposalP: Promise<boolean> | undefined;
|
|
522
|
+
if (container.deltaManager.outbound.idle) {
|
|
523
|
+
// Need to generate an op to force write mode
|
|
524
|
+
debugWait(`${record.index}: container force write connection`);
|
|
525
|
+
const maybeContainer = container as Partial<IContainer>;
|
|
526
|
+
const codeProposal = maybeContainer.getLoadedCodeDetails
|
|
527
|
+
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
528
|
+
container.getLoadedCodeDetails()!
|
|
529
|
+
: (container as any).chaincodePackage;
|
|
530
|
+
|
|
531
|
+
proposalP = container.proposeCodeDetails(codeProposal);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Wait for nack
|
|
535
|
+
debugWait(`${record.index}: Wait for container disconnect`);
|
|
536
|
+
container.deltaManager.outbound.resume();
|
|
537
|
+
await new Promise<void>((resolve) => container.once("disconnected", resolve));
|
|
538
|
+
const accepted = proposalP ? await proposalP : false;
|
|
539
|
+
assert(!accepted, "A proposal in read mode should be rejected");
|
|
540
|
+
await container.deltaManager.outbound.pause();
|
|
541
|
+
|
|
542
|
+
// Ensure the container is reconnect.
|
|
543
|
+
if (container.connectionState !== ConnectionState.Connected) {
|
|
544
|
+
debugWait(`${record.index}: Wait for container reconnection`);
|
|
545
|
+
await waitForContainerConnection(container);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
debugWait(`${record.index}: pausing container inbound queues`);
|
|
550
|
+
|
|
551
|
+
// Pause inbound
|
|
552
|
+
await container.deltaManager.inbound.pause();
|
|
553
|
+
|
|
554
|
+
debugWait(`${record.index}: container paused`);
|
|
555
|
+
|
|
556
|
+
record.pauseP = undefined;
|
|
557
|
+
record.paused = true;
|
|
466
558
|
}
|
|
467
559
|
|
|
468
560
|
/**
|
|
469
561
|
* Pause all queue activities on all tracked containers, and resume only
|
|
470
562
|
* inbound to process ops until it is idle. All queues are left in the paused state
|
|
471
|
-
* after the function
|
|
563
|
+
* after the function.
|
|
564
|
+
*
|
|
565
|
+
* Pausing will switch the container to write mode. See `pauseProcessing`
|
|
472
566
|
*/
|
|
473
567
|
public async processIncoming(...containers: IContainer[]) {
|
|
474
568
|
return this.processQueue(containers, (container) => container.deltaManager.inbound);
|
|
@@ -477,7 +571,9 @@ export class LoaderContainerTracker implements IOpProcessingController {
|
|
|
477
571
|
/**
|
|
478
572
|
* Pause all queue activities on all tracked containers, and resume only
|
|
479
573
|
* outbound to process ops until it is idle. All queues are left in the paused state
|
|
480
|
-
* after the function
|
|
574
|
+
* after the function.
|
|
575
|
+
*
|
|
576
|
+
* Pausing will switch the container to write mode. See `pauseProcessing`
|
|
481
577
|
*/
|
|
482
578
|
public async processOutgoing(...containers: IContainer[]) {
|
|
483
579
|
return this.processQueue(containers, (container) => container.deltaManager.outbound);
|
|
@@ -494,9 +590,15 @@ export class LoaderContainerTracker implements IOpProcessingController {
|
|
|
494
590
|
const resumed: IDeltaQueue<U>[] = [];
|
|
495
591
|
|
|
496
592
|
const containersToApply = this.getContainers(containers);
|
|
593
|
+
|
|
497
594
|
const inflightTracker = new Map<IContainer, number>();
|
|
498
595
|
const cleanup: (() => void)[] = [];
|
|
499
596
|
for (const container of containersToApply) {
|
|
597
|
+
assert(
|
|
598
|
+
container.deltaManager.active,
|
|
599
|
+
"Container should be connected in write mode already",
|
|
600
|
+
);
|
|
601
|
+
|
|
500
602
|
const queue = getQueue(container);
|
|
501
603
|
|
|
502
604
|
// track the outgoing ops (if any) to make sure they make the round trip to at least to the same client
|
|
@@ -546,7 +648,7 @@ export class LoaderContainerTracker implements IOpProcessingController {
|
|
|
546
648
|
const inHandler = (message: ISequencedDocumentMessage) => {
|
|
547
649
|
if (
|
|
548
650
|
!canBeCoalescedByService(message) &&
|
|
549
|
-
message.clientId ===
|
|
651
|
+
message.clientId === container.clientId &&
|
|
550
652
|
inflightTracker.get(container) === message.clientSequenceNumber
|
|
551
653
|
) {
|
|
552
654
|
inflightTracker.delete(container);
|
|
@@ -600,7 +702,7 @@ export class LoaderContainerTracker implements IOpProcessingController {
|
|
|
600
702
|
return `${e.message}: ${e.stack}`;
|
|
601
703
|
}
|
|
602
704
|
};
|
|
603
|
-
debugOp(`${index}: ADD: clientId: ${
|
|
705
|
+
debugOp(`${index}: ADD: clientId: ${container.clientId}`);
|
|
604
706
|
container.deltaManager.outbound.on("op", (messages) => {
|
|
605
707
|
for (const msg of messages) {
|
|
606
708
|
debugOp(
|
|
@@ -614,7 +716,7 @@ export class LoaderContainerTracker implements IOpProcessingController {
|
|
|
614
716
|
const getInboundHandler = (type: string) => {
|
|
615
717
|
return (msg: ISequencedDocumentMessage) => {
|
|
616
718
|
const clientSeq =
|
|
617
|
-
msg.clientId ===
|
|
719
|
+
msg.clientId === container.clientId
|
|
618
720
|
? `cli: ${msg.clientSequenceNumber.toString().padStart(3)}`
|
|
619
721
|
: " ";
|
|
620
722
|
debugOp(
|
package/src/packageVersion.ts
CHANGED