Skip to content

CASSANALYTICS-197: Source token ranges from Cassandra for mutation tracked keyspaces - #244

Open
mansikhara wants to merge 10 commits into
apache:mutation-tracking-supportfrom
mansikhara:CASSANALYTICS-197
Open

mansikhara wants to merge 10 commits into
apache:mutation-tracking-supportfrom
mansikhara:CASSANALYTICS-197

Conversation

@mansikhara

@mansikhara mansikhara commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

https://issues.apache.org/jira/browse/CASSANALYTICS-197

The bulk reader derives token ranges itself from node tokens plus the replication factor. CassandraRing documents that this assumes racks are not in use, but Cassandra's replica
assignment is rack aware (NetworkTopologyStrategy.acceptableRackRepeats), so the two only agree when a datacenter has a single rack.

For a mutation tracked keyspace a range may be replicated to a witness that holds no data, so the reader must know which instance replicates which range. The derived ranges cannot
answer that reliably.

Changes

  • Tracked keyspaces take the range-to-replica mapping from the token-range-replicas endpoint, using readReplicas so pending ranges are excluded
  • Untracked keyspaces are unchanged: same /ring call, same derivation, and the extra request is not issued
  • Node discovery still comes from /ring in both cases, so snapshot creation, sizing and the Sidecar client pool are untouched. Only the range-to-replica mapping differs
  • CassandraRing gains an optional Cassandra-supplied range mapping. When absent it derives exactly as before. Both hand-rolled serializers carry the new field and the JDK format
    version is bumped
  • Replicas are reported as address:port while instances are keyed by fqdn, so the join goes through the replica metadata
  • Fails at job setup if the reported ranges do not cover the whole ring. Otherwise an uncovered range surfaces much later as a per-partition consistency error resembling a cluster
    availability problem
  • CassandraCluster now honours dcAndRackSupplier for a single datacenter. It previously applied only when dcCount > 1, so a multi-rack single-DC topology was silently ignored
  • Adds a forcecassandratokenranges option so the new path can be exercised against untracked keyspaces, the only coverage available until Cassandra 6.0 bridge modules land

Testing

  • 61 unit tests: the replica join, datacenter filtering, skipping a replica absent from the ring, rejecting a multi-token node, ring-coverage failure, replica dedup, and both
    serialization round-trips
  • Two new integration tests, both passing locally against Cassandra 5.0
  • BulkReaderMultiRackTest is a regression guard rather than witness coverage. Every replica in it holds data, so both range sources return the same rows even though they select
    different replicas. Actual witness behaviour needs a tracked keyspace and waits on CASSANALYTICS-192

mkhara added 10 commits August 28, 2026 16:46
…r witness-enabled keyspaces

Cassandra accepts a replication factor of the form <replicas>/<transient>, which witness
replicas under mutation tracking reuse, so a witness-enabled keyspace declares
'datacenter1': '3/1' meaning three replicas of which one is a witness.

CqlUtils.extractReplicationFactor() passed each datacenter value to Integer.parseInt, so a
bulk read against any such keyspace failed during job setup with an uncaught
NumberFormatException.

Transient counts are now tracked per datacenter alongside the existing totals and exposed
through getFullReplicationFactor(), getTransientReplicationFactor(), getFullReplicas(dc),
getTransientReplicas(dc) and hasTransientReplicas(). getTotalReplicationFactor() keeps its
existing meaning of all replicas including witnesses, so behaviour for untracked keyspaces
is unchanged. Parsing applies the same constraints Cassandra enforces in
locator.ReplicationFactor.validate. A new parseStrict factory reports an unparseable or
empty replication map at parse time rather than dropping the datacenter and failing later
with a misleading "DC not found in replication factor"; the lenient constructor is retained
unchanged for CDC callers.

The Kryo serializer and CassandraRing's hand-rolled JDK readObject/writeObject are updated
so the new field survives serialization to Spark executors. CassandraRing gains a pinned
serialVersionUID and a format-version byte, because the class signature did not change and
an older stream would otherwise be silently misread rather than rejected.

Prerequisite for CASSANALYTICS-164.

patch by Mansi Khara; reviewed by TBD for CASSANALYTICS-194
CassandraRing derives token ranges locally from tokens and the replication factor. That
derivation ignores racks, while Cassandra's replica assignment is rack aware, so the derived
ranges are only guaranteed to match Cassandra for a single rack per datacenter. Witness
replica support needs replica information per real token range, so the derived ranges cannot
be relied on.

Adds an optional range-to-replicas mapping. When supplied, init() uses it instead of
deriving; when absent the behaviour is unchanged, so untracked reads and the CDC caller are
unaffected. Both hand-rolled serializers carry the new field and the JDK format version is
bumped to 2.

This is groundwork only. Nothing supplies the mapping yet.
For a tracked keyspace a range may be replicated to a witness that holds no data, so the
reader must know which instance replicates which range. The locally derived ranges cannot
answer that: the derivation ignores racks while Cassandra's replica assignment is rack aware,
so the two only agree for a single rack per datacenter.

