{"record":{"id":"05345a0551e6e2f4","repo":"apache/pulsar","slug":"the-pulsartlsfactory-passed-to-tlsfactory-has","errorCode":null,"errorMessage":"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.","messagePattern":"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\\.","errorType":"exception","errorClass":"java.lang.IllegalStateException","httpStatus":null,"severity":"error","filePath":"pulsar-client-v5/src/main/java/org/apache/pulsar/client/impl/v5/PulsarClientBuilderV5.java","lineNumber":169,"sourceCode":"     * testing and claiming are one operation. Checking with {@code contains} and adding afterwards left a\n     * window spanning the whole build: two concurrent {@code build()} calls carrying the same factory both\n     * found the set empty, both proceeded, and both adopted — {@code initialize} twice, {@code close} twice,\n     * and whichever client 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.\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 passed to tlsFactory(...) has already been \"\n                    + \"adopted by a client built from this builder, or is claimed by a build still in \"\n                    + \"progress. The client initializes that instance and closes it with itself, so it \"\n                    + \"cannot be handed to a second client — closing either one would break TLS for the \"\n                    + \"other. Call tlsFactory(...) with a 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 therefore retains one reference per adoption performed, which is bounded by the\n     * factories the caller created in the first place.","sourceCodeStart":151,"sourceCodeEnd":187,"githubUrl":"https://github.com/apache/pulsar/blob/820761864ed8e2a7d2e52dd9763ad2ae117c1395/pulsar-client-v5/src/main/java/org/apache/pulsar/client/impl/v5/PulsarClientBuilderV5.java#L151-L187","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nPulsarTlsFactory f = PulsarTlsFactory.builder()...build();\nvar b = PulsarClient.builder().serviceUrl(url).tlsFactory(f);\nPulsarClient c1 = b.build();\nPulsarClient c2 = b.build(); // IllegalStateException: factory already adopted\n// after\nPulsarClient c1 = PulsarClient.builder().serviceUrl(url).tlsFactory(newFactory()).build();\nPulsarClient c2 = PulsarClient.builder().serviceUrl(url).tlsFactory(newFactory()).build();","handlingStrategy":"validation","validationCode":"// Track adoption yourself before rebuilding:\nif (factoryUsedByClient) {\n    factory = PulsarTlsFactory.builder()...build(); // fresh instance per client\n}\nbuilder.tlsFactory(factory);\nPulsarClient c = builder.build();\nfactoryUsedByClient = true;","typeGuard":null,"tryCatchPattern":"try {\n    client = builder.build();\n} catch (IllegalStateException e) {\n    if (e.getMessage().contains(\"PulsarTlsFactory\")) {\n        builder.tlsFactory(newFactory()); // retry only with a fresh instance\n        client = builder.build();\n    } else throw e;\n}","preventionTips":["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."],"tags":["pulsar","tls","lifecycle","illegal-state","builder-reuse"],"backgroundTag":"tls-factory-already-adopted","analyzedSha":"820761864ed8e2a7d2e52dd9763ad2ae117c1395","analyzedAt":"2026-09-06T00:14:20.138Z","contentChangedAt":"2026-09-06T00:14:20.138Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}