.buildkite/scripts/smart-retry/README.md
Smart retry handles automatic retries for tasks/tests in two different scenarios:
The basic idea is that when a Buildkite step fails, or is cancelled due to the spot instance being terminated, we need to retry the step, but skip all of the successful work we did in the previous test.
There are a lot of edge cases. You could have a scenario like:
All 4 attempts have to be thought of as a single, combined run to understand what the end result is. The end result that is relevant to the user is:
Here's the overall process that makes this work, ignoring spot preemptions for now:
task-status from the previous step, and have Gradle skip all tests/tasks that completed successfully across previous attempts.task-status file with the final status of all tests and tasks.task-status from the previous attempt, if there was one, so that each attempt has a cumulative view of all the previous attempts.InternalTestRerunPlugin prunes previously successful work at three levels, from coarsest to finest:
successfulTasks is disabled outright with onlyIf(false).successfulSuites is excluded with a Class.* filter pattern.Class#method reference listed in successfulTests is excluded with a filter pattern for that method.Individual test pruning needs care for suites run by the randomized runner with a @ParametersFactory, because those report one test per parameter, for example test {yaml=analysis-common/30_tokenizers/letter}.
RandomizedRunner#applyFilters keeps a test candidate as soon as either its parameterized description or its bare method description is allowed to run, so excluding only the parameterized name has no effect at all.
The plugin therefore emits the bare Class.method pattern in addition to the parameterized one.
That does not take the method's other parameters down with it: those still carry a parameterized name that no pattern matches, which is enough to keep them running.
MutedTestsBuildService does the same thing for muted-tests.yml, and the two mechanisms contribute patterns to the same filter.
Individual test pruning assumes tests are independent. Upgrade suites are not: they are parameterized over upgrade phases and an early phase sets up the cluster state a later phase asserts on, so skipping a phase that already passed makes the later phase fail.
Projects hosting such suites opt out in their build.gradle:
smartRetry.pruneIndividualTests.set(false)
Task and suite pruning stay active for those projects, since both either skip a suite in its entirety or not at all.
Here's how the above process additionally handles spot preemptions:
task-status file as usual.PREEMPTED and also add a value for the preemption time. Upload the buildscan as normal.In the subsequent retry, any tests/tasks that didn't fully complete before the preemption (or never ran at all), will execute.
task-status report at the end of execution