@fluidframework/test-utils 2.0.0-dev.4.1.0.148229 → 2.0.0-dev.4.3.0.157531

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.
@@ -11,6 +11,7 @@ import {
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
@@ -178,33 +180,35 @@ export class LoaderContainerTracker implements IOpProcessingController {
178
180
  * Make sure all the tracked containers are synchronized.
179
181
  *
180
182
  * No isDirty (non-readonly) containers
181
- *
182
183
  * No extra clientId in quorum of any container that is not tracked and still opened.
183
- *
184
184
  * - i.e. no pending Join/Leave message.
185
- *
186
185
  * No unresolved proposal (minSeqNum \>= lastProposalSeqNum)
187
- *
188
186
  * lastSequenceNumber of all container is the same
189
- *
190
187
  * clientSequenceNumberObserved is the same as clientSequenceNumber sent
191
- *
192
188
  * - this overlaps with !isDirty, but include task scheduler ops.
193
- *
194
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.
195
194
  */
196
195
  public async ensureSynchronized(...containers: IContainer[]): Promise<void> {
197
196
  const resumed = this.resumeProcessing(...containers);
198
197
 
199
- let waitingSequenceNumberSynchronized = false;
198
+ let waitingSequenceNumberSynchronized: string | undefined;
200
199
  // eslint-disable-next-line no-constant-condition
201
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
+
202
206
  const containersToApply = this.getContainers(containers);
203
207
  if (containersToApply.length === 0) {
204
208
  break;
205
209
  }
206
210
 
207
- // Ignore readonly dirty containers, because it can't sent up and nothing can be done about it being dirty
211
+ // Ignore readonly dirty containers, because it can't sent ops and nothing can be done about it being dirty
208
212
  const dirtyContainers = containersToApply.filter((c) => {
209
213
  const { deltaManager, isDirty } = c;
210
214
  return deltaManager.readOnlyInfo.readonly !== true && isDirty;
@@ -213,20 +217,22 @@ export class LoaderContainerTracker implements IOpProcessingController {
213
217
  // Wait for all the leave messages
214
218
  const pendingClients = this.getPendingClients(containersToApply);
215
219
  if (pendingClients.length === 0) {
216
- if (this.isSequenceNumberSynchronized(containersToApply)) {
220
+ const needSync = this.needSequenceNumberSynchronize(containersToApply);
221
+ if (needSync === undefined) {
217
222
  // done, we are in sync
218
223
  break;
219
224
  }
220
- if (!waitingSequenceNumberSynchronized) {
221
- // Only write it out once
222
- waitingSequenceNumberSynchronized = true;
223
- debugWait("Waiting for sequence number synchronized");
224
- await timeoutAwait(this.waitForAnyInboundOps(containersToApply), {
225
- errorMsg: "Timeout on waiting for sequence number synchronized",
226
- });
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);
227
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
+ });
228
234
  } else {
229
- waitingSequenceNumberSynchronized = false;
235
+ waitingSequenceNumberSynchronized = undefined;
230
236
  await timeoutAwait(this.waitForPendingClients(pendingClients), {
231
237
  errorMsg: "Timeout on waiting for pending join or leave op",
232
238
  });
@@ -234,12 +240,9 @@ export class LoaderContainerTracker implements IOpProcessingController {
234
240
  } else {
235
241
  // Wait for all the containers to be saved
236
242
  debugWait(
237
- `Waiting container to be saved ${dirtyContainers.map(
238
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
239
- (c) => this.containers.get(c)!.index,
240
- )}`,
243
+ `Waiting container to be saved ${this.containerIndexStrings(dirtyContainers)}`,
241
244
  );
242
- waitingSequenceNumberSynchronized = false;
245
+ waitingSequenceNumberSynchronized = undefined;
243
246
  await Promise.all(
244
247
  dirtyContainers.map(async (c) =>
245
248
  Promise.race([
@@ -251,11 +254,6 @@ export class LoaderContainerTracker implements IOpProcessingController {
251
254
  ),
252
255
  );
253
256
  }
254
-
255
- // yield a turn to allow side effect of the ops we just processed execute before we check again
256
- await new Promise<void>((resolve) => {
257
- setTimeout(resolve, 0);
258
- });
259
257
  }
260
258
 
261
259
  // Pause all container that was resumed
@@ -311,48 +309,78 @@ export class LoaderContainerTracker implements IOpProcessingController {
311
309
  *
312
310
  * @param containersToApply - the set of containers to check
313
311
  */
314
- private isSequenceNumberSynchronized(containersToApply: IContainer[]) {
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
+
315
322
  // clientSequenceNumber check detects ops in flight, both on the wire and in the outbound queue
316
323
  // We need both client sequence number and isDirty check because:
317
324
  // - Currently isDirty flag ignores ops for task scheduler, so we need the client sequence number check
318
325
  // - But isDirty flags include ops during forceReadonly and disconnected, because we don't submit
319
326
  // the ops in the first place, clientSequenceNumber is not assigned
320
327
 
321
- const isClientSequenceNumberSynchronized = containersToApply.every((container) => {
328
+ const containerWithInflightOps = containersToApply.filter((container) => {
322
329
  if (container.deltaManager.readOnlyInfo.readonly === true) {
323
330
  // Ignore readonly container. the clientSeqNum and clientSeqNumObserved might be out of sync
324
331
  // because we transition to readonly when outbound is not empty or the in transit op got lost
325
- return true;
332
+ return false;
326
333
  }
327
334
  // Note that in read only mode, the op won't be submitted
328
335
  let deltaManager = container.deltaManager as any;
329
336
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
330
337
  const { trailingNoOps } = this.containers.get(container)!;
331
- // Back-compat: clientSequenceNumber & clientSequenceNumberObserved moved to ConnectionManager in 0.53
338
+ // Back-compat: lastSubmittedClientId/clientSequenceNumber/clientSequenceNumberObserved moved to ConnectionManager in 0.53
332
339
  if (!("clientSequenceNumber" in deltaManager)) {
333
340
  deltaManager = deltaManager.connectionManager;
334
341
  }
335
342
  assert("clientSequenceNumber" in deltaManager, "no clientSequenceNumber");
336
343
  assert("clientSequenceNumberObserved" in deltaManager, "no clientSequenceNumber");
344
+ // If last submittedClientId isn't the current clientId, then we haven't send any ops
337
345
  return (
338
- deltaManager.clientSequenceNumber ===
339
- (deltaManager.clientSequenceNumberObserved as number) + trailingNoOps
346
+ deltaManager.lastSubmittedClientId === container.clientId &&
347
+ deltaManager.clientSequenceNumber !==
348
+ (deltaManager.clientSequenceNumberObserved as number) + trailingNoOps
340
349
  );
341
350
  });
342
351
 
343
- if (!isClientSequenceNumberSynchronized) {
344
- return false;
352
+ if (containerWithInflightOps.length !== 0) {
353
+ return {
354
+ reason: "InflightOps",
355
+ message: `waiting for containers with inflight ops: ${this.containerIndexStrings(
356
+ containerWithInflightOps,
357
+ )}`,
358
+ };
345
359
  }
346
360
 
347
- const minSeqNum = containersToApply[0].deltaManager.minimumSequenceNumber;
348
- if (minSeqNum < this.lastProposalSeqNum) {
349
- // There is an unresolved proposal
350
- return false;
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
+ };
351
375
  }
376
+ return undefined;
377
+ }
352
378
 
353
- // Check to see if all the container has process the same number of ops.
354
- const seqNum = containersToApply[0].deltaManager.lastSequenceNumber;
355
- return containersToApply.every((c) => c.deltaManager.lastSequenceNumber === seqNum);
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
+ );
356
384
  }
357
385
 
358
386
  /**
@@ -425,6 +453,10 @@ export class LoaderContainerTracker implements IOpProcessingController {
425
453
  const containersToApply = this.getContainers(containers);
426
454
  for (const container of containersToApply) {
427
455
  const record = this.containers.get(container);
456
+ assert(
457
+ record?.pauseP === undefined,
458
+ "Cannot resume container while pausing is in progress",
459
+ );
428
460
  if (record?.paused === true) {
429
461
  debugWait(`${record.index}: container resumed`);
430
462
  container.deltaManager.inbound.resume();
@@ -439,26 +471,98 @@ export class LoaderContainerTracker implements IOpProcessingController {
439
471
  /**
440
472
  * Pause all queue activities on the containers given, or all tracked containers
441
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.
442
479
  */
443
480
  public async pauseProcessing(...containers: IContainer[]) {
444
- const pauseP: Promise<void>[] = [];
481
+ const waitP: Promise<void>[] = [];
445
482
  const containersToApply = this.getContainers(containers);
446
483
  for (const container of containersToApply) {
447
484
  const record = this.containers.get(container);
448
485
  if (record !== undefined && !record.paused) {
449
- debugWait(`${record.index}: container paused`);
450
- pauseP.push(container.deltaManager.inbound.pause());
451
- pauseP.push(container.deltaManager.outbound.pause());
452
- record.paused = true;
486
+ if (record.pauseP === undefined) {
487
+ record.pauseP = this.pauseContainer(container, record);
488
+ }
489
+ waitP.push(record.pauseP);
490
+ }
491
+ }
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);
453
546
  }
454
547
  }
455
- await Promise.all(pauseP);
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;
456
558
  }
457
559
 
458
560
  /**
459
561
  * Pause all queue activities on all tracked containers, and resume only
460
562
  * inbound to process ops until it is idle. All queues are left in the paused state
461
- * after the function
563
+ * after the function.
564
+ *
565
+ * Pausing will switch the container to write mode. See `pauseProcessing`
462
566
  */
463
567
  public async processIncoming(...containers: IContainer[]) {
464
568
  return this.processQueue(containers, (container) => container.deltaManager.inbound);
@@ -467,7 +571,9 @@ export class LoaderContainerTracker implements IOpProcessingController {
467
571
  /**
468
572
  * Pause all queue activities on all tracked containers, and resume only
469
573
  * outbound to process ops until it is idle. All queues are left in the paused state
470
- * after the function
574
+ * after the function.
575
+ *
576
+ * Pausing will switch the container to write mode. See `pauseProcessing`
471
577
  */
472
578
  public async processOutgoing(...containers: IContainer[]) {
473
579
  return this.processQueue(containers, (container) => container.deltaManager.outbound);
@@ -484,9 +590,15 @@ export class LoaderContainerTracker implements IOpProcessingController {
484
590
  const resumed: IDeltaQueue<U>[] = [];
485
591
 
486
592
  const containersToApply = this.getContainers(containers);
593
+
487
594
  const inflightTracker = new Map<IContainer, number>();
488
595
  const cleanup: (() => void)[] = [];
489
596
  for (const container of containersToApply) {
597
+ assert(
598
+ container.deltaManager.active,
599
+ "Container should be connected in write mode already",
600
+ );
601
+
490
602
  const queue = getQueue(container);
491
603
 
492
604
  // track the outgoing ops (if any) to make sure they make the round trip to at least to the same client
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/test-utils";
9
- export const pkgVersion = "2.0.0-dev.4.1.0.148229";
9
+ export const pkgVersion = "2.0.0-dev.4.3.0.157531";
@@ -8,6 +8,7 @@ import {
8
8
  IHostLoader,
9
9
  IFluidCodeDetails,
10
10
  LoaderHeader,
11
+ ILoader,
11
12
  } from "@fluidframework/container-definitions";
12
13
  import {
13
14
  ITelemetryGenericEvent,
@@ -112,6 +113,9 @@ export interface ITestContainerConfig {
112
113
 
113
114
  /** Loader options for the loader used to create containers */
114
115
  loaderProps?: Partial<ILoaderProps>;
116
+
117
+ /** Temporary flag: simulate read connection using delay connection, default is true */
118
+ simulateReadConnectionUsingDelay?: boolean;
115
119
  }
116
120
 
117
121
  export const createDocumentId = (): string => uuid();
@@ -382,12 +386,20 @@ export class TestObjectProvider implements ITestObjectProvider {
382
386
  requestHeader?: IRequestHeader,
383
387
  ): Promise<IContainer> {
384
388
  const loader = this.createLoader([[defaultCodeDetails, entryPoint]], loaderProps);
389
+ return this.resolveContainer(loader, requestHeader);
390
+ }
385
391
 
386
- // Once ADO#3889 is done to switch default connection mode to "read" on load, we don't need
392
+ private async resolveContainer(
393
+ loader: ILoader,
394
+ requestHeader?: IRequestHeader,
395
+ delay: boolean = true,
396
+ ) {
397
+ // Once AB#3889 is done to switch default connection mode to "read" on load, we don't need
387
398
  // to load "delayed" across the board. Remove the following code.
388
399
  const delayConnection =
389
- requestHeader === undefined || requestHeader[LoaderHeader.reconnect] !== false;
390
- const headers: IRequestHeader = delayConnection
400
+ delay &&
401
+ (requestHeader === undefined || requestHeader[LoaderHeader.reconnect] !== false);
402
+ const headers: IRequestHeader | undefined = delayConnection
391
403
  ? {
392
404
  [LoaderHeader.loadMode]: { deltaConnection: "delayed" },
393
405
  ...requestHeader,
@@ -399,19 +411,18 @@ export class TestObjectProvider implements ITestObjectProvider {
399
411
  headers,
400
412
  });
401
413
 
402
- // Once ADO#3889 is done to switch default connection mode to "read" on load, we don't need
414
+ // Once AB#3889 is done to switch default connection mode to "read" on load, we don't need
403
415
  // to load "delayed" across the board. Remove the following code.
404
416
  if (delayConnection) {
405
- // Older version may not have connect, use resume instead.
417
+ // Older version may not have connect/disconnect. It was add in PR#9439, and available >= 0.59.1000
406
418
  const maybeContainer = container as Partial<IContainer>;
407
419
  if (maybeContainer.connect !== undefined) {
408
420
  container.connect();
409
421
  } else {
410
- // back compat
422
+ // back compat. Remove when we don't support < 0.59.1000
411
423
  (container as any).resume();
412
424
  }
413
425
  }
414
-
415
426
  return container;
416
427
  }
417
428
 
@@ -464,10 +475,12 @@ export class TestObjectProvider implements ITestObjectProvider {
464
475
  requestHeader?: IRequestHeader,
465
476
  ): Promise<IContainer> {
466
477
  const loader = this.makeTestLoader(testContainerConfig);
467
- const container = await loader.resolve({
468
- url: await this.driver.createContainerUrl(this.documentId),
469
- headers: requestHeader,
470
- });
478
+
479
+ const container = await this.resolveContainer(
480
+ loader,
481
+ requestHeader,
482
+ testContainerConfig?.simulateReadConnectionUsingDelay,
483
+ );
471
484
  await this.waitContainerToCatchUp(container);
472
485
 
473
486
  return container;