Fix race condition: prevent data loss when merging chained MRs
Summary
Fixes a race condition where commits are lost when two chained merge requests are merged in quick succession.
Scenario:
- MR A:
branch-a→master(with "delete source branch") - MR B:
branch-b→branch-a - MR A merges, enqueues async
DeleteSourceBranchWorker - MR B merges into
branch-abefore the worker retargets it tomaster - Worker deletes
branch-a— taking MR B's merge commit with it - MR B shows as "merged" but commits are gone
Related: https://gitlab.com/gitlab-com/request-for-help/-/work_items/4264
Fix
Two layers of protection behind the prevent_merge_race_condition feature flag:
-
Exclusive lease gate: When MR A merges with "delete source branch", an
ExclusiveLeaseis obtained inexecute(viaobtain_branch_deletion_lease!) markingbranch-aas pending deletion (branch_pending_deletion:{project_id}:{branch_name}).MergeService#validate!checks this lease viatarget_branch_pending_deletion_check!— if MR B tries to merge intobranch-a, it gets a clear error: "The target branch is scheduled for deletion. Please wait for the branch to be retargeted." The lease has a 5-minute TTL as a safety net. -
Cleanup: After the worker retargets chained MRs and deletes the branch, the lease is explicitly cancelled via
release_branch_deletion_lease. The TTL ensures auto-expiry if the worker crashes.
Verified on GDK
Reproduced the race condition with a sleep(15) in DeleteSourceBranchWorker to widen the async window:
| Scenario | Without fix (FF off) | With fix (FF on) |
|---|---|---|
| MR B merge attempt | Succeeds into doomed branch | Blocked with error message |
| MR B commits on master? | No — stranded on branch-a | N/A (merge blocked) |
| Branch deleted? | SHA guard may prevent it, but commits still stranded | Yes — safely deleted after retarget completes |
| MR B after worker runs | Shows "merged" but commits not on master | Stays open, retargeted to master, ready to merge safely |
| Data loss? | Yes | No |
Files changed
-
app/services/merge_requests/merge_service.rb— Obtains exclusive lease inexecute(viaobtain_branch_deletion_lease!) before validation when deleting source branch, addstarget_branch_pending_deletion_check!tovalidate!, releases lease on merge failure -
app/workers/merge_requests/delete_source_branch_worker.rb— AddsLEASE_KEY_PREFIXandLEASE_TIMEOUTconstants; cancels the lease viarelease_branch_deletion_leaseafter retarget and branch deletion when the feature flag is enabled -
config/feature_flags/development/prevent_merge_race_condition.yml— New feature flag (default disabled)