When the keyspace is tracked, the range to replica mapping is now taken from the
token-range-replicas endpoint (readReplicas, so pending ranges are excluded). Untracked
keyspaces keep using the ring response exactly as before.

Node discovery still comes from the ring response in both cases, so snapshot creation, sizing
and the Sidecar client pool are untouched. Only the range to replica mapping changes.

Replicas are reported as "address:port" and instances are keyed by fqdn, so the join goes
through the replica metadata. A replica absent from the ring is skipped with a warning, since
the ring is the source of truth for which nodes the reader can reach. A node owning multiple
tokens is rejected, because it cannot be represented as a single CassandraInstance; mutation
tracking requires num_tokens=1 so this should not arise.

Adds a forceCassandraTokenRanges option to exercise the new path against untracked keyspaces.
Tracked keyspaces need Cassandra 6.0 bridge modules before they can be created in integration
tests, so without this the path would merge with no integration coverage.
Covers createCassandraRingFromTokenRangeReplicas: the address-to-fqdn join through the replica
metadata, datacenter filtering, skipping a replica absent from the ring, rejecting a node that
owns multiple tokens, and failing when Cassandra reports no read replicas.

The fixture uses a range boundary the local derivation cannot produce, so the tests fail if
the Cassandra-reported ranges are ignored.

Also documents that forceCassandraTokenRanges is set lowercased, since Spark lowercases option
keys and MapUtils looks them up that way.
…ck-aware cluster

BulkReaderCassandraTokenRangesTest exercises the new topology path against an ordinary keyspace
using forcecassandratokenranges, since tracked keyspaces cannot be created until Cassandra 6.0
bridge modules land. It asserts the full dataset rather than a row count, because an incomplete
read is the failure mode the path exists to prevent, and covers LOCAL_QUORUM and a pushdown
filter.

BulkReaderMultiRackTest closes a coverage gap. Every existing test places all nodes of a
datacenter in one rack, where Cassandra's rack-aware assignment reduces to taking the next RF
nodes in ring order, which is what the reader derives. The cluster here uses six nodes across
three racks with two adjacent nodes per rack and RF 3, so ring order starts 1,2,3 while
Cassandra must pick 1,3,5 to satisfy one replica per rack. The two topology sources therefore
genuinely disagree. One node per rack, or round-robin racks, would coincide and prove nothing.

CassandraCluster only applied dcAndRackSupplier when dcCount was greater than one, so a
single-datacenter multi-rack topology was silently ignored and fell back to the default rack.
An explicit supplier is now always honoured; the built-in two-datacenter default is unchanged.
Four nodes is the minimum that makes the two topology sources disagree, so the test no longer
asks for six. NetworkTopologyStrategy computes acceptableRackRepeats as RF minus rackCount,
which is zero with three racks and RF 3, so a node whose rack was already used is skipped: for
the range starting at node 1 the reader derives ring order 1,2,3 while Cassandra picks 1,3,4.
Six nodes added no coverage and needed two more loopback aliases.
A range whose replicas were all skipped, or a topology response that simply omits part of the
ring, previously produced a ring with a gap and no error. CassandraRing seeds its range map
across the whole ring, so the uncovered range is present but with an empty replica list, and
the reader only discovers this once a Spark partition covering it fails with
NotEnoughReplicasException - which reads as a cluster availability problem rather than an
incomplete topology. Note that TokenPartitioner's coverage assertion does not catch it either.

Also deduplicates the replicas recorded per range. PartitionedDataLayer counts replicas per
range to decide whether the consistency level is satisfied, so a duplicate would inflate that
count and could let a read proceed at a consistency level it has not actually met.
Four formatting violations in the new tests: whitespace after '{' in three array initialisers,
and a brace left on the same line as a method signature.

Also records what BulkReaderMultiRackTest does and does not establish. Every replica in it is a
full replica holding data, so both replica selections return all rows and the reads agree even
though Cassandra and the reader choose different replicas. It guards the range mapping and the
replica join against regression; it does not show that witness replicas are handled, because
choosing the wrong replicas only loses data once some of them hold none. A green run should not
be read as covering the witness case.
…izer index widths

The class javadoc asserted that token ranges are calculated assuming racks are not in use. That
is now only true of the local derivation, and it is exactly the assumption the Cassandra-supplied
ranges exist to avoid, so it described the opposite of the new behaviour. It now distinguishes
the two paths.

The JDK serializer wrote replica counts and indexes as shorts while the Kryo serializer used
ints. Not a defect, since the instance count is itself a short so indexes cannot exceed its
range, but the two paths have to stay in step and the asymmetry would silently truncate if that
width ever widened. Both now use int.
@mansikhara mansikhara changed the title Source token ranges from Cassandra for mutation tracked keyspaces CASSANALYTICS-197: Source token ranges from Cassandra for mutation tracked keyspaces Sep 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant