> Historic PR-documentation is not maintained along with new PRs — treat it as a snapshot, not as current documentation. ## The Problem Since PR#119 a CI build and a pull-request target build of one repository run at the same time. On mih09, in the repository `werkrevisor` with its PR#5, both started at 15:56:17 and needed the same rootfs, which was not imported yet. Both called `werkdock import … werkator-buildenv-f426341f4a1b`: - The CI build failed with exit code 125: `open …/images/werkator-buildenv-f426341f4a1b/latest.tmp/image.json: no such file or directory`. - The pull-request target build ran on a half-imported image: `bwrap: execvp /bin/sh: Permission denied`. werkdock stages an import in `<image>.tmp` and renames it into place, so an interrupted import leaves no image. But the staging directory has a fixed name, and an import removes it first: the second import deleted the first one's files under it. Two repositories naming the same new rootfs could hit this before PR#119 too, with `maxConcurrent` above 1. The pull-request target build `ai-review` then reported only "build failed after 00:37" to Gitea. The reviewer saw a red gate without a comment, because werkrevisor never started; nothing said that a restart would help. ## Non-Goals - No change in werkdock here; see [Follow-up PRs](#follow-up-prs). - No new `BuildStatus`: the result stays `failed`, only the status text and the log say what failed. A new status would have to be known by the UI, the API, retention and the restart rules, for a difference that only the reader of the status needs. - No lock between two Werkator instances of the same OS user. ## The Scenarios ### Feature: one import per image #### Scenario#121.01: two builds that need the same new image import it once So that neither build fails in the import, nor runs on a half-imported image. - **Given** two builds, of the same or of two repositories of the instance, with the same werkdock rootfs - **and** werkdock does not have the image yet - **When** both start at the same moment - **Then** one of them imports the image - **and** the other waits for that import, finds the image and imports nothing - **and** both start their command ##### Verified by - [WerkdockBuildRunnerUnitTest](../../src/test/kotlin/de/hoennig/werkator/build/WerkdockBuildRunnerUnitTest.kt) `two builds that need the same new image import it once, also from two repositories (Scenario#121.01)` The test fails without the lock: both builds import. #### Scenario#121.02: a build that waited for a failed import imports the image itself So that one failed import costs one build, not every build that waited for it. - **Given** two builds wait for the same new image - **When** the first import fails - **Then** that build fails - **and** the other build imports the image itself and starts its command ##### Verified by - [WerkdockBuildRunnerUnitTest](../../src/test/kotlin/de/hoennig/werkator/build/WerkdockBuildRunnerUnitTest.kt) `a build that waited for a failed import imports the image itself (Scenario#121.02)` ### Feature: a failed environment is told apart from a failed build #### Scenario#121.03: a build that fails before its commands give a result reports a failed environment So that a reader of the status knows there is no verdict of the build, and that a restart may help. - **Given** a build - **When** its runtime fails before the command has a process, for example in the image import - **or** `docker run` or `werkdock run` ends with exit code 125, their code for an error of their own - **Then** the build ends `failed` - **and** its Gitea status says `build environment failed after mm:ss` - **and** its log says that the build's commands did not run, for a failure before the command - **When** the command itself ends with any other exit code - **Then** the status says `build failed after mm:ss` as before ##### Verified by - [BuildExecutorUnitTest](../../src/test/kotlin/de/hoennig/werkator/build/BuildExecutorUnitTest.kt) `a build whose runtime fails before its command runs reports a failed environment, not a failed build (Scenario#121.03)` - [GiteaStatusTextUnitTest](../../src/test/kotlin/de/hoennig/werkator/build/GiteaStatusTextUnitTest.kt) `a build whose environment failed says so, since it has no verdict of its own (Scenario#121.03)` - [DispatchingBuildRunnerUnitTest](../../src/test/kotlin/de/hoennig/werkator/build/DispatchingBuildRunnerUnitTest.kt) `the runtime that runs the build decides whether its exit code is its own failure (Scenario#121.03)` - [WerkdockBuildRunnerUnitTest](../../src/test/kotlin/de/hoennig/werkator/build/WerkdockBuildRunnerUnitTest.kt) `only werkdock's own exit code 125 is a failure of the runtime (Scenario#121.03)` ## The Solution `WerkdockBuildRunner.ensureImage` asks `werkdock images` first, without a lock, as before. If the image is missing, it takes a lock per image name, asks again, and only then downloads and imports. The locks live in the runner's companion object: one per image for the whole JVM, because the image store belongs to the OS user and not to a repository. `BuildRunner.isRuntimeFailure(exitCode, branchConfig)` says whether an exit code is the runtime's own failure. The native runner says never; the Docker and the werkdock runner say so for 125, their documented code for an error of their own; the dispatcher asks the runner that runs the build. The executor marks a build `environmentFailed`: - when an exception ends the run before a command of the build had a process (`commandStarted`), or - when a command's exit code is the runtime's own failure. `GiteaStatusText.of` words a `failed` build with that mark as `build environment failed`. `bwrap: execvp /bin/sh: Permission denied` itself exits with bwrap's code 1, which werkdock passes through like any command's code. Werkator cannot tell it from a failed command; with the lock, the half-imported image that caused it no longer occurs. A local PIT run of the changed classes: | Method | Mutation-CRAP | Complexity | Mutation score | |----------------------------------------|---------------|------------|----------------| | `GiteaStatusText.of` | 10 | 10 | 1.00 | | `BuildExecutor.crashed` | 4 | 4 | 1.00 | | `WerkdockBuildRunner.ensureImage` | 3.14 | 3 | 0.75 | | `WerkdockBuildRunner.isRuntimeFailure` | 2 | 2 | 1.00 | | `DockerBuildRunner.isRuntimeFailure` | 2 | 2 | 1.00 | | `BuildExecutor.runCommand` | 1.42 | 1 | 0.25 | No method of these classes is above 15; the gate stays at 31. ## Additional Changes - `docs/configuration.md`: one sentence on the import in the werkdock notes, one on the status text beside the status context. - Version 1.37.2 with its entry in `releases.html`. ## Follow-up PRs For `mi/werkdock`, not started: - `Store.Import` stages in a directory of its own (`<image>.<random>.tmp`), so concurrent imports from two Werkator instances or a shell cannot remove each other's files; the rename stays the atomic step, and an import that finds the image already in place reports it. - `werkdock run` answers 126 or 127 like docker when bwrap cannot execute the command, so the caller can tell a sandbox that cannot start from a command that failed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: mhoennig <michael@hoennig.de> Reviewed-on: #121
7.6 KiB
PR#121: One Import per Werkdock Image, and a Status Text for a Failed Build Environment
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
Since PR#119 a CI build and a pull-request target build of one repository run at the same time.
On mih09, in the repository werkrevisor with its PR#5, both started at 15:56:17 and needed the same rootfs, which was not imported yet.
Both called werkdock import … werkator-buildenv-f426341f4a1b:
- The CI build failed with exit code 125:
open …/images/werkator-buildenv-f426341f4a1b/latest.tmp/image.json: no such file or directory. - The pull-request target build ran on a half-imported image:
bwrap: execvp /bin/sh: Permission denied.
werkdock stages an import in <image>.tmp and renames it into place, so an interrupted import leaves no image.
But the staging directory has a fixed name, and an import removes it first: the second import deleted the first one's files under it.
Two repositories naming the same new rootfs could hit this before PR#119 too, with maxConcurrent above 1.
The pull-request target build ai-review then reported only "build failed after 00:37" to Gitea.
The reviewer saw a red gate without a comment, because werkrevisor never started; nothing said that a restart would help.
Non-Goals
- No change in werkdock here; see Follow-up PRs.
- No new
BuildStatus: the result staysfailed, only the status text and the log say what failed. A new status would have to be known by the UI, the API, retention and the restart rules, for a difference that only the reader of the status needs. - No lock between two Werkator instances of the same OS user.
The Scenarios
Feature: one import per image
Scenario#121.01: two builds that need the same new image import it once
So that neither build fails in the import, nor runs on a half-imported image.
- Given two builds, of the same or of two repositories of the instance, with the same werkdock rootfs
- and werkdock does not have the image yet
- When both start at the same moment
- Then one of them imports the image
- and the other waits for that import, finds the image and imports nothing
- and both start their command
Verified by
- WerkdockBuildRunnerUnitTest
two builds that need the same new image import it once, also from two repositories (Scenario#121.01)
The test fails without the lock: both builds import.
Scenario#121.02: a build that waited for a failed import imports the image itself
So that one failed import costs one build, not every build that waited for it.
- Given two builds wait for the same new image
- When the first import fails
- Then that build fails
- and the other build imports the image itself and starts its command
Verified by
- WerkdockBuildRunnerUnitTest
a build that waited for a failed import imports the image itself (Scenario#121.02)
Feature: a failed environment is told apart from a failed build
Scenario#121.03: a build that fails before its commands give a result reports a failed environment
So that a reader of the status knows there is no verdict of the build, and that a restart may help.
- Given a build
- When its runtime fails before the command has a process, for example in the image import
- or
docker runorwerkdock runends with exit code 125, their code for an error of their own
- or
- Then the build ends
failed- and its Gitea status says
build environment failed after mm:ss - and its log says that the build's commands did not run, for a failure before the command
- and its Gitea status says
- When the command itself ends with any other exit code
- Then the status says
build failed after mm:ssas before
Verified by
- BuildExecutorUnitTest
a build whose runtime fails before its command runs reports a failed environment, not a failed build (Scenario#121.03) - GiteaStatusTextUnitTest
a build whose environment failed says so, since it has no verdict of its own (Scenario#121.03) - DispatchingBuildRunnerUnitTest
the runtime that runs the build decides whether its exit code is its own failure (Scenario#121.03) - WerkdockBuildRunnerUnitTest
only werkdock's own exit code 125 is a failure of the runtime (Scenario#121.03)
The Solution
WerkdockBuildRunner.ensureImage asks werkdock images first, without a lock, as before.
If the image is missing, it takes a lock per image name, asks again, and only then downloads and imports.
The locks live in the runner's companion object: one per image for the whole JVM, because the image store belongs to the OS user and not to a repository.
BuildRunner.isRuntimeFailure(exitCode, branchConfig) says whether an exit code is the runtime's own failure.
The native runner says never; the Docker and the werkdock runner say so for 125, their documented code for an error of their own; the dispatcher asks the runner that runs the build.
The executor marks a build environmentFailed:
- when an exception ends the run before a command of the build had a process (
commandStarted), or - when a command's exit code is the runtime's own failure.
GiteaStatusText.of words a failed build with that mark as build environment failed.
bwrap: execvp /bin/sh: Permission denied itself exits with bwrap's code 1, which werkdock passes through like any command's code.
Werkator cannot tell it from a failed command; with the lock, the half-imported image that caused it no longer occurs.
A local PIT run of the changed classes:
| Method | Mutation-CRAP | Complexity | Mutation score |
|---|---|---|---|
GiteaStatusText.of |
10 | 10 | 1.00 |
BuildExecutor.crashed |
4 | 4 | 1.00 |
WerkdockBuildRunner.ensureImage |
3.14 | 3 | 0.75 |
WerkdockBuildRunner.isRuntimeFailure |
2 | 2 | 1.00 |
DockerBuildRunner.isRuntimeFailure |
2 | 2 | 1.00 |
BuildExecutor.runCommand |
1.42 | 1 | 0.25 |
No method of these classes is above 15; the gate stays at 31.
Additional Changes
docs/configuration.md: one sentence on the import in the werkdock notes, one on the status text beside the status context.- Version 1.37.2 with its entry in
releases.html.
Follow-up PRs
For mi/werkdock, not started:
Store.Importstages in a directory of its own (<image>.<random>.tmp), so concurrent imports from two Werkator instances or a shell cannot remove each other's files; the rename stays the atomic step, and an import that finds the image already in place reports it.werkdock runanswers 126 or 127 like docker when bwrap cannot execute the command, so the caller can tell a sandbox that cannot start from a command that failed.
🤖 Generated with Claude Code