Skip to content

Commit a32b39d

Browse files
izaitsevfbIvan Zaitsev
andauthored
Aggregate autorevert restart runs like any other run in the HUD (partial revert of #6954) (#8494)
✴️ iz2: Partial revert of #6954 — the UI half of #8300. **Problem.** Autorevert's restart runs (`workflow_dispatch` on `trunk/<sha>`) were filtered out of the HUD, so their results — including real failures — never reached the grid. And a multi-run cell was merged pairwise, newest `job.id` wins, with the flaky flag taken from the *loser*: a pass followed by a restart failure rendered plain red, the reverse order rendered flaky `F`. **Proposed solution.** Invariant: **who issued a run does not change how the HUD aggregates it.** A push, a re-run attempt and a restart are all just runs; a cell's verdict follows from the set of conclusions, not from the issuer and not from arrival order. Origin is reported on the tooltip, never aggregated on. Rules, in the new `torchci/lib/mergeCellRuns.ts`: - one run → show that run - success + a real failure, in any order → show the success, marked flaky `F` - `cancelled` loses to any non-cancelled run, and is deliberately not failure evidence (unlike `isFailure()`); `skipped` loses to a real result, as before - otherwise rank success > failure > pending > neutral > skipped, newest run breaking a tie within a class **What changes?** Restart results reach the grid on the same terms as any other run, and a mixed cell renders `F` in either order. This governs every multi-run cell, not just restarts — a twice-scheduled periodic job or a re-run now follows the ranking above instead of newest-id-wins, so an older success outranks a newer pending run. Measured over 14 days on the 266 `pytorch/pytorch` trunk shas that had restart runs (cells without a restart were not measured): **14,405 cells differ from `main`** — 13,687 restart-only cells now show their real conclusion, 208 turn `F` (restart failed, natural run passed), 195 the reverse, 223 stop being `F` (a cancelled co-run no longer counts as failure), 92 in smaller classes. **Changed, at a high level** - `hud_query` — admit restart runs; project the run's origin and dispatching identity - `mergeCellRuns.ts` + `types.ts` — the rules above and which run represents the cell; unit-tested on both permutation axes (order, issuer) - `fetchHud.ts` — collect every run per cell and delegate; the old reducer is gone - `JobTooltip.tsx` — one radio per run behind the cell, with the grid's own status glyph and a `gh` link to that run; picking one rebinds the log viewer, links and failure classification to it - `commit_jobs_query` — admits restarts that *passed* (all were hidden before); its comment records the divergence below **Known gaps.** A restart that *failed* still doesn't show on the commit page: `fetchCommit` dedups by newest id with no flaky marker, so admitting one would replace the natural conclusion instead of combining with it. Grid and commit page disagree there until that page can render `F` — tracked as a follow-up. `commit_jobs_batch_query` and its `viable_strict_sole_blocker` mirror are untouched: `is_green()` has no per-job-name dedup and vetoes on the first non-success row, so restarts only add vetoes (17 currently-green commits would go red, 0 red would go green) — #8300's second action item. Refs #8300 --------- Co-authored-by: Ivan Zaitsev <izaitsevfb@meta.com> Co-authored-by: Ivan Zaitsev <izaitsevfb@users.noreply.github.com>
1 parent a6cd5e7 commit a32b39d

13 files changed

Lines changed: 2090 additions & 55 deletions

File tree

torchci/clickhouse_queries/commit_jobs_query/query.sql

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,21 @@ WITH job AS (
3737
job.torchci_classification_kg.'context' as context,
3838
job.runner_name AS runner_name,
3939
workflow.head_commit. 'author'.'email' AS authorEmail,
40-
job.run_attempt AS run_attempt
40+
job.run_attempt AS run_attempt,
41+
-- Origin of the run, so the workflow picker can say what it is offering instead of a bare
42+
-- numeric id. Same multiIf as hud_query's run_origin on purpose: one run must not read two
43+
-- different ways on the grid and on this page. A plain push stays NULL -- it is the
44+
-- overwhelming majority and the default reading.
45+
multiIf(
46+
workflow.event = 'workflow_dispatch'
47+
AND workflow.head_branch LIKE 'trunk/%',
48+
'autorevert',
49+
job.run_attempt > 1,
50+
'retry',
51+
workflow.event = 'push',
52+
NULL,
53+
workflow.event
54+
) AS run_origin
4155
FROM
4256
workflow_job job final
4357
INNER JOIN workflow_run workflow final ON workflow.id = job.run_id
@@ -46,7 +60,15 @@ WITH job AS (
4660
AND job.name != 'generate-test-matrix'
4761
AND workflow.event != 'workflow_run' -- Filter out workflow_run-triggered jobs, which have nothing to do with the SHA
4862
AND workflow.event != 'repository_dispatch' -- Filter out repository_dispatch-triggered jobs, which have nothing to do with the SHA
49-
AND NOT (workflow.event = 'workflow_dispatch' AND workflow.head_branch LIKE 'trunk/%') -- Filter out restart jobs
63+
-- KNOWN DIVERGENCE FROM hud_query, deliberate. hud_query no longer filters restarts by
64+
-- conclusion at all: it admits every run and lets mergeCellRuns decide the cell
65+
-- issuer-agnostically, so a restart that failed makes a passing cell render flaky instead of
66+
-- vanishing. This surface cannot do that yet -- fetchCommit dedups by newest id and has no
67+
-- flaky marker to render, so admitting a non-success restart here would silently REPLACE the
68+
-- natural conclusion rather than combine with it. Until the commit page can express a flaky
69+
-- result, the success-only filter stays, and the grid and the commit page will disagree about
70+
-- a commit whose restart failed.
71+
AND NOT (workflow.event = 'workflow_dispatch' AND workflow.head_branch LIKE 'trunk/%' AND job.conclusion_kg != 'success')
5072
AND workflow.id in (select id from materialized_views.workflow_run_by_head_sha where head_sha = {sha: String})
5173
AND (
5274
{workflowId: Int64} = 0
@@ -88,13 +110,27 @@ WITH job AS (
88110
[ ] as context,
89111
'' AS runner_name,
90112
workflow.head_commit.author.email AS authorEmail,
91-
workflow.run_attempt as run_attempt
113+
workflow.run_attempt as run_attempt,
114+
-- Same column as the branch above, because a UNION ALL needs matching shapes. These rows
115+
-- carry workflow_id = 0, which fetchCommit normalizes to null and getWorkflowIdsByName then
116+
-- filters out, so a startup-failure pseudo-row never reaches the picker -- this is
117+
-- projected for the union, not for the dropdown.
118+
multiIf(
119+
workflow.event = 'workflow_dispatch'
120+
AND workflow.head_branch LIKE 'trunk/%',
121+
'autorevert',
122+
workflow.run_attempt > 1,
123+
'retry',
124+
workflow.event = 'push',
125+
NULL,
126+
workflow.event
127+
) AS run_origin
92128
FROM
93129
workflow_run workflow final
94130
WHERE
95131
workflow.event != 'workflow_run' -- Filter out workflow_run-triggered jobs, which have nothing to do with the SHA
96132
AND workflow.event != 'repository_dispatch' -- Filter out repository_dispatch-triggered jobs, which have nothing to do with the SHA
97-
AND NOT (workflow.event = 'workflow_dispatch' AND workflow.head_branch LIKE 'trunk/%') -- Filter out restart jobs
133+
AND NOT (workflow.event = 'workflow_dispatch' AND workflow.head_branch LIKE 'trunk/%' AND workflow.conclusion != 'success') -- Autorevert restart runs count only when they PASSED. This branch maps a still-queued run to 'failure', so admitting a restart here would put a red "Workflow Startup Failure / trunk" box on the commit page for the whole queue window
98134
AND workflow.id in (select id from materialized_views.workflow_run_by_head_sha where head_sha = {sha: String})
99135
AND (
100136
{workflowId: Int64} = 0
@@ -128,7 +164,8 @@ SELECT
128164
runner_name AS runnerName,
129165
authorEmail,
130166
time,
131-
run_attempt AS runAttempt
167+
run_attempt AS runAttempt,
168+
run_origin AS runOrigin
132169
FROM
133170
job
134171
ORDER BY

torchci/clickhouse_queries/hud_query/query.sql

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,63 @@ WITH job AS (
1818
job.torchci_classification_kg.'line' as line,
1919
job.torchci_classification_kg.'captures' as captures,
2020
job.torchci_classification_kg.'line_num' as line_num,
21-
annotation.annotation as annotation
21+
annotation.annotation as annotation,
22+
-- Origin of the run, REPORTED but never aggregated on -- aggregation is issuer-agnostic by
23+
-- design (mergeCellRuns). A plain push is left NULL rather than spelled out: it is the
24+
-- overwhelming majority and the default reading, and fetchHud strips nulls before shipping
25+
-- the grid, so ordinary rows cost nothing.
26+
-- NULL means specifically a `push` run, so the UI can name the origin without guessing. Any
27+
-- other event (schedule, a non-trunk workflow_dispatch, ...) carries its own event name --
28+
-- calling those "push" would be wrong, and periodic schedules are one of the reasons a cell
29+
-- has several runs in the first place.
30+
multiIf(
31+
job.workflow_event = 'workflow_dispatch'
32+
AND job.head_branch LIKE 'trunk/%',
33+
'autorevert',
34+
job.run_attempt > 1,
35+
'retry',
36+
job.workflow_event = 'push',
37+
NULL,
38+
job.workflow_event
39+
) AS run_origin,
40+
tupleElement(restart_run.latest, 1) as restart_actor_login,
41+
tupleElement(restart_run.latest, 2) as restart_triggering_actor_login,
42+
tupleElement(restart_run.latest, 3) as restart_run_attempt
2243
FROM
2344
workflow_job job final
2445
LEFT JOIN job_annotation annotation final ON job.id = annotation.jobID
46+
-- workflow_job carries no actor column, so the dispatching identity has to come from
47+
-- workflow_run. Scoped to restart runs for the requested shas only, and keyed through the
48+
-- same materialized view commit_jobs_query uses so this is a primary-key lookup instead of
49+
-- a scan on head_sha (which is not in workflow_run's sorting key).
50+
-- GROUP BY + argMax rather than FINAL: workflow_run is a ReplacingMergeTree with no version
51+
-- column, and FINAL on it measured ~5.6s against ~0.7s for this shape on the same input.
52+
-- run_attempt is monotonic per run, so argMax also expresses what we actually want here --
53+
-- the latest attempt -- which FINAL alone does not guarantee.
54+
LEFT JOIN (
55+
SELECT
56+
id,
57+
-- ONE argMax over a tuple rather than three separate ones: independent argMax calls
58+
-- may resolve their tie differently and report an actor from one physical row with a
59+
-- triggering actor from another. Ordering by run_attempt takes the latest attempt.
60+
argMax(
61+
(actor.'login', triggering_actor.'login', run_attempt),
62+
run_attempt
63+
) AS latest
64+
FROM workflow_run
65+
WHERE
66+
id in (
67+
select id from materialized_views.workflow_run_by_head_sha
68+
where head_sha in {shas: Array(String)}
69+
)
70+
-- That materialized view is keyed on head_sha ALONE (its only columns are id and
71+
-- head_sha), so it can return a run id belonging to a different repo that shares the
72+
-- sha -- a fork, most obviously. Constrain the repo here, since the MV cannot.
73+
AND repository.'full_name' = {repo: String}
74+
AND event = 'workflow_dispatch'
75+
AND head_branch LIKE 'trunk/%'
76+
GROUP BY id
77+
) restart_run ON restart_run.id = job.run_id
2578
WHERE
2679
job.name != 'ciflow_should_run'
2780
AND job.name != 'generate-test-matrix'
@@ -33,7 +86,12 @@ WITH job AS (
3386
) -- Should be filtered out by the workflow_event filters, but workflow_event takes some time to populate
3487
AND job.workflow_event != 'workflow_run' -- Filter out workflow_run-triggered jobs, which have nothing to do with the SHA
3588
AND job.workflow_event != 'repository_dispatch' -- Filter out repository_dispatch-triggered jobs, which have nothing to do with the SHA
36-
AND NOT (job.workflow_event = 'workflow_dispatch' AND job.head_branch LIKE 'trunk/%') -- Filter out restart jobs
89+
-- Autorevert restart runs are no longer filtered out here, and are deliberately NOT filtered
90+
-- by conclusion either. Who issued a run must not change how the HUD aggregates it, so a
91+
-- restart is admitted on the same terms as a push or a re-run attempt and the cell verdict is
92+
-- decided by mergeCellRuns over the whole set of runs. Filtering by conclusion here was the
93+
-- issuer-dependent shortcut this replaces: it dropped a restart that failed, was pending or
94+
-- was skipped, which silently hid real results rather than aggregating them.
3795
AND job.id in (select id from materialized_views.workflow_job_by_head_sha where head_sha in {shas: Array(String)})
3896
AND job.repository_full_name = {repo: String}
3997
AND job.workflow_name != 'Upload test stats while running' -- Continuously running cron job that cancels itself to avoid running concurrently
@@ -61,6 +119,35 @@ SELECT
61119
if(line = '', [ ], [ line ]) AS failureLines,
62120
if(line_num = 0, [ ], [ line_num ]) AS failureLineNumbers,
63121
captures as failureCaptures,
64-
annotation as failureAnnotation
122+
annotation as failureAnnotation,
123+
run_origin as runOrigin,
124+
-- The identity fields below are gated on an autorevert origin, so none can appear on a run the
125+
-- origin does not also mark. The two sides are derived independently -- the origin from
126+
-- workflow_job, the identity from workflow_run -- and nothing guarantees they agree under
127+
-- ingestion lag. The reverse case (origin present, identity missing) stays possible and renders
128+
-- fine, since each field is conditional in the tooltip.
129+
-- An unmatched LEFT JOIN also yields the column default ('' / 0) rather than NULL, so normalize:
130+
-- fetchHud strips only nulls, and an empty string would ship on every ordinary run and make
131+
-- "field is present" a false test for "this run was dispatched by autorevert".
132+
-- coalesce, not a bare comparison: run_origin is Nullable, and `NULL != 'autorevert'` is NULL,
133+
-- which would make the whole if() condition NULL rather than false.
134+
if(coalesce(run_origin, '') = 'autorevert', nullIf(restart_actor_login, ''), NULL) as restartDispatchedBy,
135+
-- Only meaningful when it differs: triggering_actor equals the actor on a first attempt, and
136+
-- names whoever re-ran the run on later ones. Collapsing the two would hide a human re-running
137+
-- a bot's restart.
138+
if(
139+
coalesce(run_origin, '') = 'autorevert'
140+
AND restart_triggering_actor_login != ''
141+
AND restart_triggering_actor_login != restart_actor_login,
142+
restart_triggering_actor_login,
143+
NULL
144+
) as restartRerunBy,
145+
-- Deliberately NOT called runAttempt: commit_jobs_query already ships that name with different
146+
-- semantics (the JOB's run_attempt), and the HUD page reads it at
147+
-- pages/hud/[repoOwner]/[repoName]/[branch]/[[...page]].tsx as `cr.run_attempt >
148+
-- (existing.runAttempt ?? 0)` when merging crcr rows. That comparison relies on the field being
149+
-- undefined in the HUD path today, so populating it here would silently change which job data
150+
-- wins those cells.
151+
if(coalesce(run_origin, '') = 'autorevert', nullIf(restart_run_attempt, 0), NULL) as restartRunAttempt
65152
FROM
66153
job

torchci/components/commit/WorkflowBox.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
import { fetcher } from "lib/GeneralUtils";
1717
import { getConclusionSeverityForSorting } from "lib/JobClassifierUtil";
1818
import { getDurationDisplay, isFailedJob } from "lib/jobUtils";
19+
import { describeWorkflowRun } from "lib/runOrigin";
1920
import { getSearchRes, LogSearchResult } from "lib/searchLogs";
2021
import { Artifact, IssueData, JobData } from "lib/types";
2122
import {
@@ -264,11 +265,17 @@ export default function WorkflowBox({
264265
>
265266
<option value={""}>Select Workflow ID</option>
266267
{allWorkflowIds.sort().map((id) => (
268+
// A bare id says nothing about which run it is -- "which one of these was the
269+
// autorevert restart" was the actual question asked in review. Every entry is
270+
// named, not just the interesting ones, so the reader compares like with like
271+
// rather than reading meaning into an absent label.
267272
<option
268273
key={`${id.id} ${id.attempt}`}
269274
value={`${id.id} ${id.attempt}`}
270275
>
271-
{id.id} (Attempt {id.attempt})
276+
{`${id.id} (Attempt ${id.attempt}) — ${describeWorkflowRun(
277+
id
278+
)}`}
272279
</option>
273280
))}
274281
</select>

0 commit comments

Comments
 (0)