Skip to content

ci: quarantine flaky tests against a ticket instead of retrying past them - #777

Draft
rkennke wants to merge 1 commit into
mainfrom
ci/flake-visibility
Draft

ci: quarantine flaky tests against a ticket instead of retrying past them#777
rkennke wants to merge 1 commit into
mainfrom
ci/flake-visibility

Conversation

@rkennke

@rkennke rkennke commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?:

Makes flaky tests a tracked, expiring debt instead of something CI quietly retries past.

The retry now buys a label, not a pass:

flaky failed one attempt, passed another
broken failed every attempt
gating not on the quarantine list — red, whichever of the above it is

A flake fails the build until someone quarantines it against a PROF ticket in ddprof-test/quarantine.txt. Every entry carries a review_by date and CI fails once it passes, so the list expires rather than accumulating. Quarantined tests still run and report — only the gating is suspended, so the pass rate keeps saying whether the test is recovering or has quietly become permanently broken.

To keep the honest path the cheap one, the PR comment prints a ready-to-paste entry for each flaky test, with cells pre-narrowed to the axis that actually failed.

Motivation:

CI could not tell a flaky test from a broken one, and its one retry erased the evidence either way.

  • The retry existed only for ASan. A test that failed on attempt 1 and passed on attempt 2 left a green job and a ::warning:: in a log, naming nothing.
  • Test reports upload only if: failure() — so the run that recovered on a retry, the one worth studying, produced no artifact at all.
  • generate-test-summary.sh already downloaded (test-reports)* and grepped TEST-*.xml for failed test names, but prepare_reports.sh copies build/reports/tests (the HTML) and never build/test-results (the XML). It searched artifacts containing no XML, so every failed job rendered "No detailed failure information available". A ** glob with no shopt -s globstar would have stopped it recursing even had the files been there.

Answering "is this flaky?" therefore meant hand-comparing PRs by eye. Retrying until green would only have made the tolerance official.

Additional Notes:

Why a text table and not JSON/YAML. The list is edited by hand far more often than by machine, so real comments, one-line diffs and clean git blame matter more than a schema. It also has to parse inside the Alpine test containers, where PyYAML cannot be assumed — this needs nothing but str.split.

Failing to classify is itself a failure. If flake_report.py cannot run, the job goes red rather than inheriting a pass nothing examined. An earlier draft had || true there and turned a real flake green during testing; there's a regression test for it now.

Behaviour changes worth knowing: retry is enabled for all four platform jobs (previously ASan only), so a flake-shaped failure costs that cell a second run; slow suites are capped at one attempt to stay clear of the 180-minute timeout; ASan keeps its retry-on-abort via RETRY_ON_NO_TEST_FAILURES, since it names no test when it dies at init.

Deferred: auto-filing PROF tickets (needs dedupe and an Atlassian credential for CI), and the GitLab dd-trace integration matrix, which still gets one shot per config.

Not exercised on real CI: the retry and gating paths only run once something has failed, which is why they are covered by fixtures instead.

How to test the change?:

.github/scripts/tests/test_quarantine.sh — 15 assertions, run by the new validate-quarantine job:

  • validator: missing ticket, non-PROF ticket, expired review_by, duplicate entry, malformed line, and that the committed list is itself valid
  • matcher: cell globs (*arm64* matches arm64 cells and not amd64), class wildcards (a.B.*)
  • gating: an un-quarantined flake stays red; a quarantined failure goes green and is still recorded; a build error is never excused by the list; an unreadable list cannot yield green

For Datadog employees:

  • This PR doesn't touch any of that.
  • JIRA: [JIRA-XXXX]

…them

CI could not tell a flaky test from a broken one, and its one retry erased the
evidence either way.

- The retry existed only for ASan. A test that failed on attempt 1 and passed
  on attempt 2 left a green job and a ::warning:: in a log, naming nothing.
- Test reports upload only `if: failure()`, so the run that recovered on a
  retry -- the one worth studying -- produced no artifact at all.
