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

  1. Call tlsFactory(...) with a NEW PulsarTlsFactory instance before each build() (tlsFactory(null) throws, so there is no way to clear the slot).
  2. Build the client once per factory; if you need multiple clients, create one factory per client.
  3. Synchronize builder usage: never call build() concurrently on the same builder instance.
  4. 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

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

Related errors


AI-assisted analysis of apache/pulsar@820761864e (2026-09-06). Data as JSON: /api/errors/05345a0551e6e2f4. Report an issue: GitHub.