Skip to content

Apply the startup timeout as one deadline from container start - #1584

Draft
cristianrgreco wants to merge 7 commits into
claude/deadline-aware-poll-helper-e07af6from
claude/startup-timeout-hooks
Draft

cristianrgreco wants to merge 7 commits into
claude/deadline-aware-poll-helper-e07af6from
claude/startup-timeout-hooks

Conversation

@cristianrgreco

@cristianrgreco cristianrgreco commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

withStartupTimeout(ms) reads as "the container must be ready within this long", but it was applied per wait. The wait strategy got the full timeout, counted from when it began, and modules that wait again in containerStarted (Kafka, Redpanda, Couchbase) started another one. A Kafka container with its 180 second default could run for close to 360 seconds before failing.

A startup timeout set on the container is now one deadline, counted from when the container is started. It is an AbortSignal created once at the top and passed down, the way a context is passed in Go. Nothing computes how much time is left. Part of #1582.

Stacked on #1594, which makes every wait a signal-aware poll. The base of this PR is that branch.

  • Core: GenericContainer creates the signal just before it starts the container. restart() creates one once Docker has restarted the container. DockerComposeEnvironment.up() creates one for all of its containers once docker compose up returns.
  • Waits: waitForContainer and WaitStrategy.waitUntilReady take the signal as an optional last argument, and poll takes it as signal. A wait that is still running when the signal aborts rejects with its own timeout error, so the error messages stay the same.
  • Callbacks: containerStarting and containerStarted receive the signal as an optional last argument.
  • Modules: Kafka and Redpanda pass it to the second wait they run in containerStarted. Couchbase passes it to each of its waits and no longer works out the time left for each one.

This follows testcontainers-go, where the caller's context is the ceiling and each wait strategy derives its timeout from it: wait/host_port.go, lifecycle.go.

Behaviour changes

Only containers and Compose environments with a startup timeout set on them are affected. That includes the modules that set a default in their constructor.

  • A start that only succeeded because each wait got the full timeout now fails at the deadline.
  • Time spent before the wait strategy begins counts: the port-binding wait and containerStarting.
  • A wait strategy inside Wait.forAll with its own, longer timeout no longer outlasts the container's or the Compose environment's startup timeout.
  • Couchbase: each wait in containerStarted has the startup timeout (60 seconds when none is set) as its own timeout, and its error reports that value.

Unchanged:

  • A timeout set only on a wait strategy, and containers with no startup timeout.
  • The port-binding wait keeps its own 30 second limit (Increase timeout for waiting for host port bindings to 30 seconds #1494) and is not passed the signal. Its time counts towards the deadline, but the deadline does not cut it short. Cutting it would make withStartupTimeout(0) fail in the port-binding wait instead of in the wait strategy, which core tests rely on.
  • Error text. A wait reports its own configured timeout, also when the signal ended it sooner.
  • Pulling the image is not counted.

Left for later, from #1582:

  • As with a context, code that is not passed the signal is not limited. That includes a subclass that overrides a module's containerStarted and calls super without it.
  • The error does not yet say which phase was waiting or what the total budget was.

Verification

  • npm run format, npm run lint, npm run knip and npm run check-compiles are clean.
  • New tests: three for poll in retry.test.ts, five in generic-container-startup-timeout.test.ts and one in docker-compose-environment.test.ts.
  • Red-green: with poll ignoring the given signal and the startup signal never aborting, seven of them fail.
    • poll: two time out after 5000ms and one fails with promise resolved "'result'" instead of rejecting.
    • Start: expected [ false ] to deeply equal [ true ] for the signal being started before the wait strategy, and expected 63702 to be less than 60000 for the wait in containerStarted.
    • Restart and Compose: expected 60216 to be less than 60000 and expected 65091 to be less than 60000.
    • All pass with this change.
  • common, docker-compose-environment, generic-container-restart.test.ts and the new test file run together: 72 tests pass.
  • wait-strategies: 77 tests pass.
  • Module suites, each run on its own: Couchbase 11, SQL Server 6, Redpanda 5 and Kafka 17 tests pass.
  • The whole generic-container directory run together: 106 of 109 pass, 1 skipped. should restart and should restart persisting state fail with fetch failed against the mapped port after the restart. They pass in the 72-test run above, and should restart fails the same way on main when that directory is run together.
  • Publish dry run (npm-publish.yml dispatched on this branch with v12.2.2): passed.

Not breaking

  • The API change is additive: an optional last argument on WaitStrategy.waitUntilReady, waitForContainer and the two protected callbacks, and a signal option on poll. Custom wait strategies and callback overrides that ignore it compile and behave as before.
  • The behaviour changes above are the purpose of the PR. They apply only when a startup timeout is set, and they can only make a start that exceeds it fail sooner.

@cristianrgreco cristianrgreco added enhancement New feature or request minor Backward compatible functionality labels Oct 12, 2026
@cristianrgreco
cristianrgreco marked this pull request as draft October 13, 2026 09:15
@cristianrgreco cristianrgreco changed the title Pass the remaining startup timeout to the containerStarted hook Apply the startup timeout as one deadline from container start Oct 13, 2026
@cristianrgreco
cristianrgreco changed the base branch from main to claude/deadline-aware-poll-helper-e07af6 October 13, 2026 16:27

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request minor Backward compatible functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant