apache/pulsar · error · IllegalStateException
the PulsarTlsFactory on this admin builder's configuration h
Error message
the PulsarTlsFactory on this admin builder's configuration has already been adopted by an admin built from it, or is claimed by a build still in progress. The admin initializes that instance and closes it with itself, so it cannot be handed to a second admin — closing either one would break TLS for the other. Set a fresh instance before building again.
What it means
IllegalStateException thrown when a PulsarTlsFactory configured on an admin builder's ClientConfigurationData is claimed by a second build. The admin adopts and initializes the factory and closes it when the admin closes, so sharing it between two admins would let closing one break the other's TLS. Each builder instance tracks adopted factories and rejects reuse.
Source
Thrown at pulsar-client-admin/src/main/java/org/apache/pulsar/client/admin/internal/PulsarAdminBuilderImpl.java:135
* found the set empty, both proceeded, and both adopted — {@code initialize} twice, {@code close} twice,
* and whichever admin 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. Synchronizing the set does not
* help; it makes each operation atomic, not the pair. This matters here above all because a
* clone-per-thread builder sharing one record is the pattern {@link #clone()} deliberately supports.
*
* <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 on this admin builder's configuration has "
+ "already been adopted by an admin built from it, or is claimed by a build still in "
+ "progress. The admin initializes that instance and closes it with itself, so it cannot "
+ "be handed to a second admin — closing either one would break TLS for the other. Set 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 family 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
- Create a fresh builder (or fresh PulsarTlsFactory via ClientBuilder-style configuration) for each admin instance.
- Build only once per builder; keep the returned PulsarAdmin and reuse it instead of rebuilding.
- If the previous admin was closed, set a brand-new PulsarTlsFactory on the configuration before building again.
- If you only need shared TLS settings (not the factory object), copy configuration fields rather than passing the same config instance.
Example fix
// before PulsarAdminBuilder builder = PulsarAdmin.builder().serviceHttpUrl(url).setPulsarTlsFactory(factory); PulsarAdmin a1 = builder.create(); PulsarAdmin a2 = builder.create(); // IllegalStateException // after PulsarAdmin a1 = PulsarAdmin.builder().serviceHttpUrl(url).setPulsarTlsFactory(newFactory()).create(); PulsarAdmin a2 = PulsarAdmin.builder().serviceHttpUrl(url).setPulsarTlsFactory(newFactory()).create();
Defensive patterns
Strategy: validation
Validate before calling
if (adminBuiltFromThisBuilder) {
throw new IllegalStateException("builder already used; create a new builder");
} Try / catch
try {
PulsarAdmin admin = builder.create();
} catch (IllegalStateException e) {
if (e.getMessage().contains("PulsarTlsFactory")) {
builder = newBuilderWithFreshTlsFactory();
admin = builder.create();
} else throw e;
} Prevention
- Treat builders as single-use: one build() per builder
- Never share ClientConfigurationData instances across builds
- Centralize admin creation in one factory method that always creates a new PulsarTlsFactory
- Close each PulsarAdmin exactly once and never rebuild from a closed admin's config
When it happens
Trigger: Building two PulsarAdmin instances from the same builder (or the same ClientConfigurationData carrying a PulsarTlsFactory) without replacing the factory between builds; also calling build() twice on one builder after a factory was already adopted.
Common situations: A helper method returning a shared builder that callers invoke twice; retry loops that re-call build() with the same builder after a failure; code that constructs one admin, closes it, then builds another from the same configuration object.
Understand the failure class
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
Related errors
- the PulsarTlsFactory passed to tlsFactory(...) has already b
- The replication cluster does not provide TLS encrypted servi
- No ${scheme} URL configured for broker ${brokerId}
- Transaction pending ack replay error with illegal state :
- Issuer URL does not use https, but must:
AI-assisted analysis of apache/pulsar@820761864e (2026-09-06).
Data as JSON: /api/errors/c8582a043a471797.
Report an issue: GitHub.