apache/pulsar · error · IllegalArgumentException

Both externalExecutorProvider and internalExecutorProvider m

Error message

Both externalExecutorProvider and internalExecutorProvider must be specified or unspecified.

What it means

The internal PulsarClientImpl constructor accepts paired executor providers: externalExecutorProvider (client API surface) and internalExecutorProvider (internal bookkeeping). If exactly one of them is null the wiring is incomplete, so the constructor throws IllegalArgumentException to prevent a half-initialized client.

Source

Thrown at pulsar-client/src/main/java/org/apache/pulsar/client/impl/PulsarClientImpl.java:272

    @Builder(builderClassName = "PulsarClientImplBuilder")
    PulsarClientImpl(ClientConfigurationData conf, EventLoopGroup eventLoopGroup, ConnectionPool connectionPool,
                     Timer timer, ExecutorProvider externalExecutorProvider,
                     ExecutorProvider internalExecutorProvider,
                     ScheduledExecutorProvider scheduledExecutorProvider,
                     ExecutorProvider lookupExecutorProvider,
                     DnsResolverGroupImpl dnsResolverGroup,
                     MemoryLimitController memoryLimitController) throws PulsarClientException {
        EventLoopGroup eventLoopGroupReference = null;
        ConnectionPool connectionPoolReference = null;
        try {
            if (conf == null || isBlank(conf.getServiceUrl())) {
                throw new PulsarClientException.InvalidConfigurationException("Invalid client configuration");
            }
            this.conf = conf;
            this.createdEventLoopGroup = eventLoopGroup == null;
            this.createdCnxPool = connectionPool == null;
            if ((externalExecutorProvider == null) != (internalExecutorProvider == null)) {
                throw new IllegalArgumentException(
                        "Both externalExecutorProvider and internalExecutorProvider must be specified or unspecified.");
            }
            this.createdExecutorProviders = externalExecutorProvider == null;
            this.createdScheduledProviders = scheduledExecutorProvider == null;
            this.createdLookupProviders = lookupExecutorProvider == null;
            eventLoopGroupReference = eventLoopGroup != null ? eventLoopGroup :
                    PulsarClientResourcesConfigurer.createEventLoopGroup(conf);
            this.eventLoopGroup = eventLoopGroupReference;
            this.instrumentProvider = new InstrumentProvider(conf.getOpenTelemetry());
            clientClock = conf.getClock();
            this.scheduledExecutorProvider = scheduledExecutorProvider != null ? scheduledExecutorProvider :
                    PulsarClientResourcesConfigurer.createScheduledExecutorProvider(conf);
            // PIP-478: resolve the client-side TLS SPI factory (new path) before the connection
            // pool and HTTP lookup are created — both read conf.getTlsFactory() to branch onto it.
            setupClientTlsFactory();
            if (connectionPool != null) {
                connectionPoolReference = connectionPool;
                dnsResolverGroupLocalInstance = null;

View on GitHub (pinned to 820761864e)

Solutions

  1. Pass both externalExecutorProvider and internalExecutorProvider, or pass null for both so the client creates its own providers.
  2. Update the calling code to construct the two providers together (typically the internal one derives from the external one).
  3. Prefer the public PulsarClient.builder() API, which never requires these arguments.

Example fix

// before
new PulsarClientImpl(conf, eventLoopGroup, externalExecutorProvider, null, ...);
// after
ExecutorProvider internal = new ExecutorProvider(numThreads, externalExecutorProvider.getScheduledExecutorProvider());
new PulsarClientImpl(conf, eventLoopGroup, externalExecutorProvider, internal, ...);
Defensive patterns

Strategy: validation

Validate before calling

if ((externalExecutorProvider == null) != (internalExecutorProvider == null)) {
    throw new IllegalArgumentException("Pass both executor providers or neither");
}

Try / catch

try {
    client = new PulsarClientImpl(conf, eventLoopGroup, ext, internal, ...);
} catch (IllegalArgumentException e) {
    // construct providers as a pair and retry once
    internal = new ExecutorProvider(numThreads, ext);
    client = new PulsarClientImpl(conf, eventLoopGroup, ext, internal, ...);
}

Prevention

When it happens

Trigger: Calling the package-private/extended PulsarClientImpl constructor passing an externalExecutorProvider without an internalExecutorProvider (or vice versa).

Common situations: Embedded/advanced integrations that build the client with custom executors (e.g. tests, framework adapters) updating one argument but not the other after a signature change.

Related errors


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