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

  1. Create a fresh builder (or fresh PulsarTlsFactory via ClientBuilder-style configuration) for each admin instance.
  2. Build only once per builder; keep the returned PulsarAdmin and reuse it instead of rebuilding.
  3. If the previous admin was closed, set a brand-new PulsarTlsFactory on the configuration before building again.
  4. 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

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

Related errors


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