apache/pulsar · error · IllegalStateException

cancelAction can only be set once.

Error message

cancelAction can only be set once.

What it means

Thrown by CompletableFutureCancellationHandler.setCancelAction when a cancel action is set more than once, or after cancellation was already handled. The action runs exactly once when the attached future is cancelled or times out; allowing re-assignment would create double-invocation or race hazards, so the handler enforces one-time set semantics.

Source

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

            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.");
        }
        this.cancelAction = Objects.requireNonNull(cancelAction);
        // handle race condition in the case that the future was already cancelled when the handler is set
        runCancelActionOnceIfCancelled();
    }

    private BiConsumer<Object, ? super Throwable> whenCompleteFunction() {
        return (v, throwable) -> {
            if (throwable instanceof CancellationException || throwable instanceof TimeoutException) {
                completionStatus = CompletionStatus.CANCELLED;
            } else {
                completionStatus = CompletionStatus.DONE;
            }
            runCancelActionOnceIfCancelled();
        };
    }

    private void runCancelActionOnceIfCancelled() {

View on GitHub (pinned to 820761864e)

Solutions

  1. Set the cancel action exactly once per handler instance, immediately after construction, before any cancellation can occur
  2. Create a new handler per operation rather than sharing it across receive/batchReceive paths
  3. Check the future state before configuring: if already done/cancelled, skip setCancelAction and run cleanup directly
  4. Refactor so cancelAction setup is part of the single code path that creates the future

Example fix

// before
handler.setCancelAction(this::cleanup);
// ... later on retry path
handler.setCancelAction(this::cleanup); // IllegalStateException
// after
if (handlerCancelActionNotSet) { // ensure single call
    handler.setCancelAction(this::cleanup);
}
// or: create a new handler per request
Defensive patterns

Strategy: try-catch

Try / catch

try {
    handler.setCancelAction(action);
} catch (IllegalStateException e) {
    // action already set, or future already cancelled — skip re-registration
    log.debug("cancel action already established for this handler");
}

Prevention

When it happens

Trigger: Calling setCancelAction twice on the same handler instance, or calling it after the future was already cancelled (cancelHandled set) — e.g. consumer code paths internalReceiveAsync and internalBatchReceiveAsync both configuring the same shared handler.

Common situations: Refactoring consumer code so two receive paths share a handler; retry logic re-setting the cancel action on the same request's handler; setting the action after the operation already timed out and was cancelled; storing the handler in a singleton service reused across requests.

Related errors


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