{"record":{"id":"e58d7e42a1cfe213","repo":"apache/cassandra","slug":"can-not-finish-joining-ring-as-join-sequence-has-n","errorCode":null,"errorMessage":"Can not finish joining ring as join sequence has not been started","messagePattern":"Can not finish joining ring as join sequence has not been started","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"src/java/org/apache/cassandra/service/StorageService.java","lineNumber":1095,"sourceCode":"     * At the point when an operator decides to bring the node out of write survey mode, we need to execute the\n     * remaining steps of the join/replace sequence. The caveat is that although this execution will recommence at the\n     * point it left off (after the START_JOIN/START_REPLACE step), the {@code finishJoiningRing} flag which causes\n     * execution to pause for write survey mode must be overridden, so that we fully execute the MID step. We also want\n     * force the {@code streamData} flag to false, to prevent re-streaming the bootstrap data. To do this, we create a\n     * temporary copy of the {@link MultiStepOperation} and manually execute its next step after verifying expected\n     * invariants. This causes the MID step to fully execute, which then moves the sequence persisted in\n     * {@link ClusterMetadata}'s in-progress sequences onto the FINISH step, and we can complete the operation in the\n     * normal way with {@link InProgressSequences#finishInProgressSequences(MultiStepOperation.SequenceKey)}\n     * */\n    private void exitWriteSurveyMode()\n    {\n        ClusterMetadata metadata = ClusterMetadata.current();\n        NodeId id = metadata.myNodeId();\n        MultiStepOperation<?> sequence = metadata.inProgressSequences.get(id);\n\n        // Double check the conditions we verified in readyToFinishJoiningRing\n        if (sequence.kind() != MultiStepOperation.Kind.JOIN && sequence.kind() != MultiStepOperation.Kind.REPLACE)\n            throw new IllegalStateException(\"Can not finish joining ring as join sequence has not been started\");\n\n        if ((sequence.kind() == MultiStepOperation.Kind.JOIN && sequence.nextStep() != Transformation.Kind.MID_JOIN)\n            || (sequence.kind() == MultiStepOperation.Kind.REPLACE && sequence.nextStep() != Transformation.Kind.MID_REPLACE))\n        {\n            throw new IllegalStateException(\"Can not finish joining ring, sequence is in an incorrect state. \" +\n                                            \"If no progress is made, cancel the join process for this node and retry\");\n        }\n\n        if (sequence.kind() == MultiStepOperation.Kind.REPLACE && sequence.nextStep() != Transformation.Kind.MID_REPLACE)\n            throw new IllegalStateException(\"Can not finish joining ring, sequence is in an incorrect state. \" +\n                                            \"If no progress is made, cancel the join process for this node and retry\");\n\n        // Create a temporary new copy of the sequence with the finishJoining flag set to true and with streaming\n        // disabled, then execute its next step (the MID_*). We do this because effectively we want to jump over the\n        // MID_JOIN/MID_REPLACE of the \"real\" sequence. Note, this does not replace the existing sequence in\n        // ClusterMetadata with the temporary copy, but an effect of executing the MID step of the copy is that it will\n        // update the persisted state of the sequence leaving it with only the FINISH_* step to complete.\n        Transformation.Kind next = sequence.nextStep();","sourceCodeStart":1077,"sourceCodeEnd":1113,"githubUrl":"https://github.com/apache/cassandra/blob/88fd0f6a0eaed8943f05ac9e8f947882b8ddc8f1/src/java/org/apache/cassandra/service/StorageService.java#L1077-L1113","documentation":"Thrown by StorageService.finishJoiningRing when the node's in-progress sequence kind is neither JOIN nor REPLACE, meaning no join/replace sequence was started even though the operator asked to finish joining the ring. This is the double-check of the conditions verified earlier in readyToFinishJoiningRing. It prevents finishing a join that does not exist in ClusterMetadata.","triggerScenarios":"Calling nodetool finishjoin / finishJoiningRing when metadata.inProgressSequences for this node holds a sequence whose kind() is not MultiStepOperation.Kind.JOIN or Kind.REPLACE — e.g. no sequence at all, or an unrelated sequence (bootstrap-only, decommission, rebuild).","commonSituations":"Running finishjoin on a node that was never bootstrapped into the ring; running it after the join already finished; running it on a node executing a different multi-step operation; confusion between resumebootstrap and finishjoin commands.","solutions":["Confirm with nodetool metadata (or logs) that a JOIN or REPLACE sequence is actually in progress for this node","If no join exists, bootstrap/join the node normally instead of calling finishjoin","If the join already completed, no action needed — the node is a ring member","If the wrong operation is in progress, cancel it and restart the intended join process"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"MultiStepOperation<?> seq = ClusterMetadata.current().inProgressSequences.get(id);\nboolean canFinish = seq != null && (seq.kind() == MultiStepOperation.Kind.JOIN || seq.kind() == MultiStepOperation.Kind.REPLACE);\nif (!canFinish) throw new IllegalStateException(\"No JOIN/REPLACE sequence in progress; cannot finish joining ring\");","typeGuard":"static boolean isJoinOrReplace(MultiStepOperation<?> seq) {\n    return seq != null && (seq.kind() == MultiStepOperation.Kind.JOIN || seq.kind() == MultiStepOperation.Kind.REPLACE);\n}","tryCatchPattern":"try {\n    storageService.finishJoiningRing();\n} catch (IllegalStateException e) {\n    if (e.getMessage().contains(\"join sequence has not been started\")) {\n        logger.warn(\"No join/replace sequence in progress; nothing to finish\", e);\n    } else throw e;\n}","preventionTips":["Verify a JOIN or REPLACE sequence is in progress (nodetool metadata) before running finishjoin","Don't confuse finishjoin with resumebootstrap — check which command matches the node's actual state","After a completed join, skip finishjoin entirely"],"tags":["bootstrap","cluster-join","illegal-state","cassandra"],"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"}