{"record":{"id":"a2d050749250e85e","repo":"apache/pulsar","slug":"cancelaction-can-only-be-set-once","errorCode":null,"errorMessage":"cancelAction can only be set once.","messagePattern":"cancelAction can only be set once\\.","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"pulsar-common/src/main/java/org/apache/pulsar/common/util/CompletableFutureCancellationHandler.java","lineNumber":94,"sourceCode":"            throw new IllegalStateException(\"A future has already been attached to this instance.\");\n        }\n        attached = true;\n        future.whenComplete(whenCompleteFunction());\n    }\n\n    /**\n     * Set the action to run when the future gets cancelled or timeouts.\n     * The cancellation or timeout might be originating from any \"upstream\" future.\n     * The implementation ensures that the cancel action gets called once.\n     * Handles possible race conditions that might happen when the future gets cancelled\n     * before the cancel action is set to this handler. In this case, the\n     * cancel action gets called when the action is set.\n     *\n     * @param cancelAction the action to run when the the future gets cancelled or timeouts\n     */\n    public void setCancelAction(Runnable cancelAction) {\n        if (this.cancelAction != null || cancelHandled.get()) {\n            throw new IllegalStateException(\"cancelAction can only be set once.\");\n        }\n        this.cancelAction = Objects.requireNonNull(cancelAction);\n        // handle race condition in the case that the future was already cancelled when the handler is set\n        runCancelActionOnceIfCancelled();\n    }\n\n    private BiConsumer<Object, ? super Throwable> whenCompleteFunction() {\n        return (v, throwable) -> {\n            if (throwable instanceof CancellationException || throwable instanceof TimeoutException) {\n                completionStatus = CompletionStatus.CANCELLED;\n            } else {\n                completionStatus = CompletionStatus.DONE;\n            }\n            runCancelActionOnceIfCancelled();\n        };\n    }\n\n    private void runCancelActionOnceIfCancelled() {","sourceCodeStart":76,"sourceCodeEnd":112,"githubUrl":"https://github.com/apache/pulsar/blob/820761864ed8e2a7d2e52dd9763ad2ae117c1395/pulsar-common/src/main/java/org/apache/pulsar/common/util/CompletableFutureCancellationHandler.java#L76-L112","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nhandler.setCancelAction(this::cleanup);\n// ... later on retry path\nhandler.setCancelAction(this::cleanup); // IllegalStateException\n// after\nif (handlerCancelActionNotSet) { // ensure single call\n    handler.setCancelAction(this::cleanup);\n}\n// or: create a new handler per request","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n    handler.setCancelAction(action);\n} catch (IllegalStateException e) {\n    // action already set, or future already cancelled — skip re-registration\n    log.debug(\"cancel action already established for this handler\");\n}","preventionTips":["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"],"tags":["concurrency","async","future","state"],"backgroundTag":"illegal-state-reuse","analyzedSha":"820761864ed8e2a7d2e52dd9763ad2ae117c1395","analyzedAt":"2026-09-06T00:14:20.138Z","contentChangedAt":"2026-09-06T00:14:20.138Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}