{"record":{"id":"2897fa98e3738fbd","repo":"apache/cassandra","slug":"can-t-revert-replacement-from","errorCode":null,"errorMessage":"Can't revert replacement from ","messagePattern":"Can't revert replacement from ","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"src/java/org/apache/cassandra/tcm/sequences/BootstrapAndReplace.java","lineNumber":333,"sourceCode":"        InetAddressAndPort replaced = metadata.directory.getNodeAddresses(startReplace.replaced()).broadcastAddress;\n        return new ProgressBarrier(latestModification, metadata.directory.location(startReplace.nodeId()), metadata.lockedRanges.locked.get(lockKey), e -> !e.equals(replaced));\n    }\n\n    @Override\n    public ClusterMetadata.Transformer cancel(ClusterMetadata metadata)\n    {\n        DataPlacements placements = metadata.placements();\n        switch (next)\n        {\n            // need to undo MID_REPLACE and START_REPLACE, but PREPARE_REPLACE doesn't affect placements\n            case FINISH_REPLACE:\n                placements = midReplace.inverseDelta().apply(metadata.directory, metadata.nextEpoch(), placements);\n            case MID_REPLACE:\n            case START_REPLACE:\n                placements = startReplace.inverseDelta().apply(metadata.directory, metadata.nextEpoch(), placements);\n                break;\n            default:\n                throw new IllegalStateException(\"Can't revert replacement from \" + next);\n        }\n\n        LockedRanges newLockedRanges = metadata.lockedRanges.unlock(lockKey);\n        return metadata.transformer()\n                       .withNodeState(startReplace.replacement(), NodeState.REGISTERED)\n                       .with(placements)\n                       .with(newLockedRanges);\n    }\n\n    public BootstrapAndReplace finishJoiningRing()\n    {\n        return new BootstrapAndReplace(latestModification, lockKey, bootstrapTokens,\n                                       next, startReplace, midReplace, finishReplace,\n                                       true, false);\n    }\n\n    /**\n     * startDelta.writes.additions contains the ranges we need to stream","sourceCodeStart":315,"sourceCodeEnd":351,"githubUrl":"https://github.com/apache/cassandra/blob/88fd0f6a0eaed8943f05ac9e8f947882b8ddc8f1/src/java/org/apache/cassandra/tcm/sequences/BootstrapAndReplace.java#L315-L351","documentation":"BootstrapAndReplace.cancel reverts a partially applied replacement by inverting transformations; its switch handles MID_REPLACE/START_REPLACE (falling through to invert startReplace). The default branch throws IllegalStateException when the current step kind is not one of the replace states, so the replacement cannot be reverted from that state.","triggerScenarios":"Calling cancel() on a BootstrapAndReplace whose current step is FINISH_REPLACE (replacement already committed) or any kind outside the replace sequence, e.g. via a retry racing with completion.","commonSituations":"Operator retries the cancel command after the replacement already finished; a failed replace leaves state at an unexpected kind; metadata replay assigns a non-replace kind to this sequence.","solutions":["Check the current step before cancelling; if the replace reached FINISH_REPLACE, treat the node as replaced and use the appropriate removal flow instead","Re-read ClusterMetadata for the authoritative node state and re-issue cancellation only for a valid pending state","If the state is genuinely unexpected, treat it as a metadata consistency issue and repair/replay the log","Ensure only one cancellation attempt runs at a time to avoid racing the sequence state"],"exampleFix":"// before\nbootstrapAndReplace.cancel(metadata);\n// after\nif (bootstrapAndReplace.nextStep() == Transformation.Kind.FINISH_REPLACE)\n    // replacement committed; nothing to revert\nelse\n    bootstrapAndReplace.cancel(metadata);","handlingStrategy":"try-catch","validationCode":"Transformation.Kind step = sequence.nextStep();\nif (step != Transformation.Kind.START_REPLACE && step != Transformation.Kind.MID_REPLACE)\n    throw new IllegalStateException(\"cannot revert replace from step \" + step);","typeGuard":null,"tryCatchPattern":"try { seq.cancel(metadata); } catch (IllegalStateException e) { /* if FINISH_REPLACE: node is replaced; use removal flow instead */ }","preventionTips":["Verify replace state before issuing cancel","Serialize cancellation attempts to avoid races with completion","On committed replacements, switch to decommission/removenode flows"],"tags":["tcm","replace","revert","state-machine","illegal-state"],"backgroundTag":"invalid-state-transition","analyzedSha":"88fd0f6a0eaed8943f05ac9e8f947882b8ddc8f1","analyzedAt":"2026-09-10T07:29:22.284Z","contentChangedAt":"2026-09-10T07:29:22.284Z","schemaVersion":2},"datasetVersion":"2026-09-14T16:17:12.679Z"}