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
- Set the cancel action exactly once per handler instance, immediately after construction, before any cancellation can occur
- Create a new handler per operation rather than sharing it across receive/batchReceive paths
- Check the future state before configuring: if already done/cancelled, skip setCancelAction and run cleanup directly
- 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
- Set the cancel action once, immediately after handler creation and before attach/timeout windows
- Never share a handler across receive and batchReceive code paths
- If the future may already be cancelled, run the action directly instead of setCancelAction
- Make cancelAction setup part of the single constructor path for the operation
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
- A future has already been attached to this instance.
- ManagedLedgerFactory is already closed.
- ManagedLedger ${name} has already been closed
- Migration is already in progress (phase: ${phase})
- Concurrent modification
AI-assisted analysis of apache/pulsar@820761864e (2026-09-06).
Data as JSON: /api/errors/a2d050749250e85e.
Report an issue: GitHub.