Return HTTP 409 (Conflict) for Sequential Bundle Race Condition#5592
Merged
mikaelweave merged 7 commits intoMay 29, 2026
Conversation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The earlier fix gated the fail-fast 409 on MergeOptions.EnlistInTransaction, but regular single-resource upserts also set enlistTransaction: true. That caused concurrent no-ETag upserts (which should retry to a last-write-wins outcome) to surface as ResourceConflictException (409) instead of retrying, breaking GivenASavedResource_WhenConcurrentlyUpsertingWithNoETagHeader_ThenTheExistingResourceIsUpdated. A SQL conflict (50409) only zombies the transaction when the command actually enlisted in an ambient C# transaction scope (the condition used in MergeResourcesWrapperAsync). Gate the fail-fast on EnlistInTransaction && SqlTransactionScope != null, captured before the merge attempt, so only sequential transaction bundles fail fast while regular and batch-bundle upserts keep retrying. Updated the enlisted unit test to set up a real ambient scope via BeginTransaction(), and added a regression test asserting enlist:true without an ambient scope still retries. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a comment noting that a SurrogateIdCollision (50424) inside an ambient C# transaction zombies the transaction the same way a 50409 conflict does, so the retries there are futile and still surface as a 500. Intentionally left unhandled since 50424 is rare and C# transactions are being deprecated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Document why a zombied-at-commit transaction returns 500 (root cause lost) while an inner-request conflict surfaces a precise 409 before commit. Condense data-store comments. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #5592 +/- ##
=======================================
Coverage 77.33% 77.34%
=======================================
Files 996 996
Lines 36534 36550 +16
Branches 5538 5541 +3
=======================================
+ Hits 28255 28268 +13
+ Misses 6912 6910 -2
- Partials 1367 1372 +5 🚀 New features to boost your workflow:
|
fhibf
approved these changes
May 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
This PR changes SQL Server transaction-bundle behavior for the confirmed WI 188321 root cause. When
MergeAsyncsees SQL conflict 50409 while enlisted in a C# transaction, it now stops retrying inside that already-invalidated transaction and throwsResourceConflictException, which maps to HTTP 409 Conflict. This prevents the request from continuing toSqlTransaction.Commit()and surfacing the later zombie transaction symptom as a generic HTTP 500.The completed
SqlTransactionfallback inBundleHandlerremains a 500 for non-conflict root causes. The change is intentionally scoped to enlisted SQL concurrency conflicts; the existing non-enlisted retry path still retries and preserves its current exhausted-retry behavior.This PR also adds targeted coverage for:
The initial PowerShell repro harness was removed from the branch.
AB#188321
Testing
dotnet test .\src\Microsoft.Health.Fhir.Api.UnitTests\Microsoft.Health.Fhir.R4.Api.UnitTests.csproj --no-restore --filter "FullyQualifiedName~BundleHandlerTests" --logger "console;verbosity=minimal"dotnet test .\src\Microsoft.Health.Fhir.Api.UnitTests\Microsoft.Health.Fhir.R4.Api.UnitTests.csproj --no-restore --filter "FullyQualifiedName~GivenATransaction_WhenTransactionIsZombiedAtCommit|FullyQualifiedName~GivenATransaction_WhenInnerRequestReturnsConflictOperationOutcome" --logger "console;verbosity=minimal"dotnet test .\src\Microsoft.Health.Fhir.SqlServer.UnitTests\Microsoft.Health.Fhir.SqlServer.UnitTests.csproj --no-restore --framework net8.0 --filter "Name~MergeAsync_OnSqlConflict_WithEnlistedTransaction" --logger "console;verbosity=minimal"dotnet test .\src\Microsoft.Health.Fhir.SqlServer.UnitTests\Microsoft.Health.Fhir.SqlServer.UnitTests.csproj --no-restore --framework net9.0 --filter "Name~MergeAsync_OnSqlConflict_WithEnlistedTransaction" --logger "console;verbosity=minimal"FHIR Team Checklist
Semver Change (docs)
Patch