- generate-test-summary.sh already downloaded `(test-reports)*` and grepped
  TEST-*.xml for failed test names, but prepare_reports.sh copies
  build/reports/tests (the HTML) and never build/test-results (the XML), so it
  searched artifacts containing no XML and every failed job rendered "No
  detailed failure information available". A `**` glob with no `shopt -s
  globstar` would have stopped it recursing even had the files been there.

Retrying until green would only have made the tolerance official. Instead the
retry now buys a label and nothing else, and an explicit list decides what may
fail:

  flaky       failed one attempt, passed another
  broken      failed every attempt
  gating      not on the quarantine list -- red, whichever of the above it is

So a flake fails the build until somebody quarantines it against a PROF ticket.
ddprof-test/quarantine.txt is a plain "|"-separated table, one entry per line,
chosen over JSON/YAML because it is edited by hand far more than by machine:
real comments, one-line diffs, clean git blame, and no parser beyond str.split
(it must also load inside the Alpine containers, where PyYAML is not a given).

Every entry carries a ticket and a review_by date, and validate-quarantine
fails CI once that date passes -- otherwise the list only grows and quarantine
becomes a permanent mute rather than tracked debt. Quarantined tests still run
and still report; only the gating is suspended, so the pass rate keeps saying
whether the test is recovering or has quietly become permanently broken.

To keep the honest path the cheap one, the PR comment prints a filled-in entry
to paste, with a `cells` glob narrowed to the axis that actually failed. The
ticket and the judgement stay with a person; the typing does not.

Reporting is grouped by test rather than by cell -- one flaky test reddens a
dozen cells and so do a dozen unrelated breakages -- and per-cell outcomes now
upload whether the cell passed or failed, since a cell that failed only on its
first attempt produces no failure artifact.

Failing to classify is itself a failure: if flake_report.py cannot run, the job
goes red rather than inheriting a pass nothing examined. An earlier draft had
`|| true` there and turned a real flake green in testing.

test_quarantine.sh covers the gating decisions against fixtures, including that
an un-quarantined flake stays red, a quarantined one does not, a build error is
never excused by the list, and an unreadable list cannot yield green. It runs
in the validate-quarantine job. The retry path only executes once something has
failed, so CI would otherwise never exercise it.

Deferred: auto-filing PROF tickets (needs dedupe and an Atlassian credential
for CI) and the GitLab dd-trace integration matrix, which still gets one shot
per config.
@dd-octo-sts

dd-octo-sts Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Scan-Build Report

User:runner@runnervmgx7h7
Working Directory:/home/runner/work/java-profiler/java-profiler/ddprof-lib/src/test/make
Command Line:make -j4 all
Clang Version:Ubuntu clang version 18.1.3 (1ubuntu1)
Date:Wed Sep 2 18:51:15 2026

Bug Summary

Bug TypeQuantityDisplay?
All Bugs1
Logic error
Dereference of null pointer1

Reports

Bug Group Bug Type ▾ File Function/Method Line Path Length
Logic errorDereference of null pointerfaultInjection.cppcrashNow242

@dd-octo-sts

dd-octo-sts Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

CI Test Results

Run: #33669575587 | Commit: 4d2d075 | Duration: 14m 45s (longest job)

All 32 test jobs passed

Status Overview

JDK glibc-aarch64/debug glibc-amd64/debug musl-aarch64/debug musl-amd64/debug
8 - - -
8-ibm - - -
8-j9 - -
8-librca - -
8-orcl - - -
11 - - -
11-j9 - -
11-librca - -
17 - -
17-graal - -
17-j9 - -
17-librca - -
21 - -
21-graal - -
21-librca - -
25 - -
25-graal - -
25-librca - -

Legend: ✅ passed | ❌ failed | ⚪ skipped | 🚫 cancelled

Summary: Total: 32 | Passed: 32 | Failed: 0


Updated: 2026-09-02 19:06:55 UTC

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant