Skip to content

feat: Wait for the test container up to a configurable startup timeout, replace one that does not become reachable, and remove one whose start fails. - #425

Merged
goloroden merged 7 commits into
mainfrom
feat/container-start-contract
Oct 19, 2026
Merged

goloroden merged 7 commits into
mainfrom
feat/container-start-contract

Conversation

@goloroden

@goloroden goloroden commented Oct 19, 2026 •

Copy link
Copy Markdown
Member

Implements the shared start contract for test containers on top of testcontainers' GenericContainer.

  • Readiness: each attempt follows its container by label from its creation on, reads the mapped port on every poll, and pings GET /api/v1/ping until it answers 200. testcontainers waits for that through its wait strategy.
  • Time: each attempt gets the startup timeout, from creating the container on; pulling the image and starting Ryuk do not count. withStartupTimeout(timeout) takes milliseconds, like the client's stallTimeout, and defaults to 30 seconds.
  • Replacement: a running container that does not become reachable in time is replaced, up to 3 containers. This includes testcontainers giving up on the port, after its fixed 10 seconds or with No host port found for host IP. An exit fails at once with the last 20 lines of the log, and a Docker error fails at once with that error as cause.
  • Cleanup: every attempt carries the label io.eventsourcingdb.container-start=<random id>, set through withLabels, by which its containers are removed once it fails, also when the time runs out while Docker is still creating them. Ryuk works as before.

Tests in src/container/startContainer.test.ts run against real Docker: unreachable but running, creation outlasting the timeout, exit at once, missing image, Docker failing after creating the container, a port that testcontainers cannot find, and a normal start. Each checks via Docker events how many containers were created and that none is left. src/container/isReachable.test.ts checks that only the status 200 counts.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QykegdHe7vhHMBN3kWxgfH

…t, replace one that does not become reachable, and remove one whose start fails.

The container now starts EventSourcingDB the way all client SDKs do. Each
attempt gets the startup timeout, 30 seconds by default and configurable
with withStartupTimeout, from creating the container until a ping of
/api/v1/ping answers 200, reading the mapped port anew on every poll. A
running container that does not become reachable in time is replaced, up
to 3 containers. A container that exits fails at once with the last 20
lines of its output, and so does a Docker error, with that error as cause.
Every attempt carries the label io.eventsourcingdb.container-start, by
which its containers are removed once it fails, also while Docker is still
creating them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QykegdHe7vhHMBN3kWxgfH
@goloroden
goloroden requested a review from a team as a code owner October 19, 2026 09:31
@goloroden goloroden self-assigned this Oct 19, 2026
goloroden and others added 6 commits October 19, 2026 11:49
…gain.

testcontainers creates and starts the container again and copies the
signing key into it, so the own create and start, the tar writer, and the
attached output are gone. Each attempt still carries its own
io.eventsourcingdb.container-start label, now set through withLabels, and
follows the container by that label from its creation on: it reads the
mapped port on every poll, pings /api/v1/ping until it answers 200, which
testcontainers waits for through its wait strategy, and fails at once with
the last 20 lines of the log, read through testcontainers, if the container
exits. If testcontainers gives up on the port of a running container, after
10 seconds or since it cannot find the port on the host, the attempt counts
as one whose database did not become reachable, and another container takes
its place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QykegdHe7vhHMBN3kWxgfH
…ound the wait for a pending start.

A failed removal no longer replaces the error of the attempt, nor stops the
remaining attempts. It is reported on a line of its own after the message.
The wait for a start that is still pending after the time ran out lasts 10
seconds at most, after which the containers of the attempt are removed by
their label nonetheless. An empty log tail reads as "no log available", since
testcontainers does not pass on errors of Docker while reading the log. The
integration tests get 180 seconds, so that 3 attempts of 30 seconds fit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QykegdHe7vhHMBN3kWxgfH
…ock, the wording of the port errors, and the default timeout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QykegdHe7vhHMBN3kWxgfH
…ed attempt has ended.

Once the wait for a pending start has run out, the attempt goes on without
waiting further, and removes the container of the attempt as soon as the
start ends.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QykegdHe7vhHMBN3kWxgfH
…e 180 seconds only to the hooks that start containers.

The other hooks and the tests keep 60 seconds. The pull test waits 6 seconds
against a startup timeout of 5 seconds, and the tests label their containers
with a label of their own SDK.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QykegdHe7vhHMBN3kWxgfH
@goloroden
goloroden merged commit b5c9812 into main Oct 19, 2026
3 checks passed
@goloroden
goloroden deleted the feat/container-start-contract branch October 19, 2026 10:57
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