fix(dockerutil): drop AutoRemove on one-off helper containers - #2
Merged
Conversation
The short-lived busybox helpers that chown a chain's data volume (volumeowner), write files into it (filewriter), and read files back out (fileretriever) each ran with HostConfig.AutoRemove=true. For the two that start the container and then ContainerWait on it, the near-instant command exits before the wait attaches on newer Docker Engines (25+), so the daemon reaps the container first and the wait returns "No such container" — failing SetVolumeOwner / file writes and any chain boot on such hosts (e.g. Docker Desktop with Engine 28.x on macOS). Remove AutoRemove from all three and rely on the existing deferred ContainerRemove(Force) to clean up the stopped container instead, closing the race while still removing it. (fileretriever never starts its container, so it was race-free; changed for uniformity.) Verified: dchain interchaintest TestDchainBoots boots to height 5 and PASSes on Docker Engine 28.3.0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes a Docker wait-ordering race in dockerutil one-off helper containers by removing daemon-side AutoRemove so containers persist long enough for ContainerWait(..., NotRunning) to reliably observe exit, and then relying on explicit ContainerRemove(..., Force: true) cleanup.
Changes:
- Removed
HostConfig.AutoRemove: truefrom the volume owner helper container to avoidContainerWaitracing container removal. - Removed
HostConfig.AutoRemove: truefrom the file writer helper container for the sameContainerWait/removal race. - Removed
HostConfig.AutoRemove: truefrom the file retriever helper container for uniformity (even though it is never started).
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| dockerutil/volumeowner.go | Drops AutoRemove to prevent helper container removal racing ContainerWait. |
| dockerutil/filewriter.go | Drops AutoRemove to prevent helper container removal racing ContainerWait. |
| dockerutil/fileretriever.go | Drops AutoRemove for uniformity across helper containers. |
Suppressed comments (2)
dockerutil/filewriter.go:82
- The deferred ContainerRemove uses the same ctx that can be canceled (the select below explicitly returns ctx.Err()). If ctx is canceled, this cleanup call will likely fail with "context canceled" and leave the helper container behind. Consider using a non-cancelable cleanup context (with a short timeout) for the remove step so the container is reliably reaped even on test timeouts/cancellation.
defer func() {
if err := w.cli.ContainerRemove(ctx, cc.ID, types.ContainerRemoveOptions{
Force: true,
}); err != nil {
w.log.Warn("Failed to remove file content container", zap.String("container_id", cc.ID), zap.Error(err))
}
dockerutil/volumeowner.go:81
- The deferred ContainerRemove uses the same ctx that can be canceled (the select below explicitly returns ctx.Err()). If ctx is canceled, this cleanup call will likely fail with "context canceled" and leave the helper container behind. Consider using a non-cancelable cleanup context (with a short timeout) for the remove step so the container is reliably reaped even on test timeouts/cancellation.
defer func() {
if err := opts.Client.ContainerRemove(ctx, cc.ID, types.ContainerRemoveOptions{
Force: true,
}); err != nil {
opts.Log.Warn("Failed to remove volume-owner container", zap.String("container_id", cc.ID), zap.Error(err))
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Three short-lived busybox helper containers in
dockerutilrun withHostConfig.AutoRemove: true:volumeowner.go— chowns a chain's data volumefilewriter.go— writes files (config/genesis) into the volumefileretriever.go— reads files back outTwo of them (
volumeowner,filewriter) start the container and then callContainerWait(..., WaitConditionNotRunning). Their command (achown) exits in microseconds. WithAutoRemoveon, the daemon removes the container the moment its process exits — and on some daemons that removal wins the race against the wait being registered, soContainerWaitreturnsNo such container. That failsSetVolumeOwner/ the file write, and with it any chain boot.Concretely this reproduces on Docker Desktop with Engine 28.3.0 (macOS) — boot panics first at
set volume owner, then (after fixing that helper) atoverwriting config/config.toml— while the same code passes on the older Engine used in CI. It's a timing difference, not a code difference.Root cause
This is a wait-ordering race, not a Docker version regression. The moby Go client documents that
ContainerWaitreturns its channels only after the wait request has been acknowledged by the daemon, specifically so callers can establish the wait before starting the container:These helpers do the opposite (start, then wait), so for a near-instant command the exit + AutoRemove can complete before the wait attaches.
AutoRemove(daemon-side removal on process exit) has existed unchanged since API v1.25 / Docker 1.13, so this is a latent bug that newer daemons' removal timing simply exposes — same race, provider-independent (cf. podman #25479).Fix
Drop
AutoRemovefrom all three helpers and let the existing deferredContainerRemove(..., Force: true)clean up the stopped container. This closes the race (the container persists until the wait observes its exit) while still guaranteeing removal.fileretrievernever starts its container (it onlyCopyFromContainers the created filesystem), so it was already race-free — changed only for uniformity.Verification
TestDchainBoots(dchain interchaintest) boots the 3-validator network to height 5 and PASSes on Docker Engine 28.3.0 / Docker Desktop (macOS), where it previously panicked atset volume owner.References
client/container_wait.go— documents the wait-before-start pattern the helpers violateAutoRemoveadded in API v1.25