You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pulsar.apache.org by lh...@apache.org on 2026/08/12 21:04:56 UTC

(pulsar) 03/05: [fix][client] PIP-478: give a bridged v4 plugin an executor when no client owns it

This is an automated email from the ASF dual-hosted git repository.

lhotari pushed a commit to branch lh-pip-478-v5-native-auth-v2
in repository https://gitbox.apache.org/repos/asf/pulsar.git

commit 3ea0bdbe1a56c8a121f07b2b3671571f93fac9be
Author: Lari Hotari <lh...@apache.org>
AuthorDate: Wed Aug 12 21:17:40 2026 +0300

    [fix][client] PIP-478: give a bridged v4 plugin an executor when no client owns it
    
    Driving the v5 model natively means every legacy v4 plugin now goes through
    LegacyV4AuthenticationAdapter, which by design refuses to run a v4 call inline — the
    caller thread is a Netty event loop, and running credential I/O there is the stall
    PIP-478 exists to remove. It off-loads to the executor its init context supplies.
    
    Not every connection is opened by a PulsarClient, though. The proxy builds broker
    connections straight from a ClientConfigurationData, and so do embedders and tests, so
    no client-owned executor is bound and the context supplied none: the adapter then failed
    every call with "used before initializeAsync() completed". That refuses connections v4
    made fine (ProxyAuthenticationTest, ProxySaslAuthenticationTest), and it only surfaced on
    the proxy because the built-in bodies never off-load.
    
    Supply a shared blocking executor for those callers instead. Running inline was the other
    option and is the wrong one: on that path the caller thread is an event loop too.
    The pool costs nothing when unused — no core threads, daemon threads retiring after a
    minute idle — and is created on first use.
    
    Verified by mutation: reverting this fails ProxySaslAuthenticationTest.testAuthentication
    with the same IllegalStateException CI reported.
    
    Assisted-by: Claude Code (Opus 5)
---
 .../pulsar/client/impl/auth/v5/V5AuthContexts.java | 39 +++++++++++++++++++---
 1 file changed, 35 insertions(+), 4 deletions(-)

diff --git a/pulsar-client/src/main/java/org/apache/pulsar/client/impl/auth/v5/V5AuthContexts.java b/pulsar-client/src/main/java/org/apache/pulsar/client/impl/auth/v5/V5AuthContexts.java
index 1e3088768332..4653267af74d 100644
--- a/pulsar-client/src/main/java/org/apache/pulsar/client/impl/auth/v5/V5AuthContexts.java
+++ b/pulsar-client/src/main/java/org/apache/pulsar/client/impl/auth/v5/V5AuthContexts.java
@@ -25,6 +25,9 @@ import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.Executor;
 import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
 import java.util.function.Supplier;
 import org.apache.pulsar.client.api.v5.auth.AuthenticationCallContext;
 import org.apache.pulsar.client.api.v5.auth.AuthenticationInitContext;
@@ -41,9 +44,10 @@ import org.apache.pulsar.http.PulsarHttpClientFactory;
  * HTTP client factory, client instance id) are late-bound into the init context via
  * {@link ClientAuthenticationServices}
  * — so a credential-fetching body (OAuth2, Athenz) can off-load its blocking fetch onto the blocking
- * executor instead of running it on the Netty event loop. When no services are bound (a plugin used
- * outside a client), the context degrades to {@code null} services and the body falls back to inline
- * computation.
+ * executor instead of running it on the Netty event loop. When no services are bound (a connection opened
+ * outside a {@code PulsarClient} — the proxy's broker connections, embedders, tests), the context reports
+ * no HTTP client factory or scheduler, but still supplies a shared blocking executor: the offload
+ * discipline holds on every path, since the caller thread there is a Netty event loop too.
  */
 public final class V5AuthContexts {
 
@@ -103,6 +107,33 @@ public final class V5AuthContexts {
         }
     }
 
+    /**
+     * The blocking executor used when no client services are bound.
+     *
+     * <p>Not every connection is opened by a {@code PulsarClient}: the proxy builds broker connections
+     * straight from a configuration, and so do embedders and tests. Those callers have no client-owned
+     * executor to lend, but a bridged v4 plugin still must not run its credential call on the caller
+     * thread — which on that path is a Netty event loop. Falling back to running inline would reintroduce
+     * exactly the stall PIP-478 exists to remove, and refusing to run at all would break connections v4
+     * made fine, so the library keeps one shared pool for them.
+     *
+     * <p>It costs nothing when unused: no core threads, daemon threads that retire after a minute of idle.
+     * Held in a holder class so it is created on first use rather than on class load.
+     */
+    private static final class SharedBlockingExecutor {
+        static final Executor INSTANCE = create();
+
+        private static Executor create() {
+            ThreadPoolExecutor executor = new ThreadPoolExecutor(0, 8, 60L, TimeUnit.SECONDS,
+                    new SynchronousQueue<>(), runnable -> {
+                        Thread thread = new Thread(runnable, "pulsar-auth-blocking-shared");
+                        thread.setDaemon(true);
+                        return thread;
+                    });
+            return executor;
+        }
+    }
+
     private static final class InitContext implements AuthenticationInitContext {
         private final String clientInstanceId;
 
@@ -122,7 +153,7 @@ public final class V5AuthContexts {
 
         @Override
         public Executor blockingExecutor() {
-            return null;
+            return SharedBlockingExecutor.INSTANCE;
         }
 
         @Override