Skip to content

executor: a starting builder no longer holds prepare, and no docker command fails because of it - #52

Merged
ismoilovdevml merged 4 commits into
mainfrom
fix/buildx-create-timeout
Oct 6, 2026
Merged

ismoilovdevml merged 4 commits into
mainfrom
fix/buildx-create-timeout

Conversation

@ismoilovdevml

Copy link
Copy Markdown
Owner

When a job's project builder was still booting, prepare spent 20 s in docker buildx create: buildx saves the instance, then probes the endpoint for --timeout (default 20 s), and the remote driver reports an unreachable endpoint as inactive without an error, so the probe always ran to the limit.

  • A booting builder gets docker buildx create --timeout 2s; any failure falls back to one unbounded create (buildx older than v0.32.0 behaves as before). A ready builder keeps today's command and its script marks the builder ready.
  • The docker wrapper now waits for every command it recognises as a build: docker build, buildx, image build, builder build, compose build and compose up/run/create --build, also after docker's global options. If the builder never answers, the build runs locally, as docker build already did.
  • Commands it does not recognise run without the builder only while it is starting and has not answered; once it is ready or has answered they use it. Before this change docker compose build against a builder that did not answer yet failed the job after about 40 s.

Measured in a Firecracker microVM (buildx v0.37.1, Compose v5.5.1): bounded create against an unreachable builder returns in 2.2–2.6 s (was 20.1 s); compose against an inactive builder fails after 40 s, and builds locally in 6 s once BUILDX_BUILDER is unset.

Tests cover the create paths (bounded, fallback, both failing, existing instance, ready) and the wrapper for booting and ready builders, with hermetic fakes; an end-to-end run against real docker and a real mTLS BuildKit passed for both states.

buildx create --driver remote saves the instance and then probes the
endpoint under its --timeout, 20 s by default. While the project's builder
is still booting nothing answers on its port, and the remote driver reports
it inactive without an error, so the create waited the full 20 s and then
succeeded anyway. Every job that found its builder booting started its
script 20 s late.

For a booting builder the script now runs the create with --timeout 2s.
If that create fails, the unbounded create runs once more: buildx before
v0.32.0 has no --timeout on create, and a builder that turns ready during
the probe can make the bounded create roll back. A create that still fails
is reported as before ("could not attach the builder") and the job builds
without the cache. Ready builders get the same create as before. The docker
wrapper, which waits for a starting builder on docker build, is unchanged.

The tests run the generated script under bash with fake docker, ip and ssh
binaries: bounded create succeeds, fails then the fallback succeeds (old
buildx, rolled-back create), both fail, and a ready builder's create
succeeding and failing.
The docker wrapper waited for a starting builder only for docker build
and docker buildx. With BUILDX_BUILDER set to a builder that does not
answer yet, docker compose build does not build locally: it fails after
about 40 s ("waiting for connection ... context deadline exceeded"). The
20 s create in prepare used to give the builder that much head start;
with the bounded create a compose build right after prepare fails the job.

The wrapper now also waits for docker compose build and for compose up,
run or create with --build. It skips compose's options that take a value
to find the command, so "compose --profile build up" does not wait. It
uses the same wait as docker build: up to BuilderWait, then it unsets
BUILDX_BUILDER and compose builds locally. A compose up that builds a
missing image without --build is not seen and does not wait; docs/jobs.md
says so, and that buildx older than v0.32.0 still waits about 20 s.

Tests:
- The wrapper the builder script installs runs under bash for docker
  build, buildx and compose commands: the builder answers, never answers
  (local build), was already found up or down, and commands that must not
  wait.
- The fake docker refuses a second create of an instance, like buildx,
  and a case covers an instance that already exists.
- The tests fail if the script uses a guest path they do not redirect.
The docker wrapper only protected the commands on its build list. Any
other command kept BUILDX_BUILDER on a builder that did not answer yet,
and a build it ran anyway failed after about 40 s instead of building
locally. The common case is docker compose up building a missing image on
a fresh VM; docker --debug build, docker --context x compose build,
docker image build and docker builder build were missed as well.

Until the builder has answered once, a command the wrapper does not see
as a build now runs with BUILDX_BUILDER unset, without probing or
waiting: such a build is local and keeps the job alive. Once a build has
found the builder up, every command uses it. Recognised builds wait as
before.

The build list also grows. The wrapper skips docker's global options
before the command, so docker --debug build and docker --context x
compose build are seen. image build and builder build are aliases of
buildx build, so they are builds too. Compose's hidden --workdir is
skipped as an option that takes a value.

Tests:
- New wrapper cases while the builder starts: compose up, push and run
  run locally at once, and image build, builder build and builds after
  docker options wait.
- After the builder answered, compose up uses it.
- The fake docker logs "local" only when BUILDX_BUILDER is unset.
- The attach test checks that the script is hermetic before Run starts.
Only the wrapper's probe wrote /run/firerunner-builder-ok, and only
builds the wrapper recognises run the probe. With a ready builder, a
docker compose up that builds a missing image without --build therefore
ran locally and cold, where before this branch it used the warm cache.

When the daemon reports the builder ready, the builder script now writes
/run/firerunner-builder-ready. A command the wrapper does not see as a
build keeps BUILDX_BUILDER if the builder is ready or has answered. It
runs without the builder while the builder starts, and always once it
was given up. Recognised builds still probe the builder first, so a ready
builder that went away since prepare still falls back to a local build.

Tests:
- Ready-state wrapper cases: compose up and run use the builder without
  a probe, compose up after the builder was given up is local, and a
  build probes, then is remote or local.
- The unit test checks that only a ready builder's script writes the
  marker.
@ismoilovdevml
ismoilovdevml merged commit 30f83da into main Oct 6, 2026
6 checks passed

This branch was successfully deployed

1 active deployment
github-pages — 30f83da8 Deployed Oct 6, 2026 by ismoilovdevml via deploy #45
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