Skip to content

Commit 64d83f1

Browse files
markfieldsanthony-murphy-agent
authored andcommitted
Fix assert 0xb7b (microsoft#25735)
There's a corner case where processing incoming ops (join/leave ops in particular) can trigger a connection state transition which can trigger a flush (but there are no changes to flush), and assert 0xb7b fires - it asserts that we should never flush while processing incoming ops. This protects against reentrancy, which is not an issue in this case. The fix is to only assert if we have something to flush.
1 parent c856a1d commit 64d83f1

File tree

2 files changed

+62
-0
lines changed

2 files changed

+62
-0
lines changed

packages/runtime/container-runtime/src/opLifecycle/outbox.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,16 @@ export class Outbox {
365365
* @param resubmitInfo - Key information when flushing a resubmitted batch. Undefined means this is not resubmit.
366366
*/
367367
public flush(resubmitInfo?: BatchResubmitInfo): void {
368+
// We have nothing to flush if all batchManagers are empty, and we we're not needing to resubmit an empty batch placeholder
369+
if (
370+
this.idAllocationBatch.empty &&
371+
this.blobAttachBatch.empty &&
372+
this.mainBatch.empty &&
373+
resubmitInfo?.batchId === undefined
374+
) {
375+
return;
376+
}
377+
368378
assert(
369379
!this.isContextReentrant(),
370380
0xb7b /* Flushing must not happen while incoming changes are being processed */,

packages/runtime/container-runtime/src/test/opLifecycle/outbox.spec.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type {
1717
ISequencedDocumentMessage,
1818
} from "@fluidframework/driver-definitions/internal";
1919
import { MockLogger } from "@fluidframework/telemetry-utils/internal";
20+
import { validateAssertionError } from "@fluidframework/test-runtime-utils/internal";
2021

2122
import type { ICompressionRuntimeOptions } from "../../compressionDefinitions.js";
2223
import { CompressionAlgorithms } from "../../compressionDefinitions.js";
@@ -1092,6 +1093,57 @@ describe("Outbox", () => {
10921093
assert.strictEqual(state.opsResubmitted, opsResubmitted, "unexpected opsResubmitted");
10931094
}
10941095

1096+
it("should not assert when flushing while reentrant with empty batches", () => {
1097+
const outbox = getOutbox({
1098+
context: getMockContext(),
1099+
opGroupingConfig: {
1100+
groupedBatchingEnabled: true,
1101+
},
1102+
});
1103+
1104+
// Mark context as reentrant
1105+
state.isReentrant = true;
1106+
1107+
// Flush with no messages - should not throw
1108+
assert.doesNotThrow(() => {
1109+
outbox.flush();
1110+
}, "Should not assert when flushing empty batches while reentrant");
1111+
1112+
validateCounts(0, 0, 0);
1113+
});
1114+
1115+
it("should assert when flushing while reentrant with non-empty batches", () => {
1116+
const outbox = getOutbox({
1117+
context: getMockContext(),
1118+
opGroupingConfig: {
1119+
groupedBatchingEnabled: true,
1120+
},
1121+
});
1122+
1123+
const messages = [createMessage(ContainerMessageType.FluidDataStoreOp, "0")];
1124+
1125+
// Submit a message (not reentrant)
1126+
state.isReentrant = false;
1127+
outbox.submit(messages[0]);
1128+
1129+
// Now mark context as reentrant and try to flush - should throw
1130+
state.isReentrant = true;
1131+
1132+
assert.throws(
1133+
() => outbox.flush(),
1134+
(error: Error) => {
1135+
return validateAssertionError(
1136+
error,
1137+
"Flushing must not happen while incoming changes are being processed",
1138+
);
1139+
},
1140+
"Should assert when flushing non-empty batches while reentrant",
1141+
);
1142+
1143+
// Verify nothing was submitted
1144+
validateCounts(0, 0, 0);
1145+
});
1146+
10951147
it("batch has reentrant ops, but grouped batching is off", () => {
10961148
const outbox = getOutbox({
10971149
context: getMockContext(),

0 commit comments

Comments
 (0)