The cancel test failed now and then because it asserted an ordering the executor deliberately does not provide. A build reaches its terminal status and leaves currentBuilds() at two different moments, and between them lie the status event, the commit status posted to the forge and the artifact copy. The test waited for the first and asserted the second in the next line, which held on an idle machine and not under load — reproduced here in one of three consecutive runs. The assertion moves into eventually, like the other waits in this test. Both places now say why the order is what it is: a build gone from currentBuilds() while its stored result still says RUNNING would read as stuck forever, and the watcher's worktree pruning borrows that same list to know which worktrees are still in use. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: mhoennig <michael@hoennig.de> Reviewed-on: #46
3.3 KiB
WARNING: This document describes only the change applied in this PR. It may already be outdated once the next PR is merged. Historic PR-documentation is not maintained along with new PRs — treat it as a snapshot, not as current documentation.
The Problem
BuildExecutorTest's "cancel only affects the addressed build, other branches keep running" fails now and then, always the same way: after cancelling build A, currentBuilds() still contains it beside build B.
It is not a race in the executor. The test asserts an ordering the executor deliberately does not provide.
A build reaches its terminal status and leaves currentBuilds() at two different moments, and there is real work between them:
transition()stores the terminal status — from here the test'sawaitStatusreturns,- the same call publishes
BuildStatusChangedEventsynchronously to every listener, - and then posts the commit status to the forge, an HTTP request,
artifactStore.persist()copies the build's artifacts,- only now
builds.remove()takes it out ofcurrentBuilds().
The test waited for step 1 and asserted step 5 in the next line.
On an idle machine that usually works; under load — a full ./gradlew build, or a busy workstation — it does not.
Reproduced here in one of three consecutive runs while other work was running.
Non-Goals
- Not changing the order. It is the benign one, see below.
- Not making
currentBuilds()exclude builds that already reached a terminal status. That would give the test its expectation and would also stop the Current view from briefly listing a finished build — but the watcher's worktree pruning relies on exactly this list to know which worktrees are in use, and a build in step 4 is still reading its worktree. Excluding it would let a branch that vanished from origin have its worktree deleted underneath a running artifact copy.
The Scenarios
Feature: cancelling one build of several
Scenario#46.01: The list of current builds catches up with the status
- Given two builds running, one of them cancelled
- Then its result reaches
CANCELLEDfirst- and it leaves
currentBuilds()once its artifacts are persisted, a moment later - and the other build is untouched throughout
- and it leaves
Verified by
The Solution
The assertion moves inside eventually, like every other assertion in this test that waits for the executor.
Both places say why. The test says the list trails the status by design and must not be read in the same breath; BuildExecutor says why the removal is last — a reader that saw a build gone from currentBuilds() while its stored result still said RUNNING would see a build stuck forever, whereas one that is terminal and still listed simply reads as finishing, and the worktree pruning depends on the same order.
Open Questions
- The Current view can briefly show a build that has already finished, for as long as its artifacts take to persist. It is honest — the build is still doing something — but it is not what "current" suggests. Fixing it properly means giving the worktree pruning its own notion of "still using a worktree" rather than borrowing
currentBuilds().
Additional Changes
- No version bump: nothing in the product changes, so there is nothing to deploy.