apache/pulsar · error · java.lang.IllegalStateException
the PulsarTlsFactory passed to tlsFactory(...) has already b
Error message
the PulsarTlsFactory passed to tlsFactory(...) has already been adopted by a client built from this builder, or is claimed by a build still in progress. The client initializes that instance and closes it with itself, so it cannot be handed to a second client — closing either one would break TLS for the other. Call tlsFactory(...) with a fresh instance before building again.
What it means
This IllegalStateException is thrown by claimTlsFactoryOrReject during build(). A PulsarTlsFactory is a stateful instance with a lifecycle: the client initializes it exactly once and closes it with itself. Handing the same instance to a second client would mean double-initialize/double-close, leaving one client serving TLS from a closed factory. The builder therefore records every adopted instance by identity and refuses to hand the same one over twice.
Source
Thrown at pulsar-client-v5/src/main/java/org/apache/pulsar/client/impl/v5/PulsarClientBuilderV5.java:169
* testing and claiming are one operation. Checking with {@code contains} and adding afterwards left a
* window spanning the whole build: two concurrent {@code build()} calls carrying the same factory both
* found the set empty, both proceeded, and both adopted — {@code initialize} twice, {@code close} twice,
* and whichever client was closed second left serving TLS from a closed factory while still reporting
* itself open, which is the exact outcome this guard exists to prevent.
*
* <p>The claim is given back by {@link #releaseTlsFactoryUnlessSpent} when the build turns out not to
* have consumed the instance. A loser of the race is therefore rejected even in the case where the
* winner went on to fail before adopting, and would have left the instance re-usable had the two run in
* sequence. That is the safe direction: refusing a build the caller can retry, rather than handing one
* live factory to two owners.
*
* @return the instance claimed for this build, to be passed to {@link #releaseTlsFactoryUnlessSpent}
* whatever the outcome, or {@code null} when no factory is configured
*/
private PulsarTlsFactory claimTlsFactoryOrReject(ClientConfigurationData handingOver) {
PulsarTlsFactory adopting = handingOver.getTlsFactory();
if (adopting != null && !adoptedTlsFactories.add(adopting)) {
throw new IllegalStateException("the PulsarTlsFactory passed to tlsFactory(...) has already been "
+ "adopted by a client built from this builder, or is claimed by a build still in "
+ "progress. The client initializes that instance and closes it with itself, so it "
+ "cannot be handed to a second client — closing either one would break TLS for the "
+ "other. Call tlsFactory(...) with a fresh instance before building again.");
}
return adopting;
}
/**
* Give back a claim the build did not consume, so a build that failed before the framework took the
* instance leaves the builder able to retry with it. A claim that WAS consumed stays, and every consumed
* instance is remembered rather than just the last one, so cycling back to an earlier factory is caught
* too. That is a deliberate trade: the record holds a strong reference to each adopted factory, so a
* closed one is not collectable while the builder lives, and the bound is the caller's own history of
* adoptions rather than anything the builder controls. Forgetting instances instead would let a
* long-lived builder silently re-adopt a closed factory, which is the failure this guards against; the
* builder therefore retains one reference per adoption performed, which is bounded by the
* factories the caller created in the first place.View on GitHub (pinned to 820761864e)
Solutions
- Call tlsFactory(...) with a NEW PulsarTlsFactory instance before each build() (tlsFactory(null) throws, so there is no way to clear the slot).
- Build the client once per factory; if you need multiple clients, create one factory per client.
- Synchronize builder usage: never call build() concurrently on the same builder instance.
- If a build failed very early (e.g. missing serviceUrl) the claim is released automatically — just fix the config and rebuild without a new factory.
Example fix
// before PulsarTlsFactory f = PulsarTlsFactory.builder()...build(); var b = PulsarClient.builder().serviceUrl(url).tlsFactory(f); PulsarClient c1 = b.build(); PulsarClient c2 = b.build(); // IllegalStateException: factory already adopted // after PulsarClient c1 = PulsarClient.builder().serviceUrl(url).tlsFactory(newFactory()).build(); PulsarClient c2 = PulsarClient.builder().serviceUrl(url).tlsFactory(newFactory()).build();
Defensive patterns
Strategy: validation
Validate before calling
// Track adoption yourself before rebuilding:
if (factoryUsedByClient) {
factory = PulsarTlsFactory.builder()...build(); // fresh instance per client
}
builder.tlsFactory(factory);
PulsarClient c = builder.build();
factoryUsedByClient = true; Try / catch
try {
client = builder.build();
} catch (IllegalStateException e) {
if (e.getMessage().contains("PulsarTlsFactory")) {
builder.tlsFactory(newFactory()); // retry only with a fresh instance
client = builder.build();
} else throw e;
} Prevention
- One PulsarTlsFactory per client — create it inside the same scope as build().
- Never share a builder across threads for build().
- Don't keep a singleton factory in DI for multiple clients.
- Remember tlsFactory(null) throws: there is no way to clear the slot, so always supply a fresh instance for a retry after adoption.
When it happens
Trigger: Calling tlsFactory(factory) then build() more than once with the same factory instance (the factory was spent by the first successful or sufficiently-far-along build); or two threads calling build() concurrently on the same builder carrying the same factory — the loser of the race gets this even if the winner's build later failed before adoption.
Common situations: A retry loop that reuses one builder and one factory after a transient first-build failure that reached adoption; sharing a singleton PulsarTlsFactory across client instances; concurrent initialization of clients from one shared builder in application startup code.
Understand the failure class
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
Related errors
- Pulsar client has been closed, can not build LookupService w
- The replication cluster does not provide TLS encrypted servi
- Failed to acquire TLS material for purpose ${purpose}
- Consumer already closed
- Topic was terminated
AI-assisted analysis of apache/pulsar@820761864e (2026-09-06).
Data as JSON: /api/errors/05345a0551e6e2f4.
Report an issue: GitHub.