Enable RefreshService diff commit preload skip by default

Flips merge_request_refresh_skip_diff_commit_preload on by default for self-managed instances. It has run at 100% on GitLab.com for about 12 days with no issues (see the rollout issue below). No application code changes - only the flag definition moves from gitlab_com_derisk to beta and default_enabled becomes true, since gitlab_com_derisk flags are not allowed to default on.

Detailed context for AI agents

What changed

One file: config/feature_flags/gitlab_com_derisk/merge_request_refresh_skip_diff_commit_preload.yml was moved to config/feature_flags/beta/merge_request_refresh_skip_diff_commit_preload.yml, with type: gitlab_com_derisk becoming type: beta and default_enabled: false becoming default_enabled: true. No other file changed. That is deliberate: with default_enabled: true, Feature.enabled? returns true when no record is persisted, so the existing guard in the service already selects the fast path by default. No spec changes were needed.

Why the type had to change

The flag was gitlab_com_derisk. That type sets can_be_default_enabled: false in lib/feature/shared.rb:20, and lib/feature/definition.rb:76 raises Feature::InvalidFeatureFlagError if such a flag sets default_enabled: true. So defaulting the flag on was impossible without changing its type. beta and ops are the only two non-deprecated types that permit default_enabled: true. Also relevant: lib/feature/definition.rb:59 enforces that the flag file's parent directory name matches its type, which is why the file had to move directories.

Why beta and not ops

ops was considered and rejected. GitLab's docs (doc/development/feature_flags/_index.md:252) say an ops flag's default_enabled "should be set to false in most cases, and only enabled to resolve temporary scalability issues" - an ops flag is an override you flip on, not the normal state. This flag's name means "skip the preload", so shipping it as ops with the correct default would have required inverting it into a new flag with a new name (something like merge_request_refresh_preload_diff_commits) plus an operational runbook. That is a lot of permanent machinery to guard a preload removal. beta gives the default-on behaviour now with the flag still available as a kill switch, and a short lifespan that pushes toward removal.

What the flag actually does

It skips an eager preload of the diff commit graph in MergeRequests::RefreshService#post_merge_candidates, at app/services/merge_requests/refresh_service.rb:151.

The preload walked: the latest diff, that diff's commits, each commit's metadata row, and each metadata row's commit_author and committer - for every open merge request targeting the pushed branch. On gitlab-org/gitlab that is roughly 2,784 merge requests on every push to master. Nothing on that code path reads the preloaded data.

The cost comes from MergeRequestDiffCommit belongs_to :merge_request_commits_metadata, which has an instance-dependent association scope. Rails evaluates that scope lambda once per diff commit during preload and builds a relation object each time. This adds almost no queries but enormous CPU and allocations - which is why the production symptom was huge CPU with tiny database time.

Measurements

Local: 100 open merge requests each with a 30-commit diff, on a 2-commit push. RefreshService CPU dropped from 8.034s to 0.046s, allocations from 655k to 69k objects. About 99% CPU reduction.

Production before the flag: three UpdateMergeRequestsWorker jobs on gitlab-org/gitlab on 2026-08-20, all ordinary small merges to master, different authors, spanning two releases. Each showed 311-343s wall time, 306-339s CPU, about 1.8 GB and 4.07 million objects, but only 2.5-4.2s of database time. The near-identical fingerprint across unrelated pushes is the signature of a fixed traversal that does not depend on push contents.

Rollout state, already completed before this MR

Enabled on dev, pre, staging and staging-ref. On production (gprd) it was ramped 10% to 25% to 50% to 100% of actors on 2026-08-26, and set globally true the same day. It was also enabled for the gitlab-org/gitlab project specifically first. The DRI confirmed on 2026-08-27 and again on 2026-09-01 that it was working well, with Sidekiq dashboard screenshots posted on the rollout issue. So this has had roughly 12 days at 100% on GitLab.com with no issues before this MR defaults it on for self-managed.

Risk and edge cases

The only consumer of the preloaded graph is MergeRequestDiff#head_commit_sha, which falls back to reading commit rows when the head_commit_sha column is NULL. That column has been populated since GitLab 8.4 (January 2016), so only pre-2016 diffs can reach that path. Without the preload that fallback is actually faster - it runs an indexed query with LIMIT 1 instead of loading and sorting every commit row in Ruby.

The realistic failure mode is a merge request that was manually merged into the target branch not being detected and closed, so it stays open. There is no data change and nothing to repair. Rollback is to disable the flag.

A spec already covers the pre-8.4 NULL head_commit_sha case with the flag both on and off, at spec/services/merge_requests/refresh_service_spec.rb:596.

One thing a reviewer should know

On GitLab.com the flag has a persisted global record from the chatops rollout. lib/feature.rb:396 only short-circuits to the code default when the flag is NOT persisted, so GitLab.com will keep reading its persisted true value rather than the new default. That is harmless because the persisted value is already true, but the persisted record should be deleted eventually so GitLab.com falls back to the code default.

Verification performed

  • bundle exec rspec spec/services/merge_requests/refresh_service_spec.rb:571 - 4 examples, 0 failures.
  • bundle exec rspec spec/models/merge_request_spec.rb:99 - 6 examples, 0 failures.
  • Loaded the definition in a Rails runner: it resolves as type=beta default_enabled=true, and Feature.enabled?(:merge_request_refresh_skip_diff_commit_preload, Project.first) returns true with no persisted record, confirming self-managed gets the fast path.

Out of scope

  • Removing the flag entirely, and with it the now barely used MergeRequest.preload_latest_diff_commit scope (app/models/merge_request.rb:591) and its specs in spec/models/merge_request_spec.rb. That is the follow-up once this is stable. The scope must stay for now because the flag can still be disabled.
  • The milestone field was deliberately left at 19.3, the milestone the flag was introduced in, because keeps/delete_old_feature_flags.rb reads that field as "introduced in" when deciding whether a flag is overdue for removal. The current VERSION file says 19.4.0-pre.
  • beta flags are documented automatically on the All feature flags page, which is generated from the YAML definition files during the docs build, so no manual docs edit is needed.

Merge request reports

Loading
Loading