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
- 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
- Retry the placement transform so pending write-replica deltas are applied before the read-replica delta
- 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
- Always complete write-placement transitions (and repair) before adding read replicas
- Order placement transforms: write replica first, then read replica
- Check the target node's write coverage of the range with nodetool/CMC before requesting read-replica status
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
- Maps differ: !=
- Can not alter a keyspace to use MetaReplicationStrategy
- Can not create a keyspace with MetaReplicationStrategy
- Can not parse replication factor
- Can only initialize cluster identifier once, but it was…
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)