Skip to content

Throw DataCorruptionError for meaningful duplicate batch detections#27668

Open
dannimad wants to merge 2 commits into
mainfrom
throw-on-duplicate
Open

Throw DataCorruptionError for meaningful duplicate batch detections#27668
dannimad wants to merge 2 commits into
mainfrom
throw-on-duplicate

Conversation

@dannimad

Copy link
Copy Markdown
Contributor

Description

Previously, duplicate-batch detection only logged a DuplicateBatch telemetry event and never threw the DataCorruptionError — this was a temporary mitigation for a live service bug that could redeliver batches.

This PR re-enables throwing, but only when either the incoming batch or the previously-recorded batch has an explicit batchId (i.e., it came from a resubmit via PendingStateManager.replayPendingStates), which indicates a genuine duplicate-batch scenario (e.g. container forking) rather than the known service-outage artifact. Duplicates where neither batch has an explicit batchId continue to be log-only.

Updated the Duplicate Batch Detection tests in containerRuntime.spec.ts to assert the new throwing behavior where applicable, and added a new test covering the still-log-only case (no explicit batchId on either side).

Reviewer Guidance

The review process is outlined on this wiki page.

dannimad and others added 2 commits July 10, 2026 02:25
Updates the Duplicate Batch Detection tests to match the new behavior:
processing a duplicate batch now throws a DataCorruptionError when
either the incoming or previously-recorded batch has an explicit
batchId, instead of always being log-only. Adds a new test covering
the still-log-only case where neither batch has an explicit batchId
(the known service-outage scenario). Also adds a changeset.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 10, 2026 02:30
@dannimad dannimad requested a review from a team as a code owner July 10, 2026 02:30

Previously, all detected duplicate batches were only logged via the `DuplicateBatch` telemetry event, and the corresponding `DataCorruptionError` was never thrown. This was a temporary mitigation for a service-side bug that could redeliver batches.

Now, the error is thrown when either the incoming batch or the previously-seen batch has an explicit `batchId` (i.e. the batch was resubmitted, as opposed to a fresh batch whose `batchId` is derived from `clientId` and `batchStartCsn`). This distinguishes genuine duplicate-batch scenarios (e.g. container forking) from the known service-outage artifact, which only ever produces duplicates without explicit batch ids. Duplicates without an explicit `batchId` on either side continue to be log-only.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 [vale] reported by reviewdog 🐶
[Microsoft.SentenceLength] Try to keep sentences short (< 30 words).


Previously, all detected duplicate batches were only logged via the `DuplicateBatch` telemetry event, and the corresponding `DataCorruptionError` was never thrown. This was a temporary mitigation for a service-side bug that could redeliver batches.

Now, the error is thrown when either the incoming batch or the previously-seen batch has an explicit `batchId` (i.e. the batch was resubmitted, as opposed to a fresh batch whose `batchId` is derived from `clientId` and `batchStartCsn`). This distinguishes genuine duplicate-batch scenarios (e.g. container forking) from the known service-outage artifact, which only ever produces duplicates without explicit batch ids. Duplicates without an explicit `batchId` on either side continue to be log-only.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [vale] reported by reviewdog 🐶
[Microsoft.Hyphens] 'previously-seen' doesn't need a hyphen.


Previously, all detected duplicate batches were only logged via the `DuplicateBatch` telemetry event, and the corresponding `DataCorruptionError` was never thrown. This was a temporary mitigation for a service-side bug that could redeliver batches.

Now, the error is thrown when either the incoming batch or the previously-seen batch has an explicit `batchId` (i.e. the batch was resubmitted, as opposed to a fresh batch whose `batchId` is derived from `clientId` and `batchStartCsn`). This distinguishes genuine duplicate-batch scenarios (e.g. container forking) from the known service-outage artifact, which only ever produces duplicates without explicit batch ids. Duplicates without an explicit `batchId` on either side continue to be log-only.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [vale] reported by reviewdog 🐶
[Microsoft.Foreign] Use 'that is' instead of 'i.e. '.


Previously, all detected duplicate batches were only logged via the `DuplicateBatch` telemetry event, and the corresponding `DataCorruptionError` was never thrown. This was a temporary mitigation for a service-side bug that could redeliver batches.

