Repository navigation
Add a deadline-aware poll helper and deprecate IntervalRetry - #1594
Draft
cristianrgreco wants to merge 4 commits into
Draft
cristianrgreco wants to merge 4 commits into
cristianrgreco wants to merge 4 commits into
Conversation
Replace the IntervalRetry class hierarchy with one exported function,
poll(fn, { until, intervalMs, timeoutMs, timeoutMessage, retryOnError }).
It rejects at the deadline even when an attempt is still in flight, passes
fn an AbortSignal that aborts at the deadline, and attaches the last
attempt's error to the timeout error as its cause.
Migrate every IntervalRetry call site in core and in the Couchbase and
MongoDB modules. IntervalRetry and Retry stay exported with their original
behaviour, marked deprecated.
Thread the signal through ContainerClient.exec and ContainerClient.inspect
so wait strategies cancel their in-flight Docker calls at the startup
timeout, and expose it as a signal option on StartedTestContainer.exec.
This was referenced Oct 13, 2026
poll is exported so modules can import it, not as a user-facing feature.
This branch has not been deployed
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.
Summary
IntervalRetryonly checked its timeout between attempts, so a hung attempt (for examplecontainer.execin the shell wait strategy) blocked past the startup timeout. A throw from an attempt escaped the loop, so callers hand-rolledtry/catchreturningundefinedand the timeout error never carried the last failure.This replaces the
Retry/AbstractRetry/Clockscaffolding with one exported function:fnreceives{ attempt, signal }.signalaborts at the deadline.causeis the last attempt's error, if that attempt threw.fnis retried by default.retryOnError: falserethrows it immediately.It is exported because modules can only import core through the
testcontainersentry point. It is a helper for modules, so it has no page indocs/. EveryIntervalRetrycall site in core, Couchbase and the MongoDB Atlas Local test now uses it.IntervalRetryandRetrystay exported with their original loop, marked@deprecated.The signal is threaded through the Docker calls the waits make, so nothing keeps running on our side after a timeout:
ContainerClient.exectakessignalin its options and destroys the exec stream on abort.ContainerClient.inspecttakes an optional{ signal }.StartedTestContainer.exec(cmd, { signal })is supported and documented.StartupCheckStrategy.checkStartupStateis passed the signal as an optional third argument.abortOnContainerExitinspect all pass it on.Other implementations this follows:
Unreliables.retryUntilSuccess.WaitStrategy.WaitUntilAsync.Execand returns an exec error immediately:wait/exec.go.Behaviour changes
retryOnError: false(health check, host port, shell, startup check, ports-bound,mcstat) so a Docker error still fails fast. Measured: a container that exits after 2s fails the port or shell wait in about 3s with the 409 "is not running" error, before and after.untilpropagates.Wait.forHttp(...).forResponsePredicatestill fails immediately when the predicate throws.readTimeoutMs. The in-flight request is cancelled at the deadline and the timeout error carries a cause.waitForContaineralready logs the same message with the container id. Abortedexecandinspectcalls no longer log "Failed to ...".Errorwas ignored. Measured locally: index ready in about 0.3s and the search result in about 1s, against 5s limits.setTimeoutwould otherwise fire after 1ms.What still runs after a timeout
exec(["sleep", "30"], { signal })aborted after 1s:execrejected at 1008ms,docker topstill listedsleep 30immediately after and 5s after, and it was gone at 32s. For wait strategies,waitForContainerstops and removes the container on failure, which ends it. For a user's ownexec, it runs until it exits or the container stops. This is documented.fetchcalls. The Couchbase migration is mechanical (same intervals, timeouts and messages) and does not pass the signal todoHttpRequest. The KV readiness wait does pass it tocontainer.exec.checkStartupStatethat ignores the signal.Overlap with other PRs
Merged since this was opened, and resolved in the merges of
main(7cc2a64, 9d68601):waitUntilKvAcceptsCredentialsnow usespolland passes the signal tocontainer.exec.streamToStringexport fromcommon/index.ts.createExecOptions,waitForExecStreamToEndandsendRequest, and the extractedwaitUntilKeyspaceIsPresentandwaitUntilPrimaryIndexIsOnlineusepollwith the remaining startup budget.Still open:
inspect-container-util-ports-exposed.ts.http-wait-strategy.ts.docs/features/containers.md.Verification
npm run format,npm run lint,npm run knip,npm run check-compilesandmkdocs build --strictpass. The first four were rerun after mergingmain.Red-green, each test run before and after the change it covers:
pollrejects when an attempt never resolves (old loop under the new signature)inspectContainerUntilPortsExposedwith an inspect that never resolves (untouchedmain)Wait.forSuccessfulCommand("sleep 30")with a 1000ms startup timeout (pre-change strategy)pollwithtimeoutMs: Infinity(without the clamp)Timed out after Infinitymsclient.execrejects and destroys the stream on abortclient.execcreates no exec for an already-aborted signalclient.inspectrejects on abortcontainer.exec(["sleep", "30"], { signal })against DockerTypeError, no signal passedTypeError/ no signal receivedcheckStartupStatereceives a signal that aborts at the timeoutexpected false to be trueTargeted suites, run locally against Docker before merging
main:generic-container-restart.test.tsfails intermittently withfetch failed/ECONNRESETwhen run in this set. It fails the same way on untouchedmainin 2 of 3 runs of the same set, and passes alone 3 of 3 on this branch.main. The most common failure on both isCould not create primary index ... rebalance in progress(7 of 19 runs here, 4 of 9 onmain). When the Docker VM (3.8 GB) was overloaded, this branch failed withURL /pools not accessible after 60000ms(4 runs) or the 30s ports-bound timeout (3 runs), whilemainfailed withCould not perform request against couchbase HTTP endpoint(4 runs). I could not reproduce the ports-bound timeout under instrumentation in 6 further runs. A local-server comparison of the HTTP wait (connection resets, hangs, 503s, then 200) gave identical request counts and timings on both sides.After merging
main(7cc2a64), which brings in #1514:generic-container.test.tsand Couchbase: 17 files, 145 passed, 1 skipped.After the second merge of
main(9d68601), format, lint, knip and check-compiles pass, and one run of the retry, container client, ports-bound,generic-container.test.ts, wait strategy, reaper, Couchbase and MongoDB Atlas Local tests gave 21 files, 186 passed, 1 skipped.Publish dry run for
v12.2.1, succeeded on 6596e1d (before the merge ofmain): https://github.com/testcontainers/testcontainers-node/actions/runs/38295782401Not breaking
IntervalRetryandRetrykeep their signatures and loop. Their protectedclock,hasTimedOutandwaitmembers are gone; they were only reachable by subclassing.poll,PollOptions,ExecOptions.signal, theinspectoptions and the thirdcheckStartupStateargument are additive.