KandraMultiDcTestcontainers

A real two-datacenter, one-node-per-DC Cassandra topology for DC-aware load-balancing/failover tests (GH #84 / ISS-076), backed by multidc-docker-compose.yml (this module's src/main/resources) via Testcontainers' ComposeContainer. Mirrors KandraTestcontainers's lazy-singleton, JVM-shared style -- one topology per JVM, reused across every test class that touches it, rather than a fresh cluster per test.

Why one node per DC (2 containers total), not 2+ nodes per DC

A single node per DC is the minimum topology that has the property this fixture exists to test: two distinct datacenters a client can genuinely fail over between, with NetworkTopologyStrategy replication that means something (as opposed to a single-DC cluster where every "DC" in config is cosmetic). Adding a second node per DC would let within-DC token-aware routing be exercised too, but that's not what this issue is about -- it's about cross-DC failover/load-balancing -- and real multi-node gossip convergence is significantly slower and flakier under Testcontainers than a two-single-node topology (observed directly while building this fixture: a single node with no custom config already takes ~15-20s to reach Startup complete; two nodes gossiping into one cluster took closer to a minute). Doubling the node count per DC would roughly double that again, for coverage this issue doesn't ask for. If a future issue specifically needs multi-node-per-DC behavior (e.g. real token-range replica placement within a DC), scale multidc-docker-compose.yml up rather than replacing this fixture.

Topology

  • cassandra-dc1 / cassandra-dc2, both plain cassandra:4.1 (matching KandraTestcontainers's single-node convention -- no custom cassandra.yaml).

  • GossipingPropertyFileSnitch via CASSANDRA_ENDPOINT_SNITCH, DC/rack via CASSANDRA_DC/ CASSANDRA_RACK, both nodes seeded from cassandra-dc1 via CASSANDRA_SEEDS -- so they gossip into one cluster rather than forming two independent single-node clusters that happen to share a Docker network. cassandra-dc2 waits for cassandra-dc1 to report service_healthy before its own container even starts, since racing dc2's bootstrap against dc1's gossip start-up is a likely source of flakiness in a from-scratch two-node cluster.

  • See multidc-docker-compose.yml itself for the exact environment/healthcheck configuration.

Usage

class DcFailoverTest {
private val db = KandraMultiDcTestcontainers.freshNetworkTopologyKeyspace(User::class)
@AfterEach fun cleanup() { db.close(); KandraMultiDcTestcontainers.unpauseAll() }

@Test fun `fails over to dc2 when dc1 is unreachable`() {
KandraMultiDcTestcontainers.pause(KandraMultiDcTestcontainers.DC1)
// ... exercise install(Kandra) with loadBalancing.dcAwareFailover / failover.onLocalDcUnavailable ...
}
}

Properties

Link copied to clipboard
val compose: ComposeContainer

The compose-managed topology -- started once per JVM, on first access, exactly like KandraTestcontainers.container's by lazy { ... start() } pattern. Startup timeouts are generous (see class doc: two-node gossip convergence from cold is meaningfully slower than the single-node case).

Link copied to clipboard
const val DC1: String
Link copied to clipboard
const val DC2: String

Functions

Link copied to clipboard

The real CQL contact point for dc (DC1/DC2) once compose has started.

Link copied to clipboard
fun freshNetworkTopologyKeyspace(vararg classes: KClass<*>, localDatacenter: String = DC1, dcReplicationMap: Map<String, Int> = mapOf(DC1 to 1, DC2 to 1), statementBuilderConsistencyConfig: ConsistencyConfig = ConsistencyConfig()): KandraRuntimeHandle

Creates a fresh keyspace with genuine NetworkTopologyStrategy replication across both DCs (default {'dc1': 1, 'dc2': 1} -- one real replica per DC, the most RF a one-node-per-DC topology can actually serve reads/writes at; pass a higher factor only for metadata-only assertions, mirroring KandraTestcontainers.freshKeyspace's identical caveat), registers classes, and creates their tables against the DC named by localDatacenter.

Link copied to clipboard
fun pause(dc: String)

Pauses the container backing dc via the Docker API (docker pause), simulating "this datacenter is unreachable" for failover tests without tearing down and losing the ability to bring it back.

Link copied to clipboard
fun unpause(dc: String)

Reverses pause for dc. Safe to call even if dc isn't currently paused.

Link copied to clipboard

Unpauses both DCs -- a safe, idempotent reset to call from @AfterEach regardless of what a test paused.