Loading
Run likely-to-fail mergeability checks first in auto-merge
- Auto-merge workers (
AutoMergeProcessWorker,ProcessAutoMergeFromEventWorker) run the full mergeability suite on every run, but most runs cannot merge yet. The most common blockers (open threads, CI) are checked 11th and 12th, so those runs pay for about ten passing checks first. - Behind the
auto_merge_process_early_exitsflag, the worker now runs the checks most likely to fail first, and exits early for a known conflict or an already queued merge. - On GDK, round trips (SQL + Redis + Gitaly) across a full auto-merge lifecycle drop by about 31-32%. No state gets worse.
Detailed context for AI agents
What does this MR do and why?
Problem:
AutoMergeProcessWorker(andProcessAutoMergeFromEventWorker, throughAutoMergeService#process) runs the full mergeability state check suite on every run.- Most runs cannot merge yet: CI still running, open threads, unapproved, a known conflict, or a merge already queued.
- The suite is ordered cheapest first (tiers trivial/light/heavy by production P99 latency) and stops at the first failure. The most common blockers, open threads and CI, are checked 11th and 12th in EE, so a run that stops on them still pays for about ten checks that pass.
- Two states fail only at the very end:
- A merge request already known to be
cannot_be_merged: the conflict check runs last, inmergeable_git_state?. - A merge already queued: the run passes everything and enqueues a second
MergeWorker, whichMergeWorkerdeduplicates, and overwritesmerge_jid.
- A merge request already known to be
- Production context: on GitLab.com this worker's slow jobs are mostly these no-op runs, and the time is largely Ruby GVL wait on the 10-thread
low-urgency-cpu-boundshard. With theenable_sidekiq_gvl_metricssample, 30-70% of this worker's job time is GVL wait. Under that load every database or Redis round trip cost roughly 165-565 ms in a local reproduction with busy threads in the same process, so fewer round trips per run means shorter jobs. Discussion: !259376 (comment 3971487486)
Change (all behind the gitlab_com_derisk flag auto_merge_process_early_exits, actor: project, default off):
MergeRequest.mergeable_state_checks(order:)takes:cheapest_first(default, unchanged, from.mergeable_state_checks_by_tier) or:likely_to_fail_first(from the new.likely_to_fail_first_checks_by_group). An unknown order raisesArgumentError..likely_to_fail_first_checks_by_groupsits next to.mergeable_state_checks_by_tierand lists every check, grouped the same way:likely_light(open, draft, threads; EE adds blocked by another MR and changes requested),likely_heavy(CI), thenlightandheavyfor the rest.- EE adds its checks into each group in its override, the same way it extends the tiers.
- Specs keep both lists to the same checks: combined, CE on its own, and the EE additions on their own, so a check cannot be added to one list without the other.
MergeRequest#mergeable?and#mergeable_state?acceptcheck_order:(default:cheapest_first).AutoMerge::MergeWhenChecksPassService#processwith the flag on:- returns early if
merge_request.cannot_be_merged?ormerge_request.merge_ongoing?; - otherwise calls
mergeable?(use_cache: false, check_order: :likely_to_fail_first). - Flag off: unchanged
mergeable?(use_cache: false).
- returns early if
AutoMerge::AddToMergeTrainWhenChecksPassService#process(EE) with the flag on passescheck_order: :likely_to_fail_first. It does not get the two early exits because it skips the conflict check on purpose (trains handle conflicts) and never callsmerge_async.- Feature flag YAML:
config/feature_flags/gitlab_com_derisk/auto_merge_process_early_exits.yml. Rollout issue: #632857.
Known small trade-off:
CheckSecurityPolicyViolationsServiceschedules an approval rules resync when policy rules are out of sync. It now runs after CI in the new order, so for an MR with out-of-sync policy rules that resync starts once CI passes instead of while CI runs. No effect when rules are in sync.
References
Screenshots or screen recordings
Not applicable, backend only.
How to set up and validate locally
- Enable the flag for a project:
Feature.enable(:auto_merge_process_early_exits, Project.find(<id>)). - Set auto-merge (merge when checks pass) on a merge request in that project while its pipeline is running, then let the pipeline succeed. It merges as before.
- Compare Sidekiq log lines for
AutoMergeProcessWorkeron that project with the flag off and on.db_countis lower for runs that cannot merge yet.
MR acceptance checklist
Evaluated against https://docs.gitlab.com/development/code_review/#acceptance-checklist
Rollout and revert
- Enable for a test project, then a busy project with many auto-merge merge requests, then a percentage of actors.
- Measure before and after with:
- Sidekiq job logs (
json.db_count,json.redis_calls,json.duration_sforAutoMergeProcessWorker); - Mimir mean DB seconds and GVL wait per job;
MergeWorkerlog lines withjson.job_status"deduplicated" (should drop).
- Sidekiq job logs (
- Revert by disabling the flag.
Alternatives considered
- Running a few checks by hand before the suite and passing
skip_*params (first prototype): worked, but bypassed the per-check mergeability logging and needed skip params kept in sync with the pre-run checks. - Sorting the tier order by a list of check identifiers (an earlier version of this MR): replaced by explicit lists next to the tiers after review.
- Passing an explicit list of check classes into
mergeable?: callers could pass a partial list and drop a check by mistake, and CE cannot reference EE-only check classes. - Exiting early when another process holds the mergeability lease: dropped, rare and small gain for an extra public method on
MergeabilityCheckService. - Reordering the default (cheapest-first) order for everyone: would change which failing check the merge widget and API report first.
Verification performed
- Specs, all passing locally:
spec/services/auto_merge/merge_when_checks_pass_service_spec.rbee/spec/services/auto_merge/add_to_merge_train_when_checks_pass_service_spec.rbspec/models/merge_request_spec.rb(.mergeable_state_checksorder invariant and#mergeable_state?check_order)- related auto-merge service and worker specs
- New examples were confirmed to fail against master code.
- GDK end-to-end traces of all worker paths and two full lifecycles, flag off and on, with the real code. Round trips = SQL + Redis + Gitaly, summed over every worker run in the lifecycle (flag off -> on):
- CI finishes last (auto-merge set while CI runs, approval, thread resolved, 3 target pushes, 2 child pipelines, pipeline success and merge, the self re-enqueued run): 318 -> 218 (-31%).
- People finish last (CI passes early, then 3 target pushes while waiting on a thread and an approval, thread resolved, approval and merge, self re-enqueued run): 256 -> 175 (-32%).
- Single runs (flag off -> on):
- CI running: 20 -> 14
- thread open: 13 -> 4
- known conflict: 32 -> 3
- merge already queued: 35 -> 4, and no second
MergeWorkerenqueue - blocked by draft: 3 -> 3
- blocked by another MR: 6 -> 5
- changes requested: 7 -> 6
- the run that merges and the unapproved run: unchanged
- No state gets worse. Every path ends in the same state (merged or not, same
merge_status) with the flag on and off.
Out of scope (follow-ups)
- Not enqueueing the worker for child pipelines.
- Suppressing the self re-enqueue.
- Making the worker's deduplication effective.
- The second full check pass in
MergeTrainService#available_for?. - Moving the worker to high urgency: !259376
Edited by Marc Shaw