CI jobs can time out on POST /api/v4/jobs/request due to unbounded CI_MERGE_REQUEST_APPROVED lazy preload
Summary
POST /api/v4/jobs/request can return HTTP 500 (Rack::Timeout::RequestTimeoutException) ~60s after a runner picks up a job, instead of a valid payload. Affected jobs hang until their timeout with no trace output. It occurs on merge request pipelines where resolving the predefined CI_MERGE_REQUEST_APPROVED variable triggers an expensive, unbounded preload.
Root cause
An interaction between two independent changes, amplified by scale:
- Unbounded preload (
d2bd00b413c0, Feb 2025). The EE approval-variable preloader unconditionally preloadsmerge_request_diff → merge_request_diff_commits → commit_author/committerover all commits. That data is only needed when the project has "Prevent approval by users who add commits" enabled (ee/app/models/approval_state.rb→merge_request.committers), but it fires regardless of the setting and regardless of commit count. - Lazy resolution moved to the request path (!234842 (merged), 19.0). Removing the
ci_lazy_predefined_variablesflag madeCI_MERGE_REQUEST_APPROVEDresolve lazily/unconditionally, shifting that preload out of pipeline creation and into/jobs/request, inside the 60s Rack window.
Trigger: Ci::BuildRunnerPresenter#git_depth_value calls variables.to_hash, forcing evaluation of every lazy variable (including CI_MERGE_REQUEST_APPROVED) just to read GIT_DEPTH. On MRs with thousands of commits, the preload exhausts the timeout budget.
Impact
- MR pipelines fail silently: jobs are picked up,
/jobs/request500s after ~60s, and jobs hang to timeout with no trace. - Worst on MRs with large diffs (thousands of commits); affects hosted and self-managed runners (server-side).
Steps to reproduce
- Create an MR whose diff contains a large number of commits (order of thousands).
- Run a merge request pipeline.
POST /api/v4/jobs/requesttakes ~60s and returnsHTTP 500(Rack::Timeout), withCI_MERGE_REQUEST_APPROVEDresolution in the backtrace.
Proposed fix
Behind feature flag ci_optimize_merge_request_approved_resolution (default off):
- Scope the commit preload so it only runs when
merge_requests_disable_committers_approval?is enabled (the only case that reads it). - Change
git_depth_valueto a targetedvariables["GIT_DEPTH"]lookup instead ofto_hash.
Follow-up (separate, approvals/mergeability domain)
When "Prevent approval by users who add commits" is enabled, the preload is genuinely needed — but deriving committer identity by loading every diff commit is itself unbounded. Computing committer user IDs via a bounded query should be tracked separately.