apache/pulsar · error · IllegalStateException

A future has already been attached to this instance.

Error message

A future has already been attached to this instance.

What it means

Thrown by CompletableFutureCancellationHandler.attachToFuture when a future is attached to a handler instance that already has one. Each cancellation handler is single-use by design: it tracks exactly one future's cancellation/timeout, so reusing it across futures would corrupt its cancellation semantics.

Source

Thrown at pulsar-common/src/main/java/org/apache/pulsar/common/util/CompletableFutureCancellationHandler.java:76

     *
     * @param <T> the result type of the future
     * @return a new future instance
     */
    public <T> CompletableFuture<T> createFuture() {
        CompletableFuture<T> future = new CompletableFuture<>();
        attachToFuture(future);
        return future;
    }

    /**
     * Attaches the cancellation handler to handle cancels
     * and timeouts. A cancellation handler instance can be used only once.
     *
     * @param future the future to attach the handler to
     */
    public synchronized void attachToFuture(CompletableFuture<?> future) {
        if (attached) {
            throw new IllegalStateException("A future has already been attached to this instance.");
        }
        attached = true;
        future.whenComplete(whenCompleteFunction());
    }

    /**
     * Set the action to run when the future gets cancelled or timeouts.
     * The cancellation or timeout might be originating from any "upstream" future.
     * The implementation ensures that the cancel action gets called once.
     * Handles possible race conditions that might happen when the future gets cancelled
     * before the cancel action is set to this handler. In this case, the
     * cancel action gets called when the action is set.
     *
     * @param cancelAction the action to run when the the future gets cancelled or timeouts
     */
    public void setCancelAction(Runnable cancelAction) {
        if (this.cancelAction != null || cancelHandled.get()) {
            throw new IllegalStateException("cancelAction can only be set once.");

View on GitHub (pinned to 820761864e)

Solutions

  1. Create a new CompletableFutureCancellationHandler per future/operation instead of reusing the instance
  2. Reset the owning object so each request constructs a fresh handler (typical consumer pattern: new handler in internalReceiveAsync)
  3. Audit code paths named in callers (readNextAsync, createFuture, whenCancelledOrTimedOut) for double-attach on retry paths
  4. Guard application code so the handler is a local variable, not a shared field

Example fix

// before
private final CompletableFutureCancellationHandler handler = new CompletableFutureCancellationHandler();
// reused across reads -> second attach throws
// after
CompletableFuture<T> fut = new CompletableFuture<>();
CompletableFutureCancellationHandler handler = new CompletableFutureCancellationHandler(); // fresh per operation
handler.attachToFuture(fut);
Defensive patterns

Strategy: try-catch

Try / catch

try {
    handler.attachToFuture(future);
} catch (IllegalStateException e) {
    throw new IllegalStateException("handler reused across futures — allocate a new CompletableFutureCancellationHandler per operation", e);
}

Prevention

When it happens

Trigger: Calling attachToFuture twice on the same handler instance — e.g. a consumer receive loop reusing a cached handler for readNextAsync across multiple calls, or createFuture/whenCancelledOrTimedOut invoked again after an initial attach.

Common situations: Caching a handler in a field and re-attaching on every retry/request; sharing one handler across concurrent reads; a refactor moving attachToFuture into code that can run twice for the same object instance (e.g. re-subscription logic).

Related errors


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