Skip to content

Replace duct-tape with resilience4j-backed internal utilities - #11778

Open
austince wants to merge 1 commit into
testcontainers:mainfrom
austince:replace-ducttape-resilience4j
Open

Replace duct-tape with resilience4j-backed internal utilities#11778
austince wants to merge 1 commit into
testcontainers:mainfrom
austince:replace-ducttape-resilience4j

Conversation

@austince

@austinceaustince commented May 19, 2026

Copy link
Copy Markdown

Fixes#9227 by moving from duct-tape to resilience4j, as described in: #9241 (comment)

The two deps are:

shaded 'io.github.resilience4j:resilience4j-timelimiter:1.7.1'
shaded 'io.github.resilience4j:resilience4j-ratelimiter:1.7.1'

These are also EOL (last release of the 1.x line happened in 2021), but don't have a thread-leak. Moving to the non-EOL 2.x version would require upgrading to Java 17 at minimum (resilience4j/resilience4j#2414).

It moves a few utilities into TC, but on the whole does not add the rate limiting/etc. logic into the TC maintenance burden. For completeness, also explored an option where we add no new deps here, but don't fully copy ducttape as in #9241. It doesn't look awful, but more logic than is proposed here.

I have added a wrapper interface to not expose the shaded resilience4j types and keep the ~same compile-time interface that ducttape exposed (just need import changes), hoping to reduce upgrade churn for the standard paths but certainly introduces breaking changes wrt binary compatibility and anyone passing ducttape types directly.

Removes the dependency on org.rnorth.duct-tape:duct-tape:1.0.8 by
re-implementing the small subset of duct-tape used by Testcontainers
inside org.testcontainers.utility, backed by resilience4j modules per
maintainer preference in testcontainers#9241.
New runtime dependencies (shaded):
- io.github.resilience4j:resilience4j-timelimiter:1.7.1
- io.github.resilience4j:resilience4j-ratelimiter:1.7.1
Internal package additions (org.testcontainers.utility):
- RateLimiter (interface) + RateLimiterBuilder: thin facade with the
historical duct-tape method names doWhenReady / getWhenReady, backed
by a resilience4j RateLimiter adapter. The shaded resilience4j type
never leaks into the public API.
- Timeouts.getWithTimeout / doWithTimeout: per-call ExecutorService
with daemon worker thread, shutdownNow() in finally and resilience4j
TimeLimiter (cancelRunningFuture=true) — fixes the duct-tape
zombie-thread bug (testcontainers#9227).
- Unreliables.retryUntilSuccess / retryUntilTrue: retry loop wrapped
in Timeouts.getWithTimeout. Interrupt-induced exceptions
(resilience4j's AcquirePermissionCancelledException, etc.) are not
allowed to overwrite the genuine last failure so callers diagnose
the real cause (e.g. SSLHandshakeException).
- TimeoutException, RetryCountExceededException: unchecked, matching
duct-tape's semantics so existing callers compile unchanged. Message
"Retry limit hit with exception" is preserved verbatim for
GenericContainerTest's existing assertion.
Public API impact:
- AbstractWaitStrategy.getRateLimiter() and withRateLimiter() now take
org.testcontainers.utility.RateLimiter instead of
org.rnorth.ducttape.ratelimits.RateLimiter. Source-compat is preserved
for callers that go through AbstractWaitStrategy; only callers that
explicitly imported the duct-tape types break.
- Both methods are added to japicmp's methodExcludes with a comment
explaining the binary-breaking change.
Callers updated to use the new types: GenericContainer,
HttpWaitStrategy, ShellStrategy, DockerHealthcheckWaitStrategy,
WaitAllStrategy, StartupCheckStrategy, DockerClientProviderStrategy,
RyukResourceReaper, CassandraQueryWaitStrategy (both packages),
YugabyteDBY{CQL,SQL}WaitStrategy, and the corresponding tests.
Refs: testcontainers#9241
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: duct-tape:1.0.8 contains a Thread Leak

1 participant

@austince