Now, the error is thrown when either the incoming batch or the previously-seen batch has an explicit `batchId` (i.e. the batch was resubmitted, as opposed to a fresh batch whose `batchId` is derived from `clientId` and `batchStartCsn`). This distinguishes genuine duplicate-batch scenarios (e.g. container forking) from the known service-outage artifact, which only ever produces duplicates without explicit batch ids. Duplicates without an explicit `batchId` on either side continue to be log-only.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [vale] reported by reviewdog 🐶
[Microsoft.Foreign] Use 'for example' instead of 'e.g. '.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

@github-actions

Copy link
Copy Markdown
Contributor

Hi! Thank you for opening this PR. Want me to review it?

Based on the diff (125 lines, 3 files), I've queued these reviewers:

  • Correctness — logic errors, race conditions, lifecycle issues
  • Security — vulnerabilities, secret exposure, injection
  • API Compatibility — breaking changes, release tags, type design
  • Performance — algorithmic regressions, memory leaks
  • Testing — coverage gaps, hollow tests

How this works

  • Adjust the reviewer set by ticking/unticking boxes above. Reviewer toggles alone don't trigger anything.

  • Tick Start review below to dispatch the review fleet.

  • After review finishes, tick Start review again to request another run — it auto-resets after each dispatch.

  • This comment updates as new commits land; your reviewer selections are preserved.

  • Start review

@github-actions

Copy link
Copy Markdown
Contributor

Bundle size comparison

Base commit: ed439e5ce5b4c6e1aec2c8aff8662e8989ad270f
Head commit: 51a193734e63c28c8daf643a5108316c77b34e7a

Notable changes

No bundles changed by ≥ 500 bytes parsed.

Per-bundle deltas

@fluid-example/bundle-size-tests

  • azureClient.js: parsed 619098 → 619182 (+84), gzip 164826 → 164890 (+64)
  • odspClient.js: parsed 591824 → 591908 (+84), gzip 158912 → 159000 (+88)
  • aqueduct.js: parsed 525587 → 525650 (+63), gzip 140710 → 140751 (+41)
  • fluidFramework.js: parsed 392708 → 392828 (+120), gzip 111481 → 111542 (+61)
  • sharedTree.js: parsed 382095 → 382208 (+113), gzip 108869 → 108924 (+55)
  • containerRuntime.js: parsed 303937 → 303979 (+42), gzip 83213 → 83233 (+20)
  • sharedString.js: parsed 175984 → 175991 (+7), gzip 49445 → 49453 (+8)
  • experimentalSharedTree.js: parsed 160798 → 160798 (0), gzip 45804 → 45804 (0)
  • matrix.js: parsed 159845 → 159852 (+7), gzip 45411 → 45418 (+7)
  • loader.js: parsed 145256 → 145270 (+14), gzip 39063 → 39076 (+13)
  • odspDriver.js: parsed 104329 → 104350 (+21), gzip 32646 → 32635 (-11)
  • directory.js: parsed 66616 → 66623 (+7), gzip 18532 → 18540 (+8)
  • 748.js: parsed 58793 → 58793 (0), gzip 17827 → 17827 (0)
  • map.js: parsed 46709 → 46716 (+7), gzip 14310 → 14317 (+7)
  • odspPrefetchSnapshot.js: parsed 45642 → 45656 (+14), gzip 15277 → 15284 (+7)
  • 985.js: parsed 44491 → 44491 (0), gzip 13726 → 13726 (0)
  • summarizerDelayLoadedModule.js: parsed 30749 → 30749 (0), gzip 7753 → 7753 (0)
  • socketModule.js: parsed 26476 → 26483 (+7), gzip 7885 → 7895 (+10)
  • createNewModule.js: parsed 12480 → 12480 (0), gzip 4786 → 4786 (0)
  • summaryModule.js: parsed 3797 → 3797 (0), gzip 1860 → 1860 (0)
  • connectionState.js: parsed 724 → 724 (0), gzip 429 → 429 (0)
  • sharedTreeAttributes.js: parsed 666 → 673 (+7), gzip 432 → 441 (+9)
  • debugAssert.js: parsed 429 → 429 (0), gzip 299 → 299 (0)
  • FluidFramework-HashFallback.js: parsed 422 → 422 (0), gzip 316 → 316 (0)

@markfields markfields left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

);

const batchIdExplicit = batchStart.batchId !== undefined;
const otherBatchIdExplicit = result.otherBatchInfo?.batchIdExplicit ?? false;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider a comment reminding that otherBatchInfo may not be known (if loaded from a snapshot) and in this corner case we treat it as false to avoid false positives on the error

@dannimad dannimad enabled auto-merge (squash) July 10, 2026 20:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants