Add ci_sources-scoped Commit.latestPipeline GraphQL field
What does this MR do and why?
Adds a latestPipeline field to the Commit GraphQL type, backed by the ci_sources-scoped project.ci_pipelines (batch loaded).
Background. The commits list badge derives a commit's CI status from the newest of commit.pipelines, which resolves through Ci::PipelinesFinder onto project.all_pipelines. That association is unscoped, so it includes dangling-source pipelines (e.g. security_orchestration_policy scans). When such a pipeline is newer than the commit's real CI pipeline, the badge shows the dangling pipeline's status — for example a green check on a commit whose actual CI pipeline failed (see #579690).
Every other surface already scopes to ci_sources and ignores dangling pipelines: the commit page, the API commit status field, and Ci::Ref#update_status_by! (next if pipeline.dangling?).
This MR adds the correctly-scoped field only.
Release sequencing — this is step 1 of 2
The commit list query is switched to this field in a follow-up shipped one release later (!243958 (merged), targeting 19.3), so the field exists in the previous release (19.2) that self-managed instances upgrade from. That guarantees no stripped/blank commit statuses during a rolling upgrade, and on GitLab.com the field is deployed before any frontend depends on it. See multi-version compatibility.
- 19.2 (this MR): add the
Commit.latestPipelinefield. - 19.3: !243958 (merged) switches the commit list badge to it.
Related
- Follow-up (frontend, 19.3): !243958 (merged)
- Root-cause analysis: #579690
MR acceptance checklist
-
bundle exec rubocopon changed files. - Backend spec added (
commit_type_spec.rb: dangling pipeline excluded). - GraphQL docs regenerated.
- Milestone: 19.2.
Database query plans
The latestPipeline resolver batch-loads a page of commits into one query (Ci::Pipeline.latest_pipeline_per_commit(..., in_current_partition: true)), replacing the current commit-list badge path (commit.pipelines(first: 1)), which runs one query per commit (N+1). It also scopes to ci_sources (excludes dangling sources such as security_orchestration_policy), unlike the current query which excludes only child pipelines (source != 12).
Partition scoping. p_ci_pipelines is partitioned by partition_id. To avoid the cross-partition locking flagged by Database/AvoidUnpartitionedCiRelations, the resolver queries the current partition first (... AND partition_id = <current>) and only falls back to a cross-partition scan for the SHAs whose latest pipeline predates it. A newer pipeline for a SHA always has both a higher id and a partition_id >= any older pipeline's, so the latest found in the current partition is the global latest. For a page of recent commits this prunes to a single partition.
Note: the current partition was
113at measurement time (SELECT id FROM ci_partitions WHERE status = 2;). The "After" plans below are the partition-scoped queries; the "Before" plan is the existing per-commit query, unchanged.
Run against the ci Database Lab clone. SHAs are real gitlab-org/gitlab@master commits (project 278964).
1. After — new batched query (recent page, 20 commits, one execution)
Recent commits resolve entirely in the current partition, so the resolver issues a single partition-pruned query and the fallback never runs:
EXPLAIN
SELECT DISTINCT ON (sha) *
FROM p_ci_pipelines
WHERE project_id = 278964
AND partition_id = 113 -- current partition first
AND (source IN (1,2,3,4,5,6,7,8,10,11) OR source IS NULL)
AND sha IN (
'8b1c831c415227f2f21959d5f7fa882d0fc7826b', 'a18c1077e06763d54af15431fa322aaf65188c20',
'ad931779d90d117c841eca6cdd569f748f826254', '9117eddda7c352a9d90d5fe13f1a474db66e0587',
'e7fb3dc4b1b38fa16b39b7b106d2298b386d50d2', 'd63b339b8bb0da0fe80471d6703dc1137f428fd3',
'5e351c99925ebef9cc7ec2d0c706e876e91ffc34', 'e17352d7aea8f56b9194af420d758bf47beb3575',
'65be1f73b5d57d090ee05755a90d91b0ec8e49dc', 'fe8d33147909fa97604039524916753850256907',
'4850b39a313b7207ab0fd14d60ef71616ab6d6ff', '4c702d721652850fe7c0335d8cbc0c4cae8312b9',
'c2214384fdadf15002fbf90bdfe1d4abe280bd0b', '423ad1ed25266b0e53d533dba54edb3a4038fbc2',
'bcd6c028b5e4109c7e9e01d86c7eba5987d06bb9', '0b4d4c1e64add719ae85ca95fd6f72a41995ec70',
'32448c7802dd293c86525cf4fbded284edf39b39', 'ed7c52b6539029e6598dd8a7b807527b0a06d077',
'8f452eab2547499ea609d15b5211325affc76771', 'd777101839a910064a48705b9806e8265915eddf'
)
ORDER BY sha ASC, id DESC;Warm cache
https://postgres.ai/console/gitlab/gitlab-production-ci/sessions/53584/commands/155749
Time: 4.319 ms
- planning: 3.928 ms
- execution: 0.391 ms
- I/O read: 0.000 ms
- I/O write: 0.000 ms
Shared buffers:
- hits: 170 (~1.30 MiB) from the buffer pool
- reads: 0 from the OS file cache, including disk I/O
- dirtied: 0
- writes: 02. Before — old per-commit query (runs once per commit; multiply cost by page size)
EXPLAIN
SELECT *
FROM p_ci_pipelines
WHERE project_id = 278964
AND source != 12
AND sha = '8b1c831c415227f2f21959d5f7fa882d0fc7826b'
ORDER BY id DESC
LIMIT 2;https://postgres.ai/console/gitlab/gitlab-production-ci/sessions/53584/commands/155709:
Time: 28.930 ms
- planning: 28.308 ms
- execution: 0.622 ms
- I/O read: 0.000 ms
- I/O write: 0.000 ms
Shared buffers:
- hits: 52 (~416.00 KiB) from the buffer pool
- reads: 0 from the OS file cache, including disk I/O
- dirtied: 0
- writes: 0 Given there are 20 queries, this extrapolates to 578.6 ms total time.
3. After — worst case: deep-history page (older partitions, Dec 2022 commits)
For a page whose commits predate the current partition, the current-partition probe returns nothing, so the resolver falls back to a single cross-partition scan for the missing SHAs (two queries total):
-- Step 1: current-partition probe (returns no rows for these old SHAs, then the fallback runs)
EXPLAIN
SELECT DISTINCT ON (sha) *
FROM p_ci_pipelines
WHERE project_id = 278964
AND partition_id = 113
AND (source IN (1,2,3,4,5,6,7,8,10,11) OR source IS NULL)
AND sha IN (
'12c3532250b2021816798ee0b52e5122ae1cd9c6', '3855402ae370c868e507de96a1c447c36bb234b7',
'1c27a71a92b31de10955c766eea3b936b63c0e62', 'f2b900e7d13db91bc7406f9647ec2d947bc1a863',
'6e3584b1a0e6b48f901a17316b90ee0cb547638d', 'aabfff5a5aec0cb326a9f992aff77821ec23edfa',
'b7fa86207fed9d044d2f4aa7c3defa45ea01cef7', '1405a7985226aabd8a99d87e6886acec52098fad',
'ef1ebb76daf64df48e7415dd964c518e105262c8', '0f98bfa9043127fb9c62778fb5fbb5e6c63d653c',
'fc07dd920a938bf4e44596939f24b2ac2d414b3b', '634486f64f91a65a7fb280e4b903373b7d9ca589',
'7ddbae53d786cad923b7006d1a91116d92ee9d4a', '12ddef2470272ed3e44e0e53515d1c74099c7dc1',
'cf87ba353a752a836288ea36f7c4076a5ff92171', 'bd88e6325d0092c2665babd8ed078f9870fbe4c3',
'44b272890435e1e909499a4a1d30369b4a2602d2', 'c29f27cbee1cc958917b357bb333d6713ce1cedb',
'6d3a35037a7011ba7a748346249e9d54c5112f47', '890a430cac7912fea96b37ce6c8161c747ce7b0f'
);
-- Step 2: cross-partition fallback for the SHAs not found in step 1 (this is the plan measured below)
EXPLAIN
SELECT DISTINCT ON (sha) *
FROM p_ci_pipelines
WHERE project_id = 278964
AND (source IN (1,2,3,4,5,6,7,8,10,11) OR source IS NULL)
AND sha IN (
'12c3532250b2021816798ee0b52e5122ae1cd9c6', '3855402ae370c868e507de96a1c447c36bb234b7',
'1c27a71a92b31de10955c766eea3b936b63c0e62', 'f2b900e7d13db91bc7406f9647ec2d947bc1a863',
'6e3584b1a0e6b48f901a17316b90ee0cb547638d', 'aabfff5a5aec0cb326a9f992aff77821ec23edfa',
'b7fa86207fed9d044d2f4aa7c3defa45ea01cef7', '1405a7985226aabd8a99d87e6886acec52098fad',
'ef1ebb76daf64df48e7415dd964c518e105262c8', '0f98bfa9043127fb9c62778fb5fbb5e6c63d653c',
'fc07dd920a938bf4e44596939f24b2ac2d414b3b', '634486f64f91a65a7fb280e4b903373b7d9ca589',
'7ddbae53d786cad923b7006d1a91116d92ee9d4a', '12ddef2470272ed3e44e0e53515d1c74099c7dc1',
'cf87ba353a752a836288ea36f7c4076a5ff92171', 'bd88e6325d0092c2665babd8ed078f9870fbe4c3',
'44b272890435e1e909499a4a1d30369b4a2602d2', 'c29f27cbee1cc958917b357bb333d6713ce1cedb',
'6d3a35037a7011ba7a748346249e9d54c5112f47', '890a430cac7912fea96b37ce6c8161c747ce7b0f'
)
ORDER BY sha ASC, id DESC;Step 1 plan — https://postgres.ai/console/gitlab/gitlab-production-ci/sessions/53584/commands/155750:
Time: 4.140 ms
- planning: 3.967 ms
- execution: 0.173 ms
- I/O read: 0.000 ms
- I/O write: 0.000 ms
Shared buffers:
- hits: 64 (~512.00 KiB) from the buffer pool
- reads: 0 from the OS file cache, including disk I/O
- dirtied: 0
- writes: 0Fallback (step 2) plan — https://postgres.ai/console/gitlab/gitlab-production-ci/sessions/53584/commands/155751:
Time: 38.727 ms
- planning: 35.534 ms
- execution: 3.193 ms
- I/O read: 0.000 ms
- I/O write: 0.000 ms
Shared buffers:
- hits: 1124 (~8.80 MiB) from the buffer pool
- reads: 0 from the OS file cache, including disk I/O
- dirtied: 0
- writes: 0 The step-1 probe adds one lightweight partition-pruned query on top of this; it only touches the current partition and returns no rows for these SHAs.
Summary
Measured on the ci Database Lab clone (gitlab-org/gitlab, 20-commit page):
| Queries | Planning | Execution (warm) | Buffers | |
|---|---|---|---|---|
Before — pipelines(first: 1) |
20 (one per commit) | ~28 ms each (~570 ms total) | ~0.6 ms each | ~52 × 20 ≈ 1040 |
| After — recent page | 1 | ~4 ms | ~0.4 ms | 170 |
| After — deep-history (probe + fallback) | 2 | ~4 ms + ~36 ms | ~0.2 ms + ~3 ms | 64 + 1124 ≈ 1188 |
- N+1 collapses to one query. The 20 per-commit queries (~570 ms of planning in aggregate) become a single batched query for a recent page, or two for a deep-history page: 18–19 fewer round-trips, and one planning pass instead of one per query (Database Lab plans each of the 20; partitioned tables commonly use custom, re-planned plans, so this multiplies in the N+1 path).
- Partition pruning is the big win. Scoping the recent-page query to the current partition (
partition_id = 113) brings it to ~4 ms planning / 0.4 ms execution / 170 buffers — roughly 8× less planning and 6× fewer buffers than the same query unscoped (~34 ms / ~1069 buffers), and cheaper than even a single old per-commit query (~29 ms). No cross-partition locking on the hot path. - Deep-history worst case stays bounded. When a page predates the current partition, the partition-pruned probe (~4 ms, 64 buffers) misses and the resolver runs one cross-partition fallback (~36 ms planning, 3 ms execution, 1124 buffers) — total ~43 ms / ~1188 buffers for the whole page. That's about one old per-commit query's planning cost, incurred only for old pages, and the ~1188 buffers are comparable to the ~1040 the 20 per-commit queries touch in aggregate. The fallback holds up via the
p_ci_pipelines_project_id_sha_idx (project_id, sha)index. - Planning-dominated; execution is trivial. All warm executions are sub-3 ms. The ~36 ms planning cost only appears on the cross-partition fallback (deep-history pages); recent pages avoid it entirely thanks to partition pruning. Absolute milliseconds aren't comparable across warm/cold runs — buffer counts and query count are the robust metrics.
- Caveat: absolute milliseconds aren't comparable across warm/cold runs; buffer counts and query count are the robust metrics. The fixed ~30 ms planning is inherent to this partitioned table and is paid once by the batched query. The
postgres.aiplans predate thepartition_idscoping and should be re-run.