apache/cassandra · error · Transformation.RejectedTransformationException

When adding a read replica, that replica needs to exist as…

Error message

When adding a read replica, that replica needs to exist as a write replica before that: 

What it means

Thrown by PlacementTransitionPlan.assertPreExistingWriteReplica when a placement transformation tries to add an endpoint as a read replica for a range in which it is not already a write replica. TCM requires read-replica additions to be preceded by full write replication for the same range, otherwise reads could miss data, so the transformation is rejected with RejectedTransformationException.

Solutions

  1. First ensure the target node is a write replica for the range (complete the write placement transition / run repair) and only then add the read replica
  2. Retry the placement transform so pending write-replica deltas are applied before the read-replica delta
  3. Inspect the logged placements and delta in the message to identify the missing write replication, and correct the transition order

Example fix

// before: read replica added before write replication
PlacementTransitionPlan.addReadReplica(node, range)   // -> RejectedTransformationException
// after: apply write placement first, then read replica
PlacementTransitionPlan.addWriteReplica(node, range);
awaitReplicationOf(node, range);
PlacementTransitionPlan.addReadReplica(node, range);
Defensive patterns

Strategy: try-catch

Validate before calling

// verify write replication exists before adding a read replica
boolean isWriteReplica = currentPlacements.get(params)
    .writes.forKey(token).endpoints().contains(node);
if (!isWriteReplica) {
    throw new IllegalStateException(node + " is not a write replica for range " + range + "; apply write placement first");
}

Try / catch

try {
    ClusterMetadataService.instance().transform(...addReadReplica...);
} catch (Transformation.RejectedTransformationException e) {
    if (e.getMessage().startsWith("When adding a read replica")) {
        // apply the write-placement transition first, then retry
    } else throw e;
}

Prevention

When it happens

Trigger: Applying a placement delta (e.g. from a read-replica placement transform or repair-driven rebalance) where for some intersecting range, none of the write replicas contains the new read replica's range (contained==false and no writeReplica.contains(newReadReplica.range())).

Common situations: Operator attempts to add a read replica (nodetool readreplica / read-replica placement transform) before write replication to that node/range has completed; a previous write-placement transition failed or is still pending; running a rebalance on inconsistent placement state after a partial transformation.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of apache/cassandra@88fd0f6a0e (2026-09-10). Data as JSON: /api/errors/cbe958bd553ce517. Report an issue: GitHub.

Appendix: source

Thrown at src/java/org/apache/cassandra/tcm/ownership/PlacementTransitionPlan.java:226

                        for (Replica writeReplica : existingWriteReplicas)
                        {
                            if (writeReplica.isFull() == newReadReplica.isFull() || (writeReplica.isFull() && newReadReplica.isTransient()))
                            {
                                if (writeReplica.range().contains(newReadReplica.range()))
                                {
                                    contained = true;
                                    break;
                                }
                                else if (writeReplica.range().intersects(newReadReplica.range()))
                                {
                                    intersectingRanges.add(writeReplica.range());
                                }
                            }
                        }
                        if (!contained && Range.normalize(intersectingRanges).stream().noneMatch(writeReplica -> writeReplica.contains(newReadReplica.range())))
                        {
                            String message = "When adding a read replica, that replica needs to exist as a write replica before that: " + newReadReplica + '\n' + placements.get(params) + '\n' + delta;
                            logger.warn(message);
                            throw new Transformation.RejectedTransformationException(message);
                        }
                    }
                }
            }
            placements = deltas.apply(endpointLookup, Epoch.FIRST, placements);
        }
    }
}

View on GitHub (pinned to 88fd0f6a0e)