@fluidframework/container-runtime 2.90.0 → 2.92.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/api-report/container-runtime.legacy.beta.api.md +2 -0
- package/container-runtime.test-files.tar +0 -0
- package/dist/containerCompatibility.d.ts +1 -1
- package/dist/containerCompatibility.d.ts.map +1 -1
- package/dist/containerCompatibility.js.map +1 -1
- package/dist/containerRuntime.d.ts +37 -10
- package/dist/containerRuntime.d.ts.map +1 -1
- package/dist/containerRuntime.js +105 -77
- package/dist/containerRuntime.js.map +1 -1
- package/dist/gc/garbageCollection.d.ts +1 -0
- package/dist/gc/garbageCollection.d.ts.map +1 -1
- package/dist/gc/garbageCollection.js +3 -8
- package/dist/gc/garbageCollection.js.map +1 -1
- package/dist/gc/gcDefinitions.d.ts +4 -0
- package/dist/gc/gcDefinitions.d.ts.map +1 -1
- package/dist/gc/gcDefinitions.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/legacy.d.ts +1 -1
- package/dist/opLifecycle/batchManager.d.ts.map +1 -1
- package/dist/opLifecycle/batchManager.js +2 -1
- package/dist/opLifecycle/batchManager.js.map +1 -1
- package/dist/opLifecycle/index.d.ts +1 -1
- package/dist/opLifecycle/index.d.ts.map +1 -1
- package/dist/opLifecycle/index.js +2 -1
- package/dist/opLifecycle/index.js.map +1 -1
- package/dist/opLifecycle/opGroupingManager.d.ts +6 -0
- package/dist/opLifecycle/opGroupingManager.d.ts.map +1 -1
- package/dist/opLifecycle/opGroupingManager.js +11 -2
- package/dist/opLifecycle/opGroupingManager.js.map +1 -1
- package/dist/opLifecycle/opSerialization.d.ts +3 -1
- package/dist/opLifecycle/opSerialization.d.ts.map +1 -1
- package/dist/opLifecycle/opSerialization.js +11 -9
- package/dist/opLifecycle/opSerialization.js.map +1 -1
- package/dist/opLifecycle/outbox.d.ts +0 -6
- package/dist/opLifecycle/outbox.d.ts.map +1 -1
- package/dist/opLifecycle/outbox.js +2 -9
- package/dist/opLifecycle/outbox.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/pendingStateManager.d.ts +7 -3
- package/dist/pendingStateManager.d.ts.map +1 -1
- package/dist/pendingStateManager.js +19 -7
- package/dist/pendingStateManager.js.map +1 -1
- package/dist/public.d.ts +1 -1
- package/dist/runtimeLayerCompatState.d.ts +1 -1
- package/dist/summary/documentSchema.d.ts +9 -3
- package/dist/summary/documentSchema.d.ts.map +1 -1
- package/dist/summary/documentSchema.js +19 -3
- package/dist/summary/documentSchema.js.map +1 -1
- package/dist/summary/orderedClientElection.js +2 -2
- package/dist/summary/orderedClientElection.js.map +1 -1
- package/dist/summary/summaryManager.d.ts +9 -0
- package/dist/summary/summaryManager.d.ts.map +1 -1
- package/dist/summary/summaryManager.js +29 -0
- package/dist/summary/summaryManager.js.map +1 -1
- package/internal.d.ts +1 -1
- package/legacy.d.ts +1 -1
- package/lib/containerCompatibility.d.ts +1 -1
- package/lib/containerCompatibility.d.ts.map +1 -1
- package/lib/containerCompatibility.js.map +1 -1
- package/lib/containerRuntime.d.ts +37 -10
- package/lib/containerRuntime.d.ts.map +1 -1
- package/lib/containerRuntime.js +106 -79
- package/lib/containerRuntime.js.map +1 -1
- package/lib/gc/garbageCollection.d.ts +1 -0
- package/lib/gc/garbageCollection.d.ts.map +1 -1
- package/lib/gc/garbageCollection.js +3 -8
- package/lib/gc/garbageCollection.js.map +1 -1
- package/lib/gc/gcDefinitions.d.ts +4 -0
- package/lib/gc/gcDefinitions.d.ts.map +1 -1
- package/lib/gc/gcDefinitions.js.map +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/lib/legacy.d.ts +1 -1
- package/lib/opLifecycle/batchManager.d.ts.map +1 -1
- package/lib/opLifecycle/batchManager.js +2 -1
- package/lib/opLifecycle/batchManager.js.map +1 -1
- package/lib/opLifecycle/index.d.ts +1 -1
- package/lib/opLifecycle/index.d.ts.map +1 -1
- package/lib/opLifecycle/index.js +1 -1
- package/lib/opLifecycle/index.js.map +1 -1
- package/lib/opLifecycle/opGroupingManager.d.ts +6 -0
- package/lib/opLifecycle/opGroupingManager.d.ts.map +1 -1
- package/lib/opLifecycle/opGroupingManager.js +10 -1
- package/lib/opLifecycle/opGroupingManager.js.map +1 -1
- package/lib/opLifecycle/opSerialization.d.ts +3 -1
- package/lib/opLifecycle/opSerialization.d.ts.map +1 -1
- package/lib/opLifecycle/opSerialization.js +11 -9
- package/lib/opLifecycle/opSerialization.js.map +1 -1
- package/lib/opLifecycle/outbox.d.ts +0 -6
- package/lib/opLifecycle/outbox.d.ts.map +1 -1
- package/lib/opLifecycle/outbox.js +2 -9
- package/lib/opLifecycle/outbox.js.map +1 -1
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/lib/pendingStateManager.d.ts +7 -3
- package/lib/pendingStateManager.d.ts.map +1 -1
- package/lib/pendingStateManager.js +19 -7
- package/lib/pendingStateManager.js.map +1 -1
- package/lib/public.d.ts +1 -1
- package/lib/runtimeLayerCompatState.d.ts +1 -1
- package/lib/summary/documentSchema.d.ts +9 -3
- package/lib/summary/documentSchema.d.ts.map +1 -1
- package/lib/summary/documentSchema.js +19 -3
- package/lib/summary/documentSchema.js.map +1 -1
- package/lib/summary/orderedClientElection.js +2 -2
- package/lib/summary/orderedClientElection.js.map +1 -1
- package/lib/summary/summaryManager.d.ts +9 -0
- package/lib/summary/summaryManager.d.ts.map +1 -1
- package/lib/summary/summaryManager.js +29 -0
- package/lib/summary/summaryManager.js.map +1 -1
- package/package.json +28 -24
- package/src/containerCompatibility.ts +2 -0
- package/src/containerRuntime.ts +153 -93
- package/src/gc/garbageCollection.ts +4 -9
- package/src/gc/gcDefinitions.ts +4 -0
- package/src/index.ts +1 -0
- package/src/opLifecycle/batchManager.ts +2 -1
- package/src/opLifecycle/index.ts +1 -0
- package/src/opLifecycle/opGroupingManager.ts +11 -1
- package/src/opLifecycle/opSerialization.ts +14 -12
- package/src/opLifecycle/outbox.ts +2 -17
- package/src/packageVersion.ts +1 -1
- package/src/pendingStateManager.ts +27 -11
- package/src/summary/documentSchema.ts +25 -2
- package/src/summary/orderedClientElection.ts +2 -2
- package/src/summary/summaryManager.ts +32 -0
|
@@ -48,12 +48,6 @@ export interface IOutboxConfig {
|
|
|
48
48
|
* The maximum size of a batch that we can send over the wire.
|
|
49
49
|
*/
|
|
50
50
|
readonly maxBatchSizeInBytes: number;
|
|
51
|
-
/**
|
|
52
|
-
* If true, maybeFlushPartialBatch will flush the batch if the reference sequence number changed
|
|
53
|
-
* since the batch started. Otherwise, it will throw in this case (apart from reentrancy which is handled elsewhere).
|
|
54
|
-
* Once the new throw-based flow is proved in a production environment, this option will be removed.
|
|
55
|
-
*/
|
|
56
|
-
readonly flushPartialBatches: boolean;
|
|
57
51
|
}
|
|
58
52
|
|
|
59
53
|
export interface IOutboxParameters {
|
|
@@ -142,7 +136,7 @@ export function localBatchToOutboundBatch({
|
|
|
142
136
|
// Shallow copy each message as we switch types
|
|
143
137
|
const outboundMessages = localBatch.messages.map<OutboundBatchMessage>(
|
|
144
138
|
({ runtimeOp, ...message }) => ({
|
|
145
|
-
contents: serializeOp(runtimeOp),
|
|
139
|
+
contents: serializeOp(runtimeOp).content,
|
|
146
140
|
...message,
|
|
147
141
|
}),
|
|
148
142
|
);
|
|
@@ -295,10 +289,7 @@ export class Outbox {
|
|
|
295
289
|
this.logger.sendTelemetryEvent(
|
|
296
290
|
{
|
|
297
291
|
// Only log error if this is truly unexpected
|
|
298
|
-
category:
|
|
299
|
-
expectedDueToReentrancy || this.params.config.flushPartialBatches
|
|
300
|
-
? "generic"
|
|
301
|
-
: "error",
|
|
292
|
+
category: expectedDueToReentrancy ? "generic" : "error",
|
|
302
293
|
eventName: "ReferenceSequenceNumberMismatch",
|
|
303
294
|
details: {
|
|
304
295
|
expectedDueToReentrancy,
|
|
@@ -314,12 +305,6 @@ export class Outbox {
|
|
|
314
305
|
);
|
|
315
306
|
}
|
|
316
307
|
|
|
317
|
-
// If we're configured to flush partial batches, do that now and return (don't throw)
|
|
318
|
-
if (this.params.config.flushPartialBatches) {
|
|
319
|
-
this.flushAll();
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
308
|
// If we are in a reentrant context, we know this can happen without causing any harm.
|
|
324
309
|
if (expectedDueToReentrancy) {
|
|
325
310
|
return;
|
package/src/packageVersion.ts
CHANGED
|
@@ -335,7 +335,9 @@ export class PendingStateManager implements IDisposable {
|
|
|
335
335
|
return this.pendingMessagesCount !== 0;
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
-
public getLocalState(snapshotSequenceNumber?: number):
|
|
338
|
+
public getLocalState(snapshotSequenceNumber?: number): {
|
|
339
|
+
pending: IPendingLocalState;
|
|
340
|
+
} {
|
|
339
341
|
assert(
|
|
340
342
|
this.initialMessages.isEmpty(),
|
|
341
343
|
0x2e9 /* "Must call getLocalState() after applying initial states" */,
|
|
@@ -359,10 +361,12 @@ export class PendingStateManager implements IDisposable {
|
|
|
359
361
|
}
|
|
360
362
|
}
|
|
361
363
|
return {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
364
|
+
pending: {
|
|
365
|
+
pendingStates: [
|
|
366
|
+
...newSavedOps,
|
|
367
|
+
...this.pendingMessages.toArray().map((message) => toSerializableForm(message)),
|
|
368
|
+
],
|
|
369
|
+
},
|
|
366
370
|
};
|
|
367
371
|
}
|
|
368
372
|
|
|
@@ -427,7 +431,7 @@ export class PendingStateManager implements IDisposable {
|
|
|
427
431
|
clientId !== undefined,
|
|
428
432
|
0xa33 /* clientId (from stateHandler) could only be undefined if we've never connected, but we have a CSN so we know that's not the case */,
|
|
429
433
|
);
|
|
430
|
-
|
|
434
|
+
const batchInfo = { clientId, batchStartCsn, length: batch.length, ignoreBatchId, staged };
|
|
431
435
|
for (const message of batch) {
|
|
432
436
|
const {
|
|
433
437
|
runtimeOp,
|
|
@@ -438,12 +442,11 @@ export class PendingStateManager implements IDisposable {
|
|
|
438
442
|
const pendingMessage: IPendingMessage = {
|
|
439
443
|
type: "message",
|
|
440
444
|
referenceSequenceNumber,
|
|
441
|
-
content: serializeOp(runtimeOp),
|
|
445
|
+
content: serializeOp(runtimeOp).content,
|
|
442
446
|
runtimeOp,
|
|
443
447
|
localOpMetadata,
|
|
444
448
|
opMetadata,
|
|
445
|
-
|
|
446
|
-
batchInfo: { clientId, batchStartCsn, length: batch.length, ignoreBatchId, staged },
|
|
449
|
+
batchInfo,
|
|
447
450
|
};
|
|
448
451
|
this.pendingMessages.push(pendingMessage);
|
|
449
452
|
}
|
|
@@ -747,8 +750,12 @@ export class PendingStateManager implements IDisposable {
|
|
|
747
750
|
* Called when the Container's connection state changes. If the Container gets connected, it replays all the pending
|
|
748
751
|
* states in its queue. This includes triggering resubmission of unacked ops.
|
|
749
752
|
* ! Note: successfully resubmitting an op that has been successfully sequenced is not possible due to checks in the ConnectionStateHandler (Loader layer)
|
|
753
|
+
*
|
|
754
|
+
* @returns The unique batch infos for all batches that were replayed.
|
|
750
755
|
*/
|
|
751
|
-
public replayPendingStates(
|
|
756
|
+
public replayPendingStates(
|
|
757
|
+
options?: ReplayPendingStateOptions,
|
|
758
|
+
): IPendingMessage["batchInfo"][] {
|
|
752
759
|
const { committingStagedBatches, squash } = {
|
|
753
760
|
...defaultReplayPendingStatesOptions,
|
|
754
761
|
...options,
|
|
@@ -775,6 +782,7 @@ export class PendingStateManager implements IDisposable {
|
|
|
775
782
|
|
|
776
783
|
const initialPendingMessagesCount = this.pendingMessages.length;
|
|
777
784
|
let remainingPendingMessagesCount = this.pendingMessages.length;
|
|
785
|
+
const replayedBatchSet = new Set<IPendingMessage["batchInfo"]>();
|
|
778
786
|
|
|
779
787
|
let seenStagedBatch = false;
|
|
780
788
|
|
|
@@ -812,6 +820,7 @@ export class PendingStateManager implements IDisposable {
|
|
|
812
820
|
if (asEmptyBatchLocalOpMetadata(pendingMessage.localOpMetadata)?.emptyBatch === true) {
|
|
813
821
|
// Resubmit no messages, with the batchId. Will result in another empty batch marker.
|
|
814
822
|
this.stateHandler.reSubmitBatch([], { batchId, staged, squash });
|
|
823
|
+
replayedBatchSet.add(pendingMessage.batchInfo);
|
|
815
824
|
continue;
|
|
816
825
|
}
|
|
817
826
|
|
|
@@ -838,6 +847,7 @@ export class PendingStateManager implements IDisposable {
|
|
|
838
847
|
],
|
|
839
848
|
{ batchId, staged, squash },
|
|
840
849
|
);
|
|
850
|
+
replayedBatchSet.add(pendingMessage.batchInfo);
|
|
841
851
|
continue;
|
|
842
852
|
}
|
|
843
853
|
// else: batchMetadataFlag === true (It's a typical multi-message batch)
|
|
@@ -877,6 +887,7 @@ export class PendingStateManager implements IDisposable {
|
|
|
877
887
|
}
|
|
878
888
|
|
|
879
889
|
this.stateHandler.reSubmitBatch(batch, { batchId, staged, squash });
|
|
890
|
+
replayedBatchSet.add(pendingMessage.batchInfo);
|
|
880
891
|
}
|
|
881
892
|
|
|
882
893
|
if (!committingStagedBatches) {
|
|
@@ -894,6 +905,8 @@ export class PendingStateManager implements IDisposable {
|
|
|
894
905
|
clientId: this.stateHandler.clientId(),
|
|
895
906
|
});
|
|
896
907
|
}
|
|
908
|
+
|
|
909
|
+
return [...replayedBatchSet];
|
|
897
910
|
}
|
|
898
911
|
|
|
899
912
|
/**
|
|
@@ -904,11 +917,13 @@ export class PendingStateManager implements IDisposable {
|
|
|
904
917
|
// callback will only be given staged messages with a valid runtime op (i.e. not empty batch and not an initial message with only serialized content)
|
|
905
918
|
stagedMessage: IPendingMessage & { runtimeOp: LocalContainerRuntimeMessage },
|
|
906
919
|
) => void,
|
|
907
|
-
):
|
|
920
|
+
): IPendingMessage["batchInfo"][] {
|
|
921
|
+
const batchSet = new Set<IPendingMessage["batchInfo"]>();
|
|
908
922
|
while (!this.pendingMessages.isEmpty()) {
|
|
909
923
|
const stagedMessage = this.pendingMessages.peekBack();
|
|
910
924
|
if (stagedMessage?.batchInfo.staged === true) {
|
|
911
925
|
this.pendingMessages.pop();
|
|
926
|
+
batchSet.add(stagedMessage.batchInfo);
|
|
912
927
|
|
|
913
928
|
if (hasTypicalRuntimeOp(stagedMessage)) {
|
|
914
929
|
callback(stagedMessage);
|
|
@@ -921,6 +936,7 @@ export class PendingStateManager implements IDisposable {
|
|
|
921
936
|
this.pendingMessages.toArray().every((m) => m.batchInfo.staged !== true),
|
|
922
937
|
0xb89 /* Shouldn't be any more staged messages */,
|
|
923
938
|
);
|
|
939
|
+
return [...batchSet];
|
|
924
940
|
}
|
|
925
941
|
}
|
|
926
942
|
|
|
@@ -512,7 +512,7 @@ function arrayToProp(arr: string[]): string[] | undefined {
|
|
|
512
512
|
*
|
|
513
513
|
* Users of this class need to use DocumentsSchemaController.sessionSchema to determine what features can be used.
|
|
514
514
|
*
|
|
515
|
-
* There are
|
|
515
|
+
* There are three modes this class can operate:
|
|
516
516
|
* 1) Legacy mode. In such mode it does not issue any ops to change document schema. Any changes happen implicitly,
|
|
517
517
|
* right away, and new features are available right away
|
|
518
518
|
* 2) Non-legacy mode. In such mode any changes to schema require an op roundtrip. This class will manage such transitions.
|
|
@@ -523,6 +523,9 @@ function arrayToProp(arr: string[]): string[] | undefined {
|
|
|
523
523
|
* then eventually all documents that are modified will have that feature reflected in their schema. It could require
|
|
524
524
|
* multiple reloads / new sessions to get there (depends on if code reacts to schema changes right away, or only consults
|
|
525
525
|
* schema on document load).
|
|
526
|
+
* 3) Schema upgrade disabled mode (disableSchemaUpgrade = true). In this mode the controller will never send DocumentSchemaChange ops
|
|
527
|
+
* and will throw an error if any incoming schema change ops are received. The document schema is effectively frozen at the schema
|
|
528
|
+
* loaded for this session (snapshot) and will not accept further schema-change ops.
|
|
526
529
|
*
|
|
527
530
|
* How schemas are changed (in non-legacy mode):
|
|
528
531
|
* If a client needs to change a schema, it will attempt to do so as part of normal ops sending process.
|
|
@@ -569,6 +572,7 @@ export class DocumentsSchemaController {
|
|
|
569
572
|
* @param onSchemaChange - callback that is called whenever schema is changed (not called on creation / load, only when processing document schema change ops)
|
|
570
573
|
* @param info - Informational properties of the document that are not subject to strict schema enforcement
|
|
571
574
|
* @param logger - telemetry logger from the runtime
|
|
575
|
+
* @param disableSchemaUpgrade - when true, the controller will never send or accept DocumentSchemaChange ops
|
|
572
576
|
*/
|
|
573
577
|
constructor(
|
|
574
578
|
existing: boolean,
|
|
@@ -578,6 +582,7 @@ export class DocumentsSchemaController {
|
|
|
578
582
|
private readonly onSchemaChange: (schema: IDocumentSchemaCurrent) => void,
|
|
579
583
|
info: IDocumentSchemaInfo,
|
|
580
584
|
logger: ITelemetryLoggerExt,
|
|
585
|
+
private readonly disableSchemaUpgrade: boolean,
|
|
581
586
|
) {
|
|
582
587
|
// For simplicity, let's only support new schema features for explicit schema control mode
|
|
583
588
|
assert(
|
|
@@ -704,9 +709,12 @@ export class DocumentsSchemaController {
|
|
|
704
709
|
* Called by Container runtime whenever it is about to send some op.
|
|
705
710
|
* It gives opportunity for controller to issue its own ops - we do not want to send ops if there are no local changes in document.
|
|
706
711
|
* Please consider note above constructor about race conditions - current design is to generate op only once in a session lifetime.
|
|
707
|
-
* @returns Optional message to send.
|
|
712
|
+
* @returns Optional message to send. Always returns undefined when disableSchemaUpgrade is true.
|
|
708
713
|
*/
|
|
709
714
|
public maybeGenerateSchemaMessage(): IDocumentSchemaChangeMessageOutgoing | undefined {
|
|
715
|
+
if (this.disableSchemaUpgrade) {
|
|
716
|
+
return undefined;
|
|
717
|
+
}
|
|
710
718
|
if (this.futureSchema !== undefined && !this.opPending) {
|
|
711
719
|
this.opPending = true;
|
|
712
720
|
assert(
|
|
@@ -739,6 +747,7 @@ export class DocumentsSchemaController {
|
|
|
739
747
|
/**
|
|
740
748
|
* Process document schema change messages
|
|
741
749
|
* Called by ContainerRuntime whenever it sees document schema messages.
|
|
750
|
+
* When disableSchemaUpgrade is true, an error is thrown if any incoming schema change ops are received.
|
|
742
751
|
* @param contents - contents of the messages
|
|
743
752
|
* @param local - whether op is local
|
|
744
753
|
* @param sequenceNumber - sequence number of the op
|
|
@@ -749,6 +758,20 @@ export class DocumentsSchemaController {
|
|
|
749
758
|
local: boolean,
|
|
750
759
|
sequenceNumber: number,
|
|
751
760
|
): boolean {
|
|
761
|
+
if (this.disableSchemaUpgrade) {
|
|
762
|
+
assert(
|
|
763
|
+
!local,
|
|
764
|
+
0xceb /* local schema change messages should never be generated when disableSchemaUpgrade is enabled */,
|
|
765
|
+
);
|
|
766
|
+
// Clients with disableSchemaUpgrade enabled should never generate schema change messages, but they
|
|
767
|
+
// may receive them from misconfigured clients. In such case, throw on any incoming schema change ops
|
|
768
|
+
// to prevent unexpected schema upgrades.
|
|
769
|
+
throw DataProcessingError.create(
|
|
770
|
+
"DocSchema: Received schema change op while disableSchemaUpgrade is enabled",
|
|
771
|
+
"processDocumentSchemaMessages",
|
|
772
|
+
undefined,
|
|
773
|
+
);
|
|
774
|
+
}
|
|
752
775
|
for (const content of contents) {
|
|
753
776
|
this.validateSeqNumber(content.refSeq, this.documentSchema.refSeq, "content.refSeq");
|
|
754
777
|
this.validateSeqNumber(this.documentSchema.refSeq, sequenceNumber, "refSeq");
|
|
@@ -515,7 +515,7 @@ export class OrderedClientElection
|
|
|
515
515
|
"InteractiveClientElected",
|
|
516
516
|
client,
|
|
517
517
|
sequenceNumber,
|
|
518
|
-
|
|
518
|
+
false /* forceSend */,
|
|
519
519
|
reason,
|
|
520
520
|
);
|
|
521
521
|
// Changing the elected parent as well.
|
|
@@ -544,7 +544,7 @@ export class OrderedClientElection
|
|
|
544
544
|
"ParentElected",
|
|
545
545
|
client,
|
|
546
546
|
sequenceNumber,
|
|
547
|
-
|
|
547
|
+
false /* forceSend */,
|
|
548
548
|
reason,
|
|
549
549
|
);
|
|
550
550
|
this._electedParent = client;
|
|
@@ -102,8 +102,17 @@ export class SummaryManager
|
|
|
102
102
|
private latestClientId: string | undefined;
|
|
103
103
|
private state = SummaryManagerState.Off;
|
|
104
104
|
private summarizer?: ISummarizer;
|
|
105
|
+
private pendingStopReason?: SummarizerStopReason;
|
|
105
106
|
private _disposed = false;
|
|
106
107
|
private summarizerStopTimeout?: ReturnType<typeof setTimeout>;
|
|
108
|
+
/**
|
|
109
|
+
* Monotonically increasing counter that tracks summarizer lifecycle generations.
|
|
110
|
+
* Incremented each time {@link cleanupAfterSummarizerStop} runs. Used by the
|
|
111
|
+
* promise chain in {@link startSummarization} to detect that cleanup has already
|
|
112
|
+
* been performed by another path (e.g. the stop timeout), so it can skip
|
|
113
|
+
* redundant cleanup that would corrupt the state machine.
|
|
114
|
+
*/
|
|
115
|
+
private summarizerGeneration = 0;
|
|
107
116
|
|
|
108
117
|
public get disposed(): boolean {
|
|
109
118
|
return this._disposed;
|
|
@@ -245,6 +254,8 @@ export class SummaryManager
|
|
|
245
254
|
|
|
246
255
|
assert(this.summarizer === undefined, 0x262 /* "Old summarizer is still working!" */);
|
|
247
256
|
|
|
257
|
+
const generation = this.summarizerGeneration;
|
|
258
|
+
|
|
248
259
|
this.delayBeforeCreatingSummarizer()
|
|
249
260
|
.then(async (startWithInitialDelay: boolean) => {
|
|
250
261
|
if (this.disposed) {
|
|
@@ -277,6 +288,13 @@ export class SummaryManager
|
|
|
277
288
|
this.summarizer = summarizer;
|
|
278
289
|
this.setupForwardedEvents(summarizer);
|
|
279
290
|
|
|
291
|
+
// A stop may have been requested while we were awaiting summarizer creation.
|
|
292
|
+
// Replay it now so the summarizer can observe the stop intent and move to exit.
|
|
293
|
+
if (this.pendingStopReason !== undefined) {
|
|
294
|
+
summarizer.stop(this.pendingStopReason);
|
|
295
|
+
this.pendingStopReason = undefined;
|
|
296
|
+
}
|
|
297
|
+
|
|
280
298
|
// Re-validate that it need to be running. Due to asynchrony, it may be not the case anymore
|
|
281
299
|
// If we can't run the LastSummary, simply return as to avoid paying the cost of launching
|
|
282
300
|
// the summarizer at all.
|
|
@@ -347,13 +365,25 @@ export class SummaryManager
|
|
|
347
365
|
}
|
|
348
366
|
})
|
|
349
367
|
.finally(() => {
|
|
368
|
+
if (generation !== this.summarizerGeneration) {
|
|
369
|
+
// Cleanup was already performed by another path (e.g. the stop timeout),
|
|
370
|
+
// and a new summarizer cycle may have started. Running cleanup again
|
|
371
|
+
// would corrupt the current state machine cycle.
|
|
372
|
+
this.logger.sendTelemetryEvent({
|
|
373
|
+
eventName: "SummarizerCleanupAlreadyDone",
|
|
374
|
+
currentState: this.state,
|
|
375
|
+
});
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
350
378
|
assert(this.state !== SummaryManagerState.Off, 0x264 /* "Expected: Not Off" */);
|
|
351
379
|
this.cleanupAfterSummarizerStop();
|
|
352
380
|
});
|
|
353
381
|
}
|
|
354
382
|
|
|
355
383
|
private cleanupAfterSummarizerStop(): void {
|
|
384
|
+
this.summarizerGeneration++;
|
|
356
385
|
this.state = SummaryManagerState.Off;
|
|
386
|
+
this.pendingStopReason = undefined;
|
|
357
387
|
|
|
358
388
|
// Clear any pending stop timeout to avoid it firing for a different summarizer
|
|
359
389
|
if (this.summarizerStopTimeout !== undefined) {
|
|
@@ -375,6 +405,7 @@ export class SummaryManager
|
|
|
375
405
|
return;
|
|
376
406
|
}
|
|
377
407
|
this.state = SummaryManagerState.Stopping;
|
|
408
|
+
this.pendingStopReason = reason;
|
|
378
409
|
|
|
379
410
|
// Stopping the running summarizer client should trigger a change
|
|
380
411
|
// in states when the running summarizer closes
|
|
@@ -481,6 +512,7 @@ export class SummaryManager
|
|
|
481
512
|
this.connectedState.off("connected", this.handleConnected);
|
|
482
513
|
this.connectedState.off("disconnected", this.handleDisconnected);
|
|
483
514
|
this.cleanupForwardedEvents();
|
|
515
|
+
this.pendingStopReason = undefined;
|
|
484
516
|
if (this.summarizerStopTimeout !== undefined) {
|
|
485
517
|
clearTimeout(this.summarizerStopTimeout);
|
|
486
518
|
}
|