Skip to content

Commit ac261fc

Browse files
izaitsevfbIvan
andauthored
[autorevert] dedupe shared recovery-detection pipeline across the two metrics queries (#8180)
## Summary `autorevert_significant_reverts` and `autorevert_weekly_metrics` both implement the same red/green-streak recovery-detection + causal-attribution pipeline — ~140 identical lines (`commits` → … → `recovery_with_attribution`). They've drifted once already: the causal red-streak filter from #8176 had to be applied to each file separately, and fixing only the summary query left the weekly chart stale until both were patched. This PR makes that shared pipeline a single, drift-proof definition. ## Approach torchci has no SQL include/templating mechanism (each `query.sql` is sent to ClickHouse verbatim), and ClickHouse view DDL is deployed manually / out-of-band. A shared **parameterized view** would be the DRY end-state, but it can't be created or verified from the read-only query path, and a merged query referencing a not-yet-deployed view would 500 the HUD metrics page until the DDL is applied by hand. So this keeps both queries self-contained and runnable, but: 1. Factors the **entire** shared pipeline — `commits` through `recovery_with_attribution`, plus a new `causally_attributed_recoveries` CTE that **centralizes the #8176 causal filter** (`reverted_commit_sha = '' OR has(red_shas, reverted_commit_sha)`) — into a block delimited by `-- @autorevert-shared-recovery-pipeline:begin` / `:end` markers. The block is **byte-identical** in both files. Only each query's final aggregation (per-revert-commit vs per-week) differs, below the `:end` marker. 2. Adds a **jest unit test** (`torchci/test/autorevertSharedPipeline.test.ts`) that fails CI if the two marked blocks aren't byte-identical, if a marker is missing/duplicated, or if the two `params.json` declare different `params`. The extractor itself is unit-tested (missing / duplicated / reversed markers) so the guard can't silently no-op. A future fix to the recovery pipeline therefore *cannot* land in one query but not the other. Moving the causal filter into the shared `causally_attributed_recoveries` CTE is the key win: previously each query re-stated it in its own tail (`reverts_only` / `unique_recoveries`), which is exactly what drifted in #8176. Now it lives once, inside the byte-identical block. > **Note:** an earlier revision of this PR enforced the byte-identity with a `lintrunner` adapter (`AUTOREVERT_SHARED_CTE`); per review it's now a torchci jest test instead, so the guard runs in the normal test suite rather than extending the linter stack. ## Validation Confirmed against ClickHouse that both refactored queries return results **identical** to the pre-refactor versions: | query | window | result | |---|---|---| | `autorevert_significant_reverts` | 2026-06-08 .. 06-15 | identical (3 rows; PR #186928 / `889f6eb` correctly stays excluded) | | `autorevert_weekly_metrics` | 2026-06-08 .. 06-15 | identical (`human_revert_recoveries = 0`) | | `autorevert_weekly_metrics` | 2026-05-01 .. 06-15 | identical across all 7 weeks (incl. human-revert and non-revert paths) | The transformation is algebraically equivalence-preserving: `significant`'s `reverts_only` predicate `is_revert = 1 AND <causal>` becomes `<causal>` (applied upstream) then `is_revert = 1`; `weekly`'s `unique_recoveries` reads the same pre-filtered set it did before. The extra (superset) columns now carried through `weekly`'s shared CTEs are unused by its final aggregation and don't change row cardinality. `prettier --check`, `tsc`, and `eslint` run clean on the new test locally (the jest run itself needs Node ≥18; the test reads the two real query files and asserts byte-identity + equal params, validated independently here). ## A parameterized view as a possible future migration If the team prefers the fully DRY form, this shared block is a clean candidate for a parameterized ClickHouse view that both queries `SELECT` from — but that needs a coordinated manual DDL deploy (the view must exist before the deployed app references it) plus a one-time confirmation that the read user can `SELECT` from a parameterized view taking an `Array(String)` argument. I'm happy to follow up with that as a separate, deploy-coordinated PR. This change is the deployment-free first step that closes the drift hole now. --------- Co-authored-by: Ivan <izaitsevfb@meta.com>
1 parent a0685f6 commit ac261fc

3 files changed

Lines changed: 219 additions & 37 deletions

File tree

torchci/clickhouse_queries/autorevert_significant_reverts/query.sql

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22
-- Finds recovery events that are reverts and attributes them to autorevert vs human
33
-- Used for autorevert metrics precision/recall calculations
44

5+
-- The block between the @autorevert-shared-recovery-pipeline markers below is kept
6+
-- BYTE-IDENTICAL with autorevert_weekly_metrics/query.sql. The
7+
-- autorevertSharedPipeline test (torchci/test/autorevertSharedPipeline.test.ts)
8+
-- fails CI if the two copies drift, so any change to the recovery-detection or
9+
-- causal-attribution pipeline (e.g. the #8176 causal red-streak filter) MUST be
10+
-- applied identically to BOTH files. Only each query's final aggregation, below the
11+
-- :end marker, is allowed to differ.
12+
-- @autorevert-shared-recovery-pipeline:begin
513
WITH commits AS (
614
SELECT
715
push.head_commit.'timestamp' AS time,
@@ -264,21 +272,32 @@ recovery_with_attribution AS (
264272
LEFT JOIN autorevert_events a ON r.reverted_commit_sha = a.reverted_sha
265273
),
266274

267-
-- Filter to only actual reverts before aggregating, and require the reverted
268-
-- commit to actually belong to the red streak it supposedly recovered. This drops
269-
-- spurious recoveries where an unrelated/flaky signal merely went red->green at the
270-
-- revert commit while the reverted commit was never part of that red streak (e.g.
271-
-- out-of-plane reverts -- ghfirst/nosignal -- credited with a coincidental flake
272-
-- clear). When the reverted commit SHA can't be parsed from the message (e.g.
273-
-- Reapply / Back out shapes), the row is kept unchanged.
274-
reverts_only AS (
275+
-- Step 10: Apply the causal-attribution filter centrally, so BOTH downstream
276+
-- queries (autorevert_significant_reverts and autorevert_weekly_metrics) share it
277+
-- and cannot drift. A revert only "fixes" a signal when the reverted commit is
278+
-- actually part of the red streak that recovered; spurious recoveries -- an
279+
-- unrelated/flaky signal that merely went red->green at the revert commit while the
280+
-- reverted commit was never in that red streak (e.g. out-of-plane ghfirst/nosignal
281+
-- reverts credited with a coincidental flake clear) -- are dropped. When the
282+
-- reverted commit SHA can't be parsed from the message (Reapply / Back out shapes),
283+
-- the row is kept unchanged.
284+
causally_attributed_recoveries AS (
275285
SELECT * FROM recovery_with_attribution
276286
WHERE
277-
is_revert = 1
278-
AND (
279-
reverted_commit_sha = ''
280-
OR has(red_shas, reverted_commit_sha)
281-
)
287+
reverted_commit_sha = ''
288+
OR has(red_shas, reverted_commit_sha)
289+
),
290+
-- @autorevert-shared-recovery-pipeline:end
291+
292+
-- ===========================================================================
293+
-- Query-specific tail: per-revert-commit detail for the precision/recall table.
294+
-- ===========================================================================
295+
296+
-- Filter to only actual reverts (the causal-attribution filter is already applied
297+
-- upstream in causally_attributed_recoveries).
298+
reverts_only AS (
299+
SELECT * FROM causally_attributed_recoveries
300+
WHERE is_revert = 1
282301
),
283302

284303
-- Aggregate by recovery_sha (one row per unique revert commit)

torchci/clickhouse_queries/autorevert_weekly_metrics/query.sql

Lines changed: 79 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22
-- Aggregates signal recovery and revert data by week for trend analysis
33
-- Computes key metrics: total recoveries, autorevert rate, human revert rate
44

5+
-- The block between the @autorevert-shared-recovery-pipeline markers below is kept
6+
-- BYTE-IDENTICAL with autorevert_significant_reverts/query.sql. The
7+
-- autorevertSharedPipeline test (torchci/test/autorevertSharedPipeline.test.ts)
8+
-- fails CI if the two copies drift, so any change to the recovery-detection or
9+
-- causal-attribution pipeline (e.g. the #8176 causal red-streak filter) MUST be
10+
-- applied identically to BOTH files. Only each query's final aggregation, below the
11+
-- :end marker, is allowed to differ.
12+
-- @autorevert-shared-recovery-pipeline:begin
513
WITH commits AS (
614
SELECT
715
push.head_commit.'timestamp' AS time,
@@ -42,6 +50,7 @@ all_jobs AS (
4250
all_runs.workflow_name AS workflow_name,
4351
job.run_attempt AS run_attempt,
4452
job.conclusion AS raw_conclusion,
53+
-- Normalize job name to group shards together (same as auto-revert logic)
4554
trim(
4655
replaceRegexpAll(
4756
replaceRegexpAll(
@@ -64,6 +73,7 @@ all_jobs AS (
6473
)
6574
),
6675

76+
-- Step 1: For each (sha, base_name, run_attempt), determine attempt status
6777
attempt_status AS (
6878
SELECT
6979
time,
@@ -79,6 +89,7 @@ attempt_status AS (
7989
GROUP BY time, sha, message, base_name, workflow_name, run_attempt
8090
),
8191

92+
-- Step 2: For each (sha, base_name), aggregate across all attempts
8293
signal_status AS (
8394
SELECT
8495
time,
@@ -96,6 +107,7 @@ signal_status AS (
96107
GROUP BY time, sha, message, base_name
97108
),
98109

110+
-- Step 3: Assign streak IDs using cumulative status changes
99111
signal_with_streaks AS (
100112
SELECT
101113
base_name,
@@ -104,25 +116,30 @@ signal_with_streaks AS (
104116
time,
105117
message,
106118
status,
119+
-- Change marker: 1 when status differs from previous
107120
if(status != lagInFrame(status, 1, status) OVER w, 1, 0) AS is_change
108121
FROM signal_status
109-
WHERE status IN ('red', 'green')
122+
WHERE status IN ('red', 'green') -- Focus on definitive states
110123
WINDOW w AS (
111124
PARTITION BY base_name
112125
ORDER BY time ASC
113126
)
114127
),
115128

129+
-- Step 4: Compute streak ID (cumulative sum of changes)
116130
signal_with_streak_ids AS (
117131
SELECT
118132
*,
119-
sum(is_change) OVER (
120-
PARTITION BY base_name
121-
ORDER BY time ASC ROWS UNBOUNDED PRECEDING
122-
) AS streak_id
133+
sum(is_change)
134+
OVER (
135+
PARTITION BY base_name
136+
ORDER BY time ASC ROWS UNBOUNDED PRECEDING
137+
)
138+
AS streak_id
123139
FROM signal_with_streaks
124140
),
125141

142+
-- Step 5: Count streak lengths and find boundaries
126143
streak_lengths AS (
127144
SELECT
128145
base_name,
@@ -138,9 +155,11 @@ streak_lengths AS (
138155
GROUP BY base_name, streak_id, status
139156
),
140157

141-
-- SHAs comprising each red streak, for causal attribution: a revert only
142-
-- genuinely "fixes" a signal if the reverted commit is part of the red streak
143-
-- that recovered (mirrors autorevert_significant_reverts).
158+
-- Step 5b: Collect the SHAs that make up each red streak (for causal attribution).
159+
-- A revert only genuinely "fixes" a signal if the reverted commit is actually part
160+
-- of the red streak that recovered. Otherwise the red->green transition at the
161+
-- revert commit is coincidental -- e.g. a flaky signal that merely happened to go
162+
-- green at the revert -- and crediting the revert with it inflates the FN count.
144163
red_streak_members AS (
145164
SELECT
146165
base_name,
@@ -151,6 +170,7 @@ red_streak_members AS (
151170
GROUP BY base_name, streak_id
152171
),
153172

173+
-- Step 6: Find recovery events: green streak that follows a red streak
154174
recovery_events AS (
155175
SELECT
156176
green.base_name AS signal_key,
@@ -161,7 +181,9 @@ recovery_events AS (
161181
green.streak_start AS recovery_time,
162182
green.first_message AS recovery_message,
163183
red.last_sha AS last_red_sha,
164-
red.streak_end AS last_red_time
184+
red.streak_end AS last_red_time,
185+
red.first_sha AS first_red_sha,
186+
red.streak_start AS first_red_time
165187
FROM streak_lengths green
166188
JOIN streak_lengths red
167189
ON
@@ -173,7 +195,7 @@ recovery_events AS (
173195
AND green.streak_length >= {minGreenCommits: UInt8}
174196
),
175197

176-
-- Get autorevert events for attribution
198+
-- Step 7: Get autorevert events for attribution
177199
autorevert_events AS (
178200
SELECT
179201
toString(commit_sha) AS reverted_sha,
@@ -185,24 +207,37 @@ autorevert_events AS (
185207
AND action = 'revert'
186208
AND dry_run = 0
187209
AND failed = 0
210+
-- Convert DateTime64 params to DateTime for comparison
188211
AND ts >= toDateTime({startTime: DateTime64(3)}) - INTERVAL 1 DAY
189212
AND ts < toDateTime({stopTime: DateTime64(3)}) + INTERVAL 1 DAY
190213
),
191214

192-
-- Extract reverted commit SHA from recovery message
215+
-- Step 8: Extract reverted commit SHA from recovery message
193216
recovery_with_reverted_sha AS (
194217
SELECT
195218
r.*,
219+
-- Check if recovery commit is a revert
196220
(
197221
r.recovery_message LIKE 'Revert %'
198222
OR r.recovery_message LIKE 'Reapply %'
199223
OR r.recovery_message LIKE 'Back out%'
200224
) AS is_revert,
225+
-- Extract reverted PR number if it's a revert
226+
extractAll(
227+
r.recovery_message,
228+
'Reverted https://github.com/pytorch/pytorch/pull/(\\d+)'
229+
) AS reverted_pr_numbers,
230+
-- Extract PR number from merge commit message
231+
extractAll(
232+
r.recovery_message,
233+
'Pull Request resolved: https://github.com/pytorch/pytorch/pull/(\\d+)'
234+
) AS merge_pr_numbers,
201235
-- Extract the actual reverted commit SHA from message (e.g., "This reverts commit abc123...")
202236
-- The regex captures the full 40-char SHA since commit messages include full SHAs
203237
arrayElement(
204238
extractAll(r.recovery_message, 'reverts commit ([a-f0-9]+)'), 1
205239
) AS reverted_commit_sha,
240+
-- SHAs comprising the red streak this recovery resolves (causal filter input)
206241
rm.red_shas AS red_shas
207242
FROM recovery_events r
208243
LEFT JOIN red_streak_members rm
@@ -211,7 +246,7 @@ recovery_with_reverted_sha AS (
211246
AND rm.streak_id = r.red_streak_id
212247
),
213248

214-
-- Join with autorevert events on full SHA match
249+
-- Step 9: Join with autorevert events on full SHA match
215250
recovery_with_attribution AS (
216251
SELECT
217252
r.signal_key,
@@ -222,32 +257,52 @@ recovery_with_attribution AS (
222257
r.recovery_message,
223258
r.last_red_sha,
224259
r.last_red_time,
260+
r.first_red_sha,
261+
r.first_red_time,
225262
r.is_revert,
263+
r.reverted_pr_numbers,
264+
r.merge_pr_numbers,
226265
r.reverted_commit_sha,
227266
r.red_shas,
228267
-- Check for autorevert attribution by matching the reverted commit SHA
229-
a.reverted_sha IS NOT NULL AND a.reverted_sha != '' AS is_autorevert
268+
a.reverted_sha IS NOT NULL AND a.reverted_sha != '' AS is_autorevert,
269+
a.autorevert_time,
270+
a.source_signal_keys AS autorevert_signal_keys
230271
FROM recovery_with_reverted_sha r
231272
LEFT JOIN autorevert_events a ON r.reverted_commit_sha = a.reverted_sha
232273
),
233274

234-
-- Deduplicate by recovery_sha to count unique revert commits
235-
-- A single revert can fix multiple signals, but we count it as one revert event
275+
-- Step 10: Apply the causal-attribution filter centrally, so BOTH downstream
276+
-- queries (autorevert_significant_reverts and autorevert_weekly_metrics) share it
277+
-- and cannot drift. A revert only "fixes" a signal when the reverted commit is
278+
-- actually part of the red streak that recovered; spurious recoveries -- an
279+
-- unrelated/flaky signal that merely went red->green at the revert commit while the
280+
-- reverted commit was never in that red streak (e.g. out-of-plane ghfirst/nosignal
281+
-- reverts credited with a coincidental flake clear) -- are dropped. When the
282+
-- reverted commit SHA can't be parsed from the message (Reapply / Back out shapes),
283+
-- the row is kept unchanged.
284+
causally_attributed_recoveries AS (
285+
SELECT * FROM recovery_with_attribution
286+
WHERE
287+
reverted_commit_sha = ''
288+
OR has(red_shas, reverted_commit_sha)
289+
),
290+
-- @autorevert-shared-recovery-pipeline:end
291+
292+
-- ===========================================================================
293+
-- Query-specific tail: weekly aggregation of recovery counts and rates.
294+
-- ===========================================================================
295+
296+
-- Deduplicate by recovery_sha to count unique revert commits. A single revert can
297+
-- fix multiple signals, but we count it as one revert event. The causal-attribution
298+
-- filter is already applied upstream in causally_attributed_recoveries.
236299
unique_recoveries AS (
237300
SELECT
238301
recovery_sha,
239302
any(recovery_time) AS recovery_time,
240303
max(is_revert) AS is_revert,
241304
max(is_autorevert) AS is_autorevert
242-
FROM recovery_with_attribution
243-
-- Causal filter (mirrors autorevert_significant_reverts): a revert only counts
244-
-- as fixing a signal if the reverted commit is part of that signal's red streak.
245-
-- Drops coincidental flake-clears at the revert commit so the weekly chart's
246-
-- human_revert_recoveries / recall stay consistent with the summary numbers.
247-
-- Non-revert recoveries have an empty reverted_commit_sha and are kept.
248-
WHERE
249-
reverted_commit_sha = ''
250-
OR has(red_shas, reverted_commit_sha)
305+
FROM causally_attributed_recoveries
251306
GROUP BY recovery_sha
252307
)
253308

0 commit comments

Comments
 (0)