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-amaster (with "delete source branch")
  • MR B: branch-bbranch-a
  • MR A merges, enqueues async DeleteSourceBranchWorker
  • MR B merges into branch-a before the worker retargets it to master
  • 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:

  1. Exclusive lease gate: When MR A merges with "delete source branch", an ExclusiveLease is obtained in execute (via obtain_branch_deletion_lease!) marking branch-a as pending deletion (branch_pending_deletion:{project_id}:{branch_name}). MergeService#validate! checks this lease via target_branch_pending_deletion_check! — if MR B tries to merge into branch-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.

  2. 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 in execute (via obtain_branch_deletion_lease!) before validation when deleting source branch, adds target_branch_pending_deletion_check! to validate!, releases lease on merge failure
  • app/workers/merge_requests/delete_source_branch_worker.rb — Adds LEASE_KEY_PREFIX and LEASE_TIMEOUT constants; cancels the lease via release_branch_deletion_lease after retarget and branch deletion when the feature flag is enabled
  • config/feature_flags/development/prevent_merge_race_condition.yml — New feature flag (default disabled)
Edited by Marc Shaw

Merge request reports

Loading