{"record":{"id":"c8582a043a471797","repo":"apache/pulsar","slug":"the-pulsartlsfactory-on-this-admin-builder-s-confi","errorCode":null,"errorMessage":"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.","messagePattern":"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\\.","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"pulsar-client-admin/src/main/java/org/apache/pulsar/client/admin/internal/PulsarAdminBuilderImpl.java","lineNumber":135,"sourceCode":"     * found the set empty, both proceeded, and both adopted — {@code initialize} twice, {@code close} twice,\n     * and whichever admin was closed second left serving TLS from a closed factory while still reporting\n     * itself open, which is the exact outcome this guard exists to prevent. Synchronizing the set does not\n     * help; it makes each operation atomic, not the pair. This matters here above all because a\n     * clone-per-thread builder sharing one record is the pattern {@link #clone()} deliberately supports.\n     *\n     * <p>The claim is given back by {@link #releaseTlsFactoryUnlessSpent} when the build turns out not to\n     * have consumed the instance. A loser of the race is therefore rejected even in the case where the\n     * winner went on to fail before adopting, and would have left the instance re-usable had the two run in\n     * sequence. That is the safe direction: refusing a build the caller can retry, rather than handing one\n     * live factory to two owners.\n     *\n     * @return the instance claimed for this build, to be passed to {@link #releaseTlsFactoryUnlessSpent}\n     *         whatever the outcome, or {@code null} when no factory is configured\n     */\n    private PulsarTlsFactory claimTlsFactoryOrReject(ClientConfigurationData handingOver) {\n        PulsarTlsFactory adopting = handingOver.getTlsFactory();\n        if (adopting != null && !adoptedTlsFactories.add(adopting)) {\n            throw new IllegalStateException(\"the PulsarTlsFactory on this admin builder's configuration has \"\n                    + \"already been adopted by an admin built from it, or is claimed by a build still in \"\n                    + \"progress. The admin initializes that instance and closes it with itself, so it cannot \"\n                    + \"be handed to a second admin — closing either one would break TLS for the other. Set a \"\n                    + \"fresh instance before building again.\");\n        }\n        return adopting;\n    }\n\n    /**\n     * Give back a claim the build did not consume, so a build that failed before the framework took the\n     * instance leaves the builder able to retry with it. A claim that WAS consumed stays, and every consumed\n     * instance is remembered rather than just the last one, so cycling back to an earlier factory is caught\n     * too. That is a deliberate trade: the record holds a strong reference to each adopted factory, so a\n     * closed one is not collectable while the builder lives, and the bound is the caller's own history of\n     * adoptions rather than anything the builder controls. Forgetting instances instead would let a\n     * long-lived builder silently re-adopt a closed factory, which is the failure this guards against; the\n     * builder family therefore retains one reference per adoption performed, which is bounded by\n     * the factories the caller created in the first place.","sourceCodeStart":117,"sourceCodeEnd":153,"githubUrl":"https://github.com/apache/pulsar/blob/820761864ed8e2a7d2e52dd9763ad2ae117c1395/pulsar-client-admin/src/main/java/org/apache/pulsar/client/admin/internal/PulsarAdminBuilderImpl.java#L117-L153","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nPulsarAdminBuilder builder = PulsarAdmin.builder().serviceHttpUrl(url).setPulsarTlsFactory(factory);\nPulsarAdmin a1 = builder.create();\nPulsarAdmin a2 = builder.create(); // IllegalStateException\n// after\nPulsarAdmin a1 = PulsarAdmin.builder().serviceHttpUrl(url).setPulsarTlsFactory(newFactory()).create();\nPulsarAdmin a2 = PulsarAdmin.builder().serviceHttpUrl(url).setPulsarTlsFactory(newFactory()).create();","handlingStrategy":"validation","validationCode":"if (adminBuiltFromThisBuilder) {\n    throw new IllegalStateException(\"builder already used; create a new builder\");\n}","typeGuard":null,"tryCatchPattern":"try {\n    PulsarAdmin admin = builder.create();\n} catch (IllegalStateException e) {\n    if (e.getMessage().contains(\"PulsarTlsFactory\")) {\n        builder = newBuilderWithFreshTlsFactory();\n        admin = builder.create();\n    } else throw e;\n}","preventionTips":["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"],"tags":["tls","resource-ownership","illegal-state"],"backgroundTag":"tls-factory-reuse","analyzedSha":"820761864ed8e2a7d2e52dd9763ad2ae117c1395","analyzedAt":"2026-09-06T00:14:20.138Z","contentChangedAt":"2026-09-06T00:14:20.138Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}