@fluidframework/container-runtime 2.102.0 → 2.103.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 +4 -0
- package/container-runtime.test-files.tar +0 -0
- package/dist/batchTracker.d.ts +1 -1
- package/dist/batchTracker.d.ts.map +1 -1
- package/dist/batchTracker.js +1 -1
- package/dist/batchTracker.js.map +1 -1
- package/dist/blobManager/blobManagerSnapSum.d.ts +2 -2
- package/dist/blobManager/blobManagerSnapSum.d.ts.map +1 -1
- package/dist/blobManager/blobManagerSnapSum.js.map +1 -1
- package/dist/connectionTelemetry.js.map +1 -1
- package/dist/containerRuntime.d.ts +16 -5
- package/dist/containerRuntime.d.ts.map +1 -1
- package/dist/containerRuntime.js +112 -9
- package/dist/containerRuntime.js.map +1 -1
- package/dist/dataStore.d.ts +2 -2
- package/dist/dataStore.d.ts.map +1 -1
- package/dist/dataStore.js.map +1 -1
- package/dist/dataStoreContexts.d.ts.map +1 -1
- package/dist/dataStoreContexts.js.map +1 -1
- package/dist/deltaScheduler.d.ts +2 -2
- package/dist/deltaScheduler.d.ts.map +1 -1
- package/dist/deltaScheduler.js.map +1 -1
- package/dist/gc/garbageCollection.d.ts +2 -2
- package/dist/gc/garbageCollection.d.ts.map +1 -1
- package/dist/gc/garbageCollection.js.map +1 -1
- package/dist/gc/gcDefinitions.d.ts +3 -3
- package/dist/gc/gcDefinitions.d.ts.map +1 -1
- package/dist/gc/gcDefinitions.js.map +1 -1
- package/dist/gc/gcTelemetry.d.ts +3 -3
- package/dist/gc/gcTelemetry.d.ts.map +1 -1
- package/dist/gc/gcTelemetry.js.map +1 -1
- package/dist/inboundBatchAggregator.d.ts +2 -2
- package/dist/inboundBatchAggregator.d.ts.map +1 -1
- package/dist/inboundBatchAggregator.js.map +1 -1
- package/dist/opLifecycle/opCompressor.d.ts.map +1 -1
- package/dist/opLifecycle/opCompressor.js.map +1 -1
- package/dist/opLifecycle/opDecompressor.d.ts.map +1 -1
- package/dist/opLifecycle/opDecompressor.js.map +1 -1
- package/dist/opLifecycle/opGroupingManager.d.ts.map +1 -1
- package/dist/opLifecycle/opGroupingManager.js.map +1 -1
- package/dist/opLifecycle/opSplitter.d.ts.map +1 -1
- package/dist/opLifecycle/opSplitter.js.map +1 -1
- package/dist/opLifecycle/outbox.d.ts.map +1 -1
- 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 +48 -1
- package/dist/pendingStateManager.d.ts.map +1 -1
- package/dist/pendingStateManager.js +54 -1
- package/dist/pendingStateManager.js.map +1 -1
- package/dist/runtimeLayerCompatState.d.ts +1 -1
- package/dist/signalTelemetryProcessing.d.ts +2 -2
- package/dist/signalTelemetryProcessing.d.ts.map +1 -1
- package/dist/signalTelemetryProcessing.js.map +1 -1
- package/dist/summary/documentSchema.d.ts +2 -2
- package/dist/summary/documentSchema.d.ts.map +1 -1
- package/dist/summary/documentSchema.js.map +1 -1
- package/dist/summary/orderedClientElection.d.ts +2 -2
- package/dist/summary/orderedClientElection.d.ts.map +1 -1
- package/dist/summary/orderedClientElection.js.map +1 -1
- package/dist/summary/summarizerClientElection.d.ts +2 -2
- package/dist/summary/summarizerClientElection.d.ts.map +1 -1
- package/dist/summary/summarizerClientElection.js.map +1 -1
- package/dist/summary/summarizerNode/summarizerNode.d.ts +3 -3
- package/dist/summary/summarizerNode/summarizerNode.d.ts.map +1 -1
- package/dist/summary/summarizerNode/summarizerNode.js.map +1 -1
- package/dist/summary/summarizerNode/summarizerNodeUtils.d.ts +2 -2
- package/dist/summary/summarizerNode/summarizerNodeUtils.d.ts.map +1 -1
- package/dist/summary/summarizerNode/summarizerNodeUtils.js.map +1 -1
- package/dist/summary/summarizerTypes.d.ts +3 -3
- package/dist/summary/summarizerTypes.d.ts.map +1 -1
- package/dist/summary/summarizerTypes.js.map +1 -1
- package/dist/summary/summaryCollection.d.ts.map +1 -1
- package/dist/summary/summaryCollection.js.map +1 -1
- package/dist/summary/summaryDelayLoadedModule/runningSummarizer.d.ts +2 -2
- package/dist/summary/summaryDelayLoadedModule/runningSummarizer.d.ts.map +1 -1
- package/dist/summary/summaryDelayLoadedModule/runningSummarizer.js.map +1 -1
- package/dist/summary/summaryDelayLoadedModule/summarizer.d.ts +2 -2
- package/dist/summary/summaryDelayLoadedModule/summarizer.d.ts.map +1 -1
- package/dist/summary/summaryDelayLoadedModule/summarizer.js.map +1 -1
- package/dist/summary/summaryDelayLoadedModule/summarizerHeuristics.d.ts +2 -2
- package/dist/summary/summaryDelayLoadedModule/summarizerHeuristics.d.ts.map +1 -1
- package/dist/summary/summaryDelayLoadedModule/summarizerHeuristics.js.map +1 -1
- package/dist/summary/summaryDelayLoadedModule/summaryGenerator.d.ts +2 -2
- package/dist/summary/summaryDelayLoadedModule/summaryGenerator.d.ts.map +1 -1
- package/dist/summary/summaryDelayLoadedModule/summaryGenerator.js.map +1 -1
- package/dist/summary/summaryManager.d.ts.map +1 -1
- package/dist/summary/summaryManager.js.map +1 -1
- package/lib/batchTracker.d.ts +1 -1
- package/lib/batchTracker.d.ts.map +1 -1
- package/lib/batchTracker.js +1 -1
- package/lib/batchTracker.js.map +1 -1
- package/lib/blobManager/blobManagerSnapSum.d.ts +2 -2
- package/lib/blobManager/blobManagerSnapSum.d.ts.map +1 -1
- package/lib/blobManager/blobManagerSnapSum.js.map +1 -1
- package/lib/connectionTelemetry.js.map +1 -1
- package/lib/containerRuntime.d.ts +16 -5
- package/lib/containerRuntime.d.ts.map +1 -1
- package/lib/containerRuntime.js +112 -9
- package/lib/containerRuntime.js.map +1 -1
- package/lib/dataStore.d.ts +2 -2
- package/lib/dataStore.d.ts.map +1 -1
- package/lib/dataStore.js.map +1 -1
- package/lib/dataStoreContexts.d.ts.map +1 -1
- package/lib/dataStoreContexts.js.map +1 -1
- package/lib/deltaScheduler.d.ts +2 -2
- package/lib/deltaScheduler.d.ts.map +1 -1
- package/lib/deltaScheduler.js +1 -1
- package/lib/deltaScheduler.js.map +1 -1
- package/lib/gc/garbageCollection.d.ts +2 -2
- package/lib/gc/garbageCollection.d.ts.map +1 -1
- package/lib/gc/garbageCollection.js.map +1 -1
- package/lib/gc/gcDefinitions.d.ts +3 -3
- package/lib/gc/gcDefinitions.d.ts.map +1 -1
- package/lib/gc/gcDefinitions.js.map +1 -1
- package/lib/gc/gcTelemetry.d.ts +3 -3
- package/lib/gc/gcTelemetry.d.ts.map +1 -1
- package/lib/gc/gcTelemetry.js.map +1 -1
- package/lib/inboundBatchAggregator.d.ts +2 -2
- package/lib/inboundBatchAggregator.d.ts.map +1 -1
- package/lib/inboundBatchAggregator.js.map +1 -1
- package/lib/opLifecycle/opCompressor.d.ts.map +1 -1
- package/lib/opLifecycle/opCompressor.js.map +1 -1
- package/lib/opLifecycle/opDecompressor.d.ts.map +1 -1
- package/lib/opLifecycle/opDecompressor.js.map +1 -1
- package/lib/opLifecycle/opGroupingManager.d.ts.map +1 -1
- package/lib/opLifecycle/opGroupingManager.js.map +1 -1
- package/lib/opLifecycle/opSplitter.d.ts.map +1 -1
- package/lib/opLifecycle/opSplitter.js.map +1 -1
- package/lib/opLifecycle/outbox.d.ts.map +1 -1
- 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 +48 -1
- package/lib/pendingStateManager.d.ts.map +1 -1
- package/lib/pendingStateManager.js +54 -1
- package/lib/pendingStateManager.js.map +1 -1
- package/lib/runtimeLayerCompatState.d.ts +1 -1
- package/lib/signalTelemetryProcessing.d.ts +2 -2
- package/lib/signalTelemetryProcessing.d.ts.map +1 -1
- package/lib/signalTelemetryProcessing.js.map +1 -1
- package/lib/summary/documentSchema.d.ts +2 -2
- package/lib/summary/documentSchema.d.ts.map +1 -1
- package/lib/summary/documentSchema.js.map +1 -1
- package/lib/summary/orderedClientElection.d.ts +2 -2
- package/lib/summary/orderedClientElection.d.ts.map +1 -1
- package/lib/summary/orderedClientElection.js.map +1 -1
- package/lib/summary/summarizerClientElection.d.ts +2 -2
- package/lib/summary/summarizerClientElection.d.ts.map +1 -1
- package/lib/summary/summarizerClientElection.js.map +1 -1
- package/lib/summary/summarizerNode/summarizerNode.d.ts +3 -3
- package/lib/summary/summarizerNode/summarizerNode.d.ts.map +1 -1
- package/lib/summary/summarizerNode/summarizerNode.js.map +1 -1
- package/lib/summary/summarizerNode/summarizerNodeUtils.d.ts +2 -2
- package/lib/summary/summarizerNode/summarizerNodeUtils.d.ts.map +1 -1
- package/lib/summary/summarizerNode/summarizerNodeUtils.js.map +1 -1
- package/lib/summary/summarizerTypes.d.ts +3 -3
- package/lib/summary/summarizerTypes.d.ts.map +1 -1
- package/lib/summary/summarizerTypes.js.map +1 -1
- package/lib/summary/summaryCollection.d.ts.map +1 -1
- package/lib/summary/summaryCollection.js.map +1 -1
- package/lib/summary/summaryDelayLoadedModule/runningSummarizer.d.ts +2 -2
- package/lib/summary/summaryDelayLoadedModule/runningSummarizer.d.ts.map +1 -1
- package/lib/summary/summaryDelayLoadedModule/runningSummarizer.js.map +1 -1
- package/lib/summary/summaryDelayLoadedModule/summarizer.d.ts +2 -2
- package/lib/summary/summaryDelayLoadedModule/summarizer.d.ts.map +1 -1
- package/lib/summary/summaryDelayLoadedModule/summarizer.js.map +1 -1
- package/lib/summary/summaryDelayLoadedModule/summarizerHeuristics.d.ts +2 -2
- package/lib/summary/summaryDelayLoadedModule/summarizerHeuristics.d.ts.map +1 -1
- package/lib/summary/summaryDelayLoadedModule/summarizerHeuristics.js.map +1 -1
- package/lib/summary/summaryDelayLoadedModule/summaryGenerator.d.ts +2 -2
- package/lib/summary/summaryDelayLoadedModule/summaryGenerator.d.ts.map +1 -1
- package/lib/summary/summaryDelayLoadedModule/summaryGenerator.js.map +1 -1
- package/lib/summary/summaryManager.d.ts.map +1 -1
- package/lib/summary/summaryManager.js.map +1 -1
- package/package.json +17 -17
- package/src/batchTracker.ts +3 -3
- package/src/blobManager/blobManagerSnapSum.ts +2 -2
- package/src/connectionTelemetry.ts +3 -3
- package/src/containerRuntime.ts +134 -15
- package/src/dataStore.ts +3 -3
- package/src/dataStoreContexts.ts +2 -2
- package/src/deltaScheduler.ts +2 -5
- package/src/gc/garbageCollection.ts +4 -4
- package/src/gc/gcDefinitions.ts +3 -3
- package/src/gc/gcTelemetry.ts +3 -3
- package/src/inboundBatchAggregator.ts +2 -2
- package/src/opLifecycle/opCompressor.ts +2 -2
- package/src/opLifecycle/opDecompressor.ts +2 -2
- package/src/opLifecycle/opGroupingManager.ts +2 -2
- package/src/opLifecycle/opSplitter.ts +2 -2
- package/src/opLifecycle/outbox.ts +2 -2
- package/src/packageVersion.ts +1 -1
- package/src/pendingStateManager.ts +80 -2
- package/src/signalTelemetryProcessing.ts +2 -2
- package/src/summary/documentSchema.ts +2 -2
- package/src/summary/orderedClientElection.ts +3 -3
- package/src/summary/summarizerClientElection.ts +2 -2
- package/src/summary/summarizerNode/summarizerNode.ts +3 -3
- package/src/summary/summarizerNode/summarizerNodeUtils.ts +2 -2
- package/src/summary/summarizerTypes.ts +3 -3
- package/src/summary/summaryCollection.ts +2 -2
- package/src/summary/summaryDelayLoadedModule/runningSummarizer.ts +2 -4
- package/src/summary/summaryDelayLoadedModule/summarizer.ts +3 -3
- package/src/summary/summaryDelayLoadedModule/summarizerHeuristics.ts +2 -2
- package/src/summary/summaryDelayLoadedModule/summaryGenerator.ts +2 -2
- package/src/summary/summaryManager.ts +2 -2
package/src/containerRuntime.ts
CHANGED
|
@@ -152,7 +152,7 @@ import type {
|
|
|
152
152
|
IEventSampler,
|
|
153
153
|
IFluidErrorBase,
|
|
154
154
|
ITelemetryGenericEventExt,
|
|
155
|
-
|
|
155
|
+
TelemetryLoggerExt,
|
|
156
156
|
MonitoringContext,
|
|
157
157
|
} from "@fluidframework/telemetry-utils/internal";
|
|
158
158
|
import {
|
|
@@ -735,7 +735,7 @@ function lastMessageFromMetadata(
|
|
|
735
735
|
* to understand if/when it is hit.
|
|
736
736
|
* We only want to log this once, to avoid spamming telemetry if we are wrong and these cases are hit commonly.
|
|
737
737
|
*/
|
|
738
|
-
export let getSingleUseLegacyLogCallback = (logger:
|
|
738
|
+
export let getSingleUseLegacyLogCallback = (logger: TelemetryLoggerExt, type: string) => {
|
|
739
739
|
return (codePath: string): void => {
|
|
740
740
|
logger.sendTelemetryEvent({
|
|
741
741
|
eventName: "LegacyMessageFormat",
|
|
@@ -1360,7 +1360,25 @@ export class ContainerRuntime
|
|
|
1360
1360
|
return this._getAttachState();
|
|
1361
1361
|
}
|
|
1362
1362
|
|
|
1363
|
-
|
|
1363
|
+
/**
|
|
1364
|
+
* Whether local op submission is currently disallowed.
|
|
1365
|
+
*
|
|
1366
|
+
* This is `true` in two distinct situations.
|
|
1367
|
+
*
|
|
1368
|
+
* First: the delta manager reports a read-only connection (host/service-imposed permission or connection state — the historical meaning of `readOnly`).
|
|
1369
|
+
*
|
|
1370
|
+
* Second: the `PendingStateManager` is replaying stashed ops (`isApplyingStashedOps`). During this window DDSes must not submit new local ops, as doing so would interleave fresh content ahead of the stashed pending stream and corrupt pending local state. Surfacing it through `isReadOnly()` lets DDSes that consult `readOnly` at realize time self-suppress; see the apply-lifecycle docs on `PendingStateManager` for the full rationale.
|
|
1371
|
+
*
|
|
1372
|
+
* Note this layers a third meaning ("transiently quiescing for stashed-op replay") onto the `readOnly` predicate, which is broader than its host/connection-permission origin.
|
|
1373
|
+
*/
|
|
1374
|
+
public readonly isReadOnly = (): boolean =>
|
|
1375
|
+
// `_deltaManager` and `pendingStateManager` are both assigned partway
|
|
1376
|
+
// through the constructor; `baseLogger` is built earlier and stamps
|
|
1377
|
+
// `isReadOnly` on every error event (e.g. layer-compat failures
|
|
1378
|
+
// during construction), so this can be called before either is
|
|
1379
|
+
// assigned. Optional chains keep that window safe.
|
|
1380
|
+
this._deltaManager?.readOnlyInfo.readonly === true ||
|
|
1381
|
+
this.pendingStateManager?.isApplyingStashedOps === true;
|
|
1364
1382
|
|
|
1365
1383
|
/**
|
|
1366
1384
|
* Current session schema - defines what options are on & off.
|
|
@@ -1597,6 +1615,8 @@ export class ContainerRuntime
|
|
|
1597
1615
|
|
|
1598
1616
|
private readonly extensions = new Map<ContainerExtensionId, ExtensionEntry>();
|
|
1599
1617
|
|
|
1618
|
+
public readonly baseLogger: ITelemetryBaseLogger;
|
|
1619
|
+
|
|
1600
1620
|
/***/
|
|
1601
1621
|
protected constructor(
|
|
1602
1622
|
context: IContainerContext,
|
|
@@ -1610,7 +1630,7 @@ export class ContainerRuntime
|
|
|
1610
1630
|
private readonly runtimeOptions: Readonly<ContainerRuntimeOptionsInternal>,
|
|
1611
1631
|
private readonly containerScope: FluidObject,
|
|
1612
1632
|
// Create a custom ITelemetryBaseLogger to output telemetry events.
|
|
1613
|
-
|
|
1633
|
+
baseLogger: ITelemetryBaseLogger,
|
|
1614
1634
|
existing: boolean,
|
|
1615
1635
|
|
|
1616
1636
|
blobManagerLoadInfo: IBlobManagerLoadInfo,
|
|
@@ -1663,15 +1683,20 @@ export class ContainerRuntime
|
|
|
1663
1683
|
|
|
1664
1684
|
this.isSnapshotInstanceOfISnapshot = snapshotWithContents !== undefined;
|
|
1665
1685
|
|
|
1666
|
-
this.
|
|
1667
|
-
logger:
|
|
1668
|
-
namespace: "ContainerRuntime",
|
|
1686
|
+
this.baseLogger = createChildLogger({
|
|
1687
|
+
logger: baseLogger,
|
|
1669
1688
|
properties: {
|
|
1670
|
-
|
|
1671
|
-
inStagingMode: this.inStagingMode,
|
|
1689
|
+
error: {
|
|
1690
|
+
inStagingMode: () => this.inStagingMode,
|
|
1691
|
+
isApplyingStashedOps: () => this.pendingStateManager?.isApplyingStashedOps,
|
|
1692
|
+
isReadOnly: () => this.isReadOnly(),
|
|
1672
1693
|
},
|
|
1673
1694
|
},
|
|
1674
1695
|
});
|
|
1696
|
+
this.mc = createChildMonitoringContext({
|
|
1697
|
+
logger: this.baseLogger,
|
|
1698
|
+
namespace: "ContainerRuntime",
|
|
1699
|
+
});
|
|
1675
1700
|
|
|
1676
1701
|
// Validate that the Loader is compatible with this Runtime.
|
|
1677
1702
|
const maybeLoaderCompatDetailsForRuntime = context as FluidObject<ILayerCompatDetails>;
|
|
@@ -1840,6 +1865,18 @@ export class ContainerRuntime
|
|
|
1840
1865
|
},
|
|
1841
1866
|
pendingRuntimeState?.pending,
|
|
1842
1867
|
this.baseLogger,
|
|
1868
|
+
{
|
|
1869
|
+
// PSM has cleared `isApplyingStashedOps`; `isReadOnly()` now
|
|
1870
|
+
// reflects the network-readonly state again. Fan out so DDSes
|
|
1871
|
+
// know they can submit once more. No open hook is needed —
|
|
1872
|
+
// the apply window opens before `channelCollection` exists,
|
|
1873
|
+
// so a fanout there would be a no-op; data stores instead
|
|
1874
|
+
// pick up the initial readonly state from `isReadOnly()`
|
|
1875
|
+
// when they're first asked.
|
|
1876
|
+
onAfterStashedOpsApplied: () => {
|
|
1877
|
+
this.notifyReadOnlyState();
|
|
1878
|
+
},
|
|
1879
|
+
},
|
|
1843
1880
|
);
|
|
1844
1881
|
|
|
1845
1882
|
let outerDeltaManager: IDeltaManagerFull = this.innerDeltaManager;
|
|
@@ -2187,6 +2224,14 @@ export class ContainerRuntime
|
|
|
2187
2224
|
telemetryDocumentId: this.telemetryDocumentId,
|
|
2188
2225
|
groupedBatchingEnabled: this.groupedBatchingEnabled,
|
|
2189
2226
|
initialSequenceNumber: this.deltaManager.initialSequenceNumber,
|
|
2227
|
+
// Number of ops since the last summary that this client is aware of (including ops still
|
|
2228
|
+
// queued for processing). Computed as the gap between the latest known op sequence number
|
|
2229
|
+
// and the sequence number of the message at which the last summary was taken (per snapshot
|
|
2230
|
+
// metadata). Falls back to lastKnownSeqNumber when no prior summary message is recorded
|
|
2231
|
+
// (e.g. new container or older snapshot without metadata).
|
|
2232
|
+
numUnsummarizedOps:
|
|
2233
|
+
this.deltaManager.lastKnownSeqNumber -
|
|
2234
|
+
(this.messageAtLastSummary?.sequenceNumber ?? 0),
|
|
2190
2235
|
minVersionForCollab: this.minVersionForCollab,
|
|
2191
2236
|
// logging hardware telemetry
|
|
2192
2237
|
deviceSpec: { ...getDeviceSpec() },
|
|
@@ -2805,6 +2850,28 @@ export class ContainerRuntime
|
|
|
2805
2850
|
return;
|
|
2806
2851
|
}
|
|
2807
2852
|
|
|
2853
|
+
// Invariant: the canSendOps edge in `setConnectionStateCore` — the
|
|
2854
|
+
// only caller of this method — cannot fire while
|
|
2855
|
+
// `applyStashedOpsAt` is in flight, because the loader awaits the
|
|
2856
|
+
// apply before transitioning the runtime to a write-capable
|
|
2857
|
+
// connection. If this assert ever fires, that contract has changed
|
|
2858
|
+
// and the submit guard at `submit()` would catch a runtime-internal
|
|
2859
|
+
// resubmit (`Rejoin`, `GC`, `FluidDataStoreOp`) for an op type
|
|
2860
|
+
// outside the apply-window allowlist.
|
|
2861
|
+
//
|
|
2862
|
+
// The precondition is held by the load sequence: `loadRuntime2`
|
|
2863
|
+
// awaits `pendingStateManager.applyStashedOpsAt(...)` before
|
|
2864
|
+
// returning the runtime, and the loader gates `setLoaded` on that
|
|
2865
|
+
// completion before any write-capable connection edge fires. A
|
|
2866
|
+
// maintainer reordering either sequence (or adding a new
|
|
2867
|
+
// `canSendOps` edge that fires before the apply resolves) is what
|
|
2868
|
+
// would trip this assert.
|
|
2869
|
+
// @see {@link ContainerRuntime.loadRuntime2} (awaits `applyStashedOpsAt`)
|
|
2870
|
+
assert(
|
|
2871
|
+
!this.pendingStateManager.isApplyingStashedOps,
|
|
2872
|
+
0xd01 /* replayPendingStates must not be called during stashed-op apply window */,
|
|
2873
|
+
);
|
|
2874
|
+
|
|
2808
2875
|
// Replaying is an internal operation and we don't want to generate noise while doing it.
|
|
2809
2876
|
// So temporarily disable dirty state change events, and save the old state.
|
|
2810
2877
|
// When we're done, we'll emit the event if the state changed.
|
|
@@ -2920,8 +2987,14 @@ export class ContainerRuntime
|
|
|
2920
2987
|
}
|
|
2921
2988
|
}
|
|
2922
2989
|
|
|
2923
|
-
|
|
2924
|
-
|
|
2990
|
+
// Boolean payload from the `"readonly"` delta-manager event is intentionally
|
|
2991
|
+
// ignored — `isReadOnly()` aggregates delta-manager readonly with the PSM
|
|
2992
|
+
// apply window, and that aggregation is the source of truth for fanout.
|
|
2993
|
+
// `channelCollection?.` guards against future wiring changes; both callers
|
|
2994
|
+
// today (the `"readonly"` listener and `onAfterStashedOpsApplied`) fire
|
|
2995
|
+
// after `channelCollection` is assigned.
|
|
2996
|
+
private readonly notifyReadOnlyState = (_readonly?: boolean): void =>
|
|
2997
|
+
this.channelCollection?.notifyReadOnlyState(this.isReadOnly());
|
|
2925
2998
|
|
|
2926
2999
|
public setConnectionState(canSendOps: boolean, clientId?: string): void {
|
|
2927
3000
|
this.setConnectionStateToConnectedOrDisconnected(canSendOps, clientId);
|
|
@@ -4078,7 +4151,7 @@ export class ContainerRuntime
|
|
|
4078
4151
|
/**
|
|
4079
4152
|
* Logger to use for correlated summary events
|
|
4080
4153
|
*/
|
|
4081
|
-
summaryLogger?:
|
|
4154
|
+
summaryLogger?: TelemetryLoggerExt;
|
|
4082
4155
|
/**
|
|
4083
4156
|
* True to run garbage collection before summarizing; defaults to true
|
|
4084
4157
|
*/
|
|
@@ -4287,7 +4360,7 @@ export class ContainerRuntime
|
|
|
4287
4360
|
/**
|
|
4288
4361
|
* Logger to use for logging GC events
|
|
4289
4362
|
*/
|
|
4290
|
-
logger?:
|
|
4363
|
+
logger?: TelemetryLoggerExt;
|
|
4291
4364
|
/**
|
|
4292
4365
|
* True to run GC sweep phase after the mark phase
|
|
4293
4366
|
*/
|
|
@@ -4718,7 +4791,7 @@ export class ContainerRuntime
|
|
|
4718
4791
|
* @returns failed summarize result (IBaseSummarizeResult) if summary should be failed, undefined otherwise.
|
|
4719
4792
|
*/
|
|
4720
4793
|
private async shouldFailSummaryOnPendingOps(
|
|
4721
|
-
logger:
|
|
4794
|
+
logger: TelemetryLoggerExt,
|
|
4722
4795
|
referenceSequenceNumber: number,
|
|
4723
4796
|
minimumSequenceNumber: number,
|
|
4724
4797
|
finalAttempt: boolean,
|
|
@@ -4840,6 +4913,52 @@ export class ContainerRuntime
|
|
|
4840
4913
|
): void {
|
|
4841
4914
|
this.verifyNotClosed();
|
|
4842
4915
|
|
|
4916
|
+
// Nothing should be submitting while we're replaying stashed ops.
|
|
4917
|
+
// The runtime is readonly during the apply window (see
|
|
4918
|
+
// `PendingStateManager._applyLifecycle`), so a compliant DDS skips
|
|
4919
|
+
// submits. If we land here anyway, a DDS bypassed the readonly gate
|
|
4920
|
+
// (e.g. a realize-time write that doesn't consult `readOnly`) and
|
|
4921
|
+
// produced a local op that has no counterpart in the saved-op
|
|
4922
|
+
// replay — we cannot reconcile the mismatch, so fail fatally. We
|
|
4923
|
+
// check here (rather than at flush) because outbox flushes are
|
|
4924
|
+
// deferred and the apply window could close before the offending op
|
|
4925
|
+
// reaches the pending queue.
|
|
4926
|
+
//
|
|
4927
|
+
// Allowlist: `BlobAttach` is a runtime-internal op type that may
|
|
4928
|
+
// legitimately fire during apply — produced by `sharePendingBlobs`,
|
|
4929
|
+
// which is invoked from `loadRuntime2` before `applyStashedOpsAt`
|
|
4930
|
+
// resolves. `IdAllocation` is not in this allowlist because the
|
|
4931
|
+
// assert at 0x9a5 below enforces that it never reaches `submit()`
|
|
4932
|
+
// at all; treating that assert as the single source of truth.
|
|
4933
|
+
//
|
|
4934
|
+
// Always surface the error event to telemetry on a bypass so we can
|
|
4935
|
+
// attribute incidents regardless of the on-switch state. The
|
|
4936
|
+
// `EnableSubmitDuringStashedApplyThrow` config opts in to the
|
|
4937
|
+
// throw + container close; by default we log only, so a first- or
|
|
4938
|
+
// third-party DDS that quietly bypasses the readonly gate in
|
|
4939
|
+
// production is observable without escalating to a fatal close.
|
|
4940
|
+
if (
|
|
4941
|
+
this.pendingStateManager.isApplyingStashedOps &&
|
|
4942
|
+
containerRuntimeMessage.type !== ContainerMessageType.BlobAttach
|
|
4943
|
+
) {
|
|
4944
|
+
const error = new UsageError("Local op submitted during stashed-op apply window", {
|
|
4945
|
+
messageType: containerRuntimeMessage.type,
|
|
4946
|
+
});
|
|
4947
|
+
this.mc.logger.sendErrorEvent({ eventName: "SubmitDuringStashedOpApply" }, error);
|
|
4948
|
+
if (
|
|
4949
|
+
this.mc.config.getBoolean(
|
|
4950
|
+
"Fluid.ContainerRuntime.EnableSubmitDuringStashedApplyThrow",
|
|
4951
|
+
) === true
|
|
4952
|
+
) {
|
|
4953
|
+
// Close the container before throwing so the "throw + close"
|
|
4954
|
+
// contract is enforced by this code path rather than by
|
|
4955
|
+
// whichever caller happens to wrap the throw in `.catch(closeFn)`.
|
|
4956
|
+
// `closeFn` is idempotent; a caller that also closes won't double-close.
|
|
4957
|
+
this.closeFn(error);
|
|
4958
|
+
throw error;
|
|
4959
|
+
}
|
|
4960
|
+
}
|
|
4961
|
+
|
|
4843
4962
|
// There should be no ops in detached container state!
|
|
4844
4963
|
assert(
|
|
4845
4964
|
this.attachState !== AttachState.Detached,
|
|
@@ -5228,7 +5347,7 @@ export class ContainerRuntime
|
|
|
5228
5347
|
private async fetchLatestSnapshotAndMaybeClose(
|
|
5229
5348
|
targetRefSeq: number,
|
|
5230
5349
|
targetAckHandle: string,
|
|
5231
|
-
logger:
|
|
5350
|
+
logger: TelemetryLoggerExt,
|
|
5232
5351
|
): Promise<void> {
|
|
5233
5352
|
const fetchedSnapshotRefSeq = await PerformanceEvent.timedExecAsync(
|
|
5234
5353
|
logger,
|
package/src/dataStore.ts
CHANGED
|
@@ -13,7 +13,7 @@ import type {
|
|
|
13
13
|
IFluidDataStoreChannel,
|
|
14
14
|
} from "@fluidframework/runtime-definitions/internal";
|
|
15
15
|
import {
|
|
16
|
-
type
|
|
16
|
+
type TelemetryLoggerExt,
|
|
17
17
|
TelemetryDataTag,
|
|
18
18
|
UsageError,
|
|
19
19
|
} from "@fluidframework/telemetry-utils/internal";
|
|
@@ -58,7 +58,7 @@ export const channelToDataStore = (
|
|
|
58
58
|
fluidDataStoreChannel: IFluidDataStoreChannel,
|
|
59
59
|
internalId: string,
|
|
60
60
|
channelCollection: ChannelCollection,
|
|
61
|
-
logger:
|
|
61
|
+
logger: TelemetryLoggerExt,
|
|
62
62
|
): IDataStore => new DataStore(fluidDataStoreChannel, internalId, channelCollection, logger);
|
|
63
63
|
|
|
64
64
|
enum AliasState {
|
|
@@ -194,7 +194,7 @@ class DataStore implements IDataStore {
|
|
|
194
194
|
private readonly fluidDataStoreChannel: IFluidDataStoreChannel,
|
|
195
195
|
private readonly internalId: string,
|
|
196
196
|
private readonly channelCollection: ChannelCollection,
|
|
197
|
-
private readonly logger:
|
|
197
|
+
private readonly logger: TelemetryLoggerExt,
|
|
198
198
|
private readonly parentContext = channelCollection.parentContext,
|
|
199
199
|
) {
|
|
200
200
|
this.pendingAliases = channelCollection.pendingAliases;
|
package/src/dataStoreContexts.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import type { IDisposable, ITelemetryBaseLogger } from "@fluidframework/core-interfaces";
|
|
7
7
|
import { assert, Deferred, Lazy } from "@fluidframework/core-utils/internal";
|
|
8
8
|
import {
|
|
9
|
-
type
|
|
9
|
+
type TelemetryLoggerExt,
|
|
10
10
|
createChildLogger,
|
|
11
11
|
} from "@fluidframework/telemetry-utils/internal";
|
|
12
12
|
|
|
@@ -68,7 +68,7 @@ export class DataStoreContexts
|
|
|
68
68
|
}
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
-
private readonly _logger:
|
|
71
|
+
private readonly _logger: TelemetryLoggerExt;
|
|
72
72
|
|
|
73
73
|
constructor(baseLogger: ITelemetryBaseLogger) {
|
|
74
74
|
this._logger = createChildLogger({ logger: baseLogger });
|
package/src/deltaScheduler.ts
CHANGED
|
@@ -7,10 +7,7 @@ import { performanceNow, type TypedEventEmitter } from "@fluid-internal/client-u
|
|
|
7
7
|
import type { IDeltaManagerFull } from "@fluidframework/container-definitions/internal";
|
|
8
8
|
import type { ISequencedDocumentMessage } from "@fluidframework/driver-definitions/internal";
|
|
9
9
|
import type { IContainerRuntimeBaseEvents } from "@fluidframework/runtime-definitions/internal";
|
|
10
|
-
import {
|
|
11
|
-
type ITelemetryLoggerExt,
|
|
12
|
-
formatTick,
|
|
13
|
-
} from "@fluidframework/telemetry-utils/internal";
|
|
10
|
+
import { type TelemetryLoggerExt, formatTick } from "@fluidframework/telemetry-utils/internal";
|
|
14
11
|
|
|
15
12
|
/**
|
|
16
13
|
* DeltaScheduler is responsible for the scheduling of inbound delta queue in cases where there
|
|
@@ -55,7 +52,7 @@ export class DeltaScheduler {
|
|
|
55
52
|
constructor(
|
|
56
53
|
private readonly deltaManager: IDeltaManagerFull,
|
|
57
54
|
private readonly runtimeEventsEmitter: TypedEventEmitter<IContainerRuntimeBaseEvents>,
|
|
58
|
-
private readonly logger:
|
|
55
|
+
private readonly logger: TelemetryLoggerExt,
|
|
59
56
|
) {
|
|
60
57
|
this.deltaManager.inbound.on("idle", this.inboundQueueIdle);
|
|
61
58
|
runtimeEventsEmitter.on("batchBegin", this.batchBegin);
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
responseToException,
|
|
20
20
|
} from "@fluidframework/runtime-utils/internal";
|
|
21
21
|
import {
|
|
22
|
-
type
|
|
22
|
+
type TelemetryLoggerExt,
|
|
23
23
|
DataProcessingError,
|
|
24
24
|
type MonitoringContext,
|
|
25
25
|
PerformanceEvent,
|
|
@@ -498,7 +498,7 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
498
498
|
/**
|
|
499
499
|
* Logger to use for logging GC events
|
|
500
500
|
*/
|
|
501
|
-
logger?:
|
|
501
|
+
logger?: TelemetryLoggerExt;
|
|
502
502
|
/**
|
|
503
503
|
* True to run GC sweep phase after the mark phase
|
|
504
504
|
*/
|
|
@@ -605,7 +605,7 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
605
605
|
private async runGC(
|
|
606
606
|
fullGC: boolean,
|
|
607
607
|
currentReferenceTimestampMs: number,
|
|
608
|
-
logger:
|
|
608
|
+
logger: TelemetryLoggerExt,
|
|
609
609
|
): Promise<IGCStats> {
|
|
610
610
|
// 1. Generate / analyze the runtime's reference graph.
|
|
611
611
|
// Get the reference graph (gcData) and run GC algorithm to get referenced / unreferenced nodes.
|
|
@@ -796,7 +796,7 @@ export class GarbageCollector implements IGarbageCollector {
|
|
|
796
796
|
private findAllNodesReferencedBetweenGCs(
|
|
797
797
|
currentGCData: IGarbageCollectionData,
|
|
798
798
|
previousGCData: IGarbageCollectionData | undefined,
|
|
799
|
-
logger:
|
|
799
|
+
logger: TelemetryLoggerExt,
|
|
800
800
|
): string[] | undefined {
|
|
801
801
|
// If we haven't run GC before there is nothing to do.
|
|
802
802
|
// No previousGCData, means nothing is unreferenced, and there are no reference state trackers to clear
|
package/src/gc/gcDefinitions.ts
CHANGED
|
@@ -14,7 +14,7 @@ import type {
|
|
|
14
14
|
} from "@fluidframework/runtime-definitions/internal";
|
|
15
15
|
import type { ReadAndParseBlob } from "@fluidframework/runtime-utils/internal";
|
|
16
16
|
import type {
|
|
17
|
-
|
|
17
|
+
TelemetryLoggerExt,
|
|
18
18
|
ITelemetryPropertiesExt,
|
|
19
19
|
} from "@fluidframework/telemetry-utils/internal";
|
|
20
20
|
|
|
@@ -396,7 +396,7 @@ export interface IGarbageCollector {
|
|
|
396
396
|
*/
|
|
397
397
|
collectGarbage(
|
|
398
398
|
options: {
|
|
399
|
-
logger?:
|
|
399
|
+
logger?: TelemetryLoggerExt;
|
|
400
400
|
runSweep?: boolean;
|
|
401
401
|
fullGC?: boolean;
|
|
402
402
|
},
|
|
@@ -505,7 +505,7 @@ export interface IGarbageCollectorCreateParams {
|
|
|
505
505
|
readonly closeFn: (error: ICriticalContainerError) => void;
|
|
506
506
|
|
|
507
507
|
readonly gcOptions: IGCRuntimeOptions;
|
|
508
|
-
readonly baseLogger:
|
|
508
|
+
readonly baseLogger: TelemetryLoggerExt;
|
|
509
509
|
readonly existing: boolean;
|
|
510
510
|
|
|
511
511
|
readonly metadata: IContainerRuntimeMetadata | undefined;
|
package/src/gc/gcTelemetry.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import type { Tagged } from "@fluidframework/core-interfaces";
|
|
7
7
|
import type { IGarbageCollectionData } from "@fluidframework/runtime-definitions/internal";
|
|
8
8
|
import {
|
|
9
|
-
type
|
|
9
|
+
type TelemetryLoggerExt,
|
|
10
10
|
type MonitoringContext,
|
|
11
11
|
generateStack,
|
|
12
12
|
tagCodeArtifacts,
|
|
@@ -350,7 +350,7 @@ export class GCTelemetryTracker {
|
|
|
350
350
|
currentGCData: IGarbageCollectionData,
|
|
351
351
|
previousGCData: IGarbageCollectionData,
|
|
352
352
|
explicitReferences: Map<string, string[]>,
|
|
353
|
-
logger:
|
|
353
|
+
logger: TelemetryLoggerExt,
|
|
354
354
|
): void {
|
|
355
355
|
for (const [nodeId, currentOutboundRoutes] of Object.entries(currentGCData.gcNodes)) {
|
|
356
356
|
const previousRoutes = previousGCData.gcNodes[nodeId] ?? [];
|
|
@@ -395,7 +395,7 @@ export class GCTelemetryTracker {
|
|
|
395
395
|
* Log events that are pending in pendingEventsQueue. This is called after GC runs in the summarizer client
|
|
396
396
|
* so that the state of an unreferenced node is updated.
|
|
397
397
|
*/
|
|
398
|
-
public async logPendingEvents(logger:
|
|
398
|
+
public async logPendingEvents(logger: TelemetryLoggerExt): Promise<void> {
|
|
399
399
|
// Events sent come only from the summarizer client. In between summaries, events are pushed to a queue and at
|
|
400
400
|
// summary time they are then logged.
|
|
401
401
|
// Events generated:
|
|
@@ -9,7 +9,7 @@ import { assert } from "@fluidframework/core-utils/internal";
|
|
|
9
9
|
import type { ISequencedDocumentMessage } from "@fluidframework/driver-definitions/internal";
|
|
10
10
|
import { isRuntimeMessage } from "@fluidframework/driver-utils/internal";
|
|
11
11
|
import {
|
|
12
|
-
type
|
|
12
|
+
type TelemetryLoggerExt,
|
|
13
13
|
DataCorruptionError,
|
|
14
14
|
DataProcessingError,
|
|
15
15
|
extractSafePropertiesFromMessage,
|
|
@@ -38,7 +38,7 @@ export class InboundBatchAggregator {
|
|
|
38
38
|
constructor(
|
|
39
39
|
private readonly deltaManager: IDeltaManagerFull,
|
|
40
40
|
private readonly getClientId: () => string | undefined,
|
|
41
|
-
private readonly logger:
|
|
41
|
+
private readonly logger: TelemetryLoggerExt,
|
|
42
42
|
) {
|
|
43
43
|
// Listen for updates and peek at the inbound
|
|
44
44
|
this.deltaManager.inbound.on("push", this.trackPending);
|
|
@@ -9,7 +9,7 @@ import { assert } from "@fluidframework/core-utils/internal";
|
|
|
9
9
|
import {
|
|
10
10
|
DataProcessingError,
|
|
11
11
|
createChildLogger,
|
|
12
|
-
type
|
|
12
|
+
type TelemetryLoggerExt,
|
|
13
13
|
} from "@fluidframework/telemetry-utils/internal";
|
|
14
14
|
import { compress } from "lz4js";
|
|
15
15
|
|
|
@@ -25,7 +25,7 @@ import { estimateSocketSize } from "./outbox.js";
|
|
|
25
25
|
* Use opGroupingManager to group a batch into a singleton batch suitable for compression.
|
|
26
26
|
*/
|
|
27
27
|
export class OpCompressor {
|
|
28
|
-
private readonly logger:
|
|
28
|
+
private readonly logger: TelemetryLoggerExt;
|
|
29
29
|
|
|
30
30
|
constructor(logger: ITelemetryBaseLogger) {
|
|
31
31
|
this.logger = createChildLogger({ logger, namespace: "OpCompressor" });
|
|
@@ -9,7 +9,7 @@ import { assert } from "@fluidframework/core-utils/internal";
|
|
|
9
9
|
import type { ISequencedDocumentMessage } from "@fluidframework/driver-definitions/internal";
|
|
10
10
|
import {
|
|
11
11
|
createChildLogger,
|
|
12
|
-
type
|
|
12
|
+
type TelemetryLoggerExt,
|
|
13
13
|
} from "@fluidframework/telemetry-utils/internal";
|
|
14
14
|
import { decompress } from "lz4js";
|
|
15
15
|
|
|
@@ -40,7 +40,7 @@ export class OpDecompressor {
|
|
|
40
40
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
41
41
|
private rootMessageContents: any | undefined;
|
|
42
42
|
private processedCount = 0;
|
|
43
|
-
private readonly logger:
|
|
43
|
+
private readonly logger: TelemetryLoggerExt;
|
|
44
44
|
|
|
45
45
|
constructor(logger: ITelemetryBaseLogger) {
|
|
46
46
|
this.logger = createChildLogger({ logger, namespace: "OpDecompressor" });
|
|
@@ -8,7 +8,7 @@ import { assert } from "@fluidframework/core-utils/internal";
|
|
|
8
8
|
import type { ISequencedDocumentMessage } from "@fluidframework/driver-definitions/internal";
|
|
9
9
|
import {
|
|
10
10
|
createChildLogger,
|
|
11
|
-
type
|
|
11
|
+
type TelemetryLoggerExt,
|
|
12
12
|
} from "@fluidframework/telemetry-utils/internal";
|
|
13
13
|
|
|
14
14
|
import type {
|
|
@@ -65,7 +65,7 @@ export interface EmptyGroupedBatch {
|
|
|
65
65
|
|
|
66
66
|
export class OpGroupingManager {
|
|
67
67
|
static readonly groupedBatchOp = "groupedBatch";
|
|
68
|
-
private readonly logger:
|
|
68
|
+
private readonly logger: TelemetryLoggerExt;
|
|
69
69
|
|
|
70
70
|
constructor(
|
|
71
71
|
private readonly config: OpGroupingManagerConfig,
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
DataCorruptionError,
|
|
12
12
|
createChildLogger,
|
|
13
13
|
extractSafePropertiesFromMessage,
|
|
14
|
-
type
|
|
14
|
+
type TelemetryLoggerExt,
|
|
15
15
|
} from "@fluidframework/telemetry-utils/internal";
|
|
16
16
|
|
|
17
17
|
import {
|
|
@@ -45,7 +45,7 @@ function isChunkedContents(contents: unknown): contents is IChunkedContents {
|
|
|
45
45
|
export class OpSplitter {
|
|
46
46
|
// Local copy of incomplete received chunks.
|
|
47
47
|
private readonly chunkMap: Map<string, string[]>;
|
|
48
|
-
private readonly logger:
|
|
48
|
+
private readonly logger: TelemetryLoggerExt;
|
|
49
49
|
|
|
50
50
|
constructor(
|
|
51
51
|
chunks: [string, string[]][],
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
UsageError,
|
|
15
15
|
createChildLogger,
|
|
16
16
|
type IFluidErrorBase,
|
|
17
|
-
type
|
|
17
|
+
type TelemetryLoggerExt,
|
|
18
18
|
} from "@fluidframework/telemetry-utils/internal";
|
|
19
19
|
|
|
20
20
|
import type { ICompressionRuntimeOptions } from "../compressionDefinitions.js";
|
|
@@ -194,7 +194,7 @@ export const estimateSocketSize = (batch: OutboundBatch): number => {
|
|
|
194
194
|
* to support slight variation in semantics for each batch (e.g. support for rebasing or grouping).
|
|
195
195
|
*/
|
|
196
196
|
export class Outbox {
|
|
197
|
-
private readonly logger:
|
|
197
|
+
private readonly logger: TelemetryLoggerExt;
|
|
198
198
|
private readonly mainBatch: BatchManager;
|
|
199
199
|
private readonly blobAttachBatch: BatchManager;
|
|
200
200
|
private batchRebasesToReport = 5;
|
package/src/packageVersion.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import type { IDisposable, ITelemetryBaseLogger } from "@fluidframework/core-interfaces";
|
|
7
7
|
import { assert, Lazy } from "@fluidframework/core-utils/internal";
|
|
8
8
|
import {
|
|
9
|
-
type
|
|
9
|
+
type TelemetryLoggerExt,
|
|
10
10
|
DataProcessingError,
|
|
11
11
|
LoggingError,
|
|
12
12
|
extractSafePropertiesFromMessage,
|
|
@@ -142,6 +142,25 @@ export interface IRuntimeStateHandler {
|
|
|
142
142
|
isAttached: () => boolean;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Optional hooks invoked at the close of the stashed-op apply lifecycle.
|
|
147
|
+
*
|
|
148
|
+
* `onAfterStashedOpsApplied` fires synchronously the first time
|
|
149
|
+
* `initialMessages` drains during `applyStashedOpsAt`, immediately after
|
|
150
|
+
* `isApplyingStashedOps` flips to `false`. Fires at most once per PSM
|
|
151
|
+
* lifetime. If an apply throws, control never reaches the close site and
|
|
152
|
+
* the hook is not invoked — load is fatal in that case.
|
|
153
|
+
*
|
|
154
|
+
* No corresponding open hook is exposed. The apply window is opened eagerly
|
|
155
|
+
* in the PSM constructor, but at that point `ContainerRuntime` has not yet
|
|
156
|
+
* wired up the downstream observers (`channelCollection` is undefined), so a
|
|
157
|
+
* fanout fired from the constructor would be a no-op. Consumers that care
|
|
158
|
+
* about the open transition can read `isApplyingStashedOps` directly.
|
|
159
|
+
*/
|
|
160
|
+
export interface PendingStateManagerHooks {
|
|
161
|
+
onAfterStashedOpsApplied?: () => void;
|
|
162
|
+
}
|
|
163
|
+
|
|
145
164
|
function isEmptyBatchPendingMessage(message: IPendingMessageFromStash): boolean {
|
|
146
165
|
const content = JSON.parse(message.content) as Partial<EmptyGroupedBatch>;
|
|
147
166
|
return content.type === "groupedBatch" && content.contents?.length === 0;
|
|
@@ -366,17 +385,62 @@ export class PendingStateManager implements IDisposable {
|
|
|
366
385
|
};
|
|
367
386
|
}
|
|
368
387
|
|
|
369
|
-
private readonly logger:
|
|
388
|
+
private readonly logger: TelemetryLoggerExt;
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* One-way lifecycle of the stashed-op apply window: `ended` → `applying` → `ended`.
|
|
392
|
+
*
|
|
393
|
+
* Default is `ended` — no stashed state means there's nothing to apply, so the window is
|
|
394
|
+
* closed before it ever opens. `ended` → `applying` happens in the constructor when
|
|
395
|
+
* stashed state is present (i.e. `initialMessages` is non-empty at construction). The
|
|
396
|
+
* open is eager so the runtime is readonly from the moment any DDS could possibly
|
|
397
|
+
* observe it. `applying` → `ended` happens the first time {@link applyStashedOpsAt}
|
|
398
|
+
* drains `initialMessages`. After that, local edits are safe — they queue FIFO behind
|
|
399
|
+
* any remaining `pendingMessages`, preserving server-side ordering.
|
|
400
|
+
*
|
|
401
|
+
* The window never reopens. After the close, subsequent `applyStashedOpsAt` calls (e.g.
|
|
402
|
+
* from late `notifyOpReplay`s) early-return at the empty guard.
|
|
403
|
+
*
|
|
404
|
+
* `pendingMessages` state is intentionally NOT part of the close condition. Those
|
|
405
|
+
* entries are drained transparently by {@link replayPendingStates} on connect via
|
|
406
|
+
* resubmit (each pop is matched by a fresh push), so the queue size is conserved across
|
|
407
|
+
* resubmit and DDSes can't distinguish a resubmit-ack from a normal ack. Holding the
|
|
408
|
+
* window open through resubmit would force resubmits to run while the runtime is
|
|
409
|
+
* readonly, which is the inverse of what we want ("never resubmit during apply stashed
|
|
410
|
+
* ops").
|
|
411
|
+
*
|
|
412
|
+
* An apply error leaves the lifecycle at `applying` because the queue isn't drained.
|
|
413
|
+
* That's fine: an error here is fatal for the load, the container is unusable, and
|
|
414
|
+
* there's no state to restore.
|
|
415
|
+
*/
|
|
416
|
+
private _applyLifecycle: "applying" | "ended" = "ended";
|
|
417
|
+
public get isApplyingStashedOps(): boolean {
|
|
418
|
+
return this._applyLifecycle === "applying";
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private readonly hooks: PendingStateManagerHooks;
|
|
370
422
|
|
|
371
423
|
constructor(
|
|
372
424
|
private readonly stateHandler: IRuntimeStateHandler,
|
|
373
425
|
stashedLocalState: IPendingLocalState | undefined,
|
|
374
426
|
logger: ITelemetryBaseLogger,
|
|
427
|
+
hooks: PendingStateManagerHooks = {},
|
|
375
428
|
) {
|
|
376
429
|
this.logger = createChildLogger({ logger });
|
|
430
|
+
this.hooks = hooks;
|
|
377
431
|
if (stashedLocalState?.pendingStates) {
|
|
378
432
|
this.initialMessages.push(...stashedLocalState.pendingStates);
|
|
379
433
|
}
|
|
434
|
+
// Open the apply window eagerly if there is any stashed work. The
|
|
435
|
+
// runtime is readonly while `isApplyingStashedOps` is true (see
|
|
436
|
+
// `ContainerRuntime.isReadOnly`); compliant DDSes consult `readOnly`
|
|
437
|
+
// at realize time and skip submits. No fanout fires here — downstream
|
|
438
|
+
// observers (`channelCollection`) are not yet constructed at this
|
|
439
|
+
// point in the runtime constructor, and the first real readonly read
|
|
440
|
+
// happens after the constructor returns.
|
|
441
|
+
if (!this.initialMessages.isEmpty()) {
|
|
442
|
+
this._applyLifecycle = "applying";
|
|
443
|
+
}
|
|
380
444
|
}
|
|
381
445
|
|
|
382
446
|
public get disposed(): boolean {
|
|
@@ -451,6 +515,10 @@ export class PendingStateManager implements IDisposable {
|
|
|
451
515
|
* @param seqNum - Sequence number at which to apply ops. Will apply all ops if seqNum is undefined.
|
|
452
516
|
*/
|
|
453
517
|
public async applyStashedOpsAt(seqNum?: number): Promise<void> {
|
|
518
|
+
if (this.initialMessages.isEmpty()) {
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
454
522
|
// apply stashed ops at sequence number
|
|
455
523
|
while (!this.initialMessages.isEmpty()) {
|
|
456
524
|
if (seqNum !== undefined) {
|
|
@@ -497,6 +565,16 @@ export class PendingStateManager implements IDisposable {
|
|
|
497
565
|
throw DataProcessingError.wrapIfUnrecognized(error, "applyStashedOp", nextMessage);
|
|
498
566
|
}
|
|
499
567
|
}
|
|
568
|
+
|
|
569
|
+
// The apply window was opened eagerly in the constructor when there
|
|
570
|
+
// was any stashed work. We close it on full successful drain only.
|
|
571
|
+
// If an apply throws above, control never reaches here and the
|
|
572
|
+
// lifecycle stays at "applying" — the load is fatal so there's no
|
|
573
|
+
// recoverable state.
|
|
574
|
+
if (this._applyLifecycle === "applying" && this.initialMessages.isEmpty()) {
|
|
575
|
+
this._applyLifecycle = "ended";
|
|
576
|
+
this.hooks.onAfterStashedOpsApplied?.();
|
|
577
|
+
}
|
|
500
578
|
}
|
|
501
579
|
|
|
502
580
|
/**
|