From f0ffa5ca14ac141b6449c5640752fd57feeea0f1 Mon Sep 17 00:00:00 2001 From: Mikita Hradovich Date: Thu, 30 Jul 2026 17:27:42 +0200 Subject: [PATCH 1/3] fix: rename 3.x driver-core CCM ITs to *Test.java so Surefire discovers them driver-core has no Failsafe plugin binding (only bound in driver-tests/osgi/*), and Surefire's default includes never match *IT.java, so these CCM integration tests were silently never executed by `mvn verify -Pshort`/`-Plong`. Renaming to *Test.java matches Surefire's default discovery pattern, mirroring the fix already applied to DriverConfigReportingCcmIT in #973. Renamed: TabletsIT, ZeroTokenNodesIT, LWTLoadBalancingIT, SchemaBuilderIT. Now that LWTLoadBalancingTest actually runs, it surfaced a real (previously undetected) bug: both test methods constructed a SimpleStatement with bound values and then passed it to session.prepare(), which rejects statements carrying values. Fixed by preparing the value-free statement and binding values only on the resulting PreparedStatement, as the tests already intended. All classes verified live against ScyllaDB 2026.1.0: Tablets (3), ZeroTokenNodes (7), and LWTLoadBalancing (2) tests pass. SchemaBuilderTest's 6 methods remain pre-existing enabled=false, unrelated to this fix. Fixes scylladb/java-driver#981. Co-Authored-By: Claude Sonnet 5 --- .../driver/core/{TabletsIT.java => TabletsTest.java} | 4 ++-- .../core/{ZeroTokenNodesIT.java => ZeroTokenNodesTest.java} | 2 +- .../{LWTLoadBalancingIT.java => LWTLoadBalancingTest.java} | 6 +++--- .../{SchemaBuilderIT.java => SchemaBuilderTest.java} | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) rename driver-core/src/test/java/com/datastax/driver/core/{TabletsIT.java => TabletsTest.java} (99%) rename driver-core/src/test/java/com/datastax/driver/core/{ZeroTokenNodesIT.java => ZeroTokenNodesTest.java} (99%) rename driver-core/src/test/java/com/datastax/driver/core/policies/{LWTLoadBalancingIT.java => LWTLoadBalancingTest.java} (97%) rename driver-core/src/test/java/com/datastax/driver/core/schemabuilder/{SchemaBuilderIT.java => SchemaBuilderTest.java} (99%) diff --git a/driver-core/src/test/java/com/datastax/driver/core/TabletsIT.java b/driver-core/src/test/java/com/datastax/driver/core/TabletsTest.java similarity index 99% rename from driver-core/src/test/java/com/datastax/driver/core/TabletsIT.java rename to driver-core/src/test/java/com/datastax/driver/core/TabletsTest.java index f3ecdd362c0..c77b959adf5 100644 --- a/driver-core/src/test/java/com/datastax/driver/core/TabletsIT.java +++ b/driver-core/src/test/java/com/datastax/driver/core/TabletsTest.java @@ -24,8 +24,8 @@ }) @ScyllaOnly @ScyllaVersion(minOSS = "6.0.0", minEnterprise = "2024.2", description = "Needs to support tablets") -public class TabletsIT extends CCMTestsSupport { - private static final Logger LOG = LoggerFactory.getLogger(TabletsIT.class); +public class TabletsTest extends CCMTestsSupport { + private static final Logger LOG = LoggerFactory.getLogger(TabletsTest.class); private static final int INITIAL_TABLETS = 32; private static final int QUERIES = 1600; private static final int REPLICATION_FACTOR = 2; diff --git a/driver-core/src/test/java/com/datastax/driver/core/ZeroTokenNodesIT.java b/driver-core/src/test/java/com/datastax/driver/core/ZeroTokenNodesTest.java similarity index 99% rename from driver-core/src/test/java/com/datastax/driver/core/ZeroTokenNodesIT.java rename to driver-core/src/test/java/com/datastax/driver/core/ZeroTokenNodesTest.java index ebcf5bdf352..cb537f030c1 100644 --- a/driver-core/src/test/java/com/datastax/driver/core/ZeroTokenNodesIT.java +++ b/driver-core/src/test/java/com/datastax/driver/core/ZeroTokenNodesTest.java @@ -14,7 +14,7 @@ import org.testng.annotations.DataProvider; import org.testng.annotations.Test; -public class ZeroTokenNodesIT { +public class ZeroTokenNodesTest { @DataProvider(name = "loadBalancingPolicies") public static Object[][] loadBalancingPolicies() { diff --git a/driver-core/src/test/java/com/datastax/driver/core/policies/LWTLoadBalancingIT.java b/driver-core/src/test/java/com/datastax/driver/core/policies/LWTLoadBalancingTest.java similarity index 97% rename from driver-core/src/test/java/com/datastax/driver/core/policies/LWTLoadBalancingIT.java rename to driver-core/src/test/java/com/datastax/driver/core/policies/LWTLoadBalancingTest.java index eed462e1dc6..5c10d31c2c0 100644 --- a/driver-core/src/test/java/com/datastax/driver/core/policies/LWTLoadBalancingIT.java +++ b/driver-core/src/test/java/com/datastax/driver/core/policies/LWTLoadBalancingTest.java @@ -39,7 +39,7 @@ * through the LWT load-balancing path (PRESERVE_REPLICA_ORDER). */ @CCMConfig(numberOfNodes = 3) -public class LWTLoadBalancingIT extends CCMTestsSupport { +public class LWTLoadBalancingTest extends CCMTestsSupport { @Override public Cluster.Builder createClusterBuilder() { @@ -61,7 +61,7 @@ public void should_route_local_serial_select_through_lwt_path() { Session session = session(); SimpleStatement simpleSelect = - new SimpleStatement("SELECT * FROM test_lwt WHERE pk = ? AND ck = ?", 1, 0); + new SimpleStatement("SELECT * FROM test_lwt WHERE pk = ? AND ck = ?"); simpleSelect.setConsistencyLevel(ConsistencyLevel.LOCAL_SERIAL); PreparedStatement preparedSelect = session.prepare(simpleSelect); @@ -91,7 +91,7 @@ public void should_route_serial_select_through_lwt_path() { Session session = session(); SimpleStatement simpleSelect = - new SimpleStatement("SELECT * FROM test_lwt WHERE pk = ? AND ck = ?", 2, 0); + new SimpleStatement("SELECT * FROM test_lwt WHERE pk = ? AND ck = ?"); simpleSelect.setConsistencyLevel(ConsistencyLevel.SERIAL); PreparedStatement preparedSelect = session.prepare(simpleSelect); diff --git a/driver-core/src/test/java/com/datastax/driver/core/schemabuilder/SchemaBuilderIT.java b/driver-core/src/test/java/com/datastax/driver/core/schemabuilder/SchemaBuilderTest.java similarity index 99% rename from driver-core/src/test/java/com/datastax/driver/core/schemabuilder/SchemaBuilderIT.java rename to driver-core/src/test/java/com/datastax/driver/core/schemabuilder/SchemaBuilderTest.java index 1a32eb2d88d..9e1b696ad4e 100644 --- a/driver-core/src/test/java/com/datastax/driver/core/schemabuilder/SchemaBuilderIT.java +++ b/driver-core/src/test/java/com/datastax/driver/core/schemabuilder/SchemaBuilderTest.java @@ -37,7 +37,7 @@ import java.util.Iterator; import org.testng.annotations.Test; -public class SchemaBuilderIT extends CCMTestsSupport { +public class SchemaBuilderTest extends CCMTestsSupport { // Test relies on existence of 'ks' keyspace, // but no such keyspace is created. If (fixed) created, From 11190888022535a76d04096f002046df927f9fac Mon Sep 17 00:00:00 2001 From: Mikita Hradovich Date: Thu, 30 Jul 2026 22:31:55 +0200 Subject: [PATCH 2/3] fix: don't pass Thrift interface flag to `ccm add` for Scylla clusters CCMBridge.add(int, int) unconditionally included `-t ` in the `ccm add` command, but scylla-ccm's `add` command has no Thrift option at all (Scylla never had a Thrift interface) -- passing it makes the whole command fail with "ccm: error: no such option: -t". Confirmed against scylladb/scylla-ccm's actual ClusterAddCmd parser (ccmlib/cmds/cluster_cmds.py, master). ZeroTokenNodesTest is the only caller of this method, and it never ran before the #981 rename fix, so this was never caught. All three "Scylla ITs" CI matrix legs on #982 failed with the identical error once the rename made the test actually execute. Verified locally against a venv with the real `scylla-ccm` (master, same as CI's `make install-scylla-ccm`) installed: all 7 ZeroTokenNodesTest methods pass, plus a full regression of the other 3 renamed classes (12/12). Co-Authored-By: Claude Sonnet 5 --- .../com/datastax/driver/core/CCMBridge.java | 44 ++++++++++++------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/driver-core/src/test/java/com/datastax/driver/core/CCMBridge.java b/driver-core/src/test/java/com/datastax/driver/core/CCMBridge.java index 344678c8149..989cb34ac43 100644 --- a/driver-core/src/test/java/com/datastax/driver/core/CCMBridge.java +++ b/driver-core/src/test/java/com/datastax/driver/core/CCMBridge.java @@ -733,24 +733,38 @@ public void add(int n) { public void add(int dc, int n) { logger.debug( String.format("Adding: node %s (%s%s:%s) to %s", n, ipPrefix, n, binaryPort, this)); - String thriftItf = ipOfNode(n) + ":" + thriftPort; String storageItf = ipOfNode(n) + ":" + storagePort; String binaryItf = ipOfNode(n) + ":" + binaryPort; String remoteLogItf = ipOfNode(n) + ":" + TestUtils.findAvailablePort(); - execute( - CCM_COMMAND - + " add node%d -d dc%s -i %s%d -t %s -l %s --binary-itf %s -j %d -r %s -s -b" - + (isDSE ? " --dse" : "") - + (isScylla ? " --scylla" : ""), - n, - dc, - ipPrefix, - n, - thriftItf, - storageItf, - binaryItf, - TestUtils.findAvailablePort(), - remoteLogItf); + if (isScylla) { + // scylla-ccm's `add` command has no thrift option: Scylla never had a Thrift interface. + execute( + CCM_COMMAND + + " add node%d -d dc%s -i %s%d -l %s --binary-itf %s -j %d -r %s -s -b --scylla", + n, + dc, + ipPrefix, + n, + storageItf, + binaryItf, + TestUtils.findAvailablePort(), + remoteLogItf); + } else { + String thriftItf = ipOfNode(n) + ":" + thriftPort; + execute( + CCM_COMMAND + + " add node%d -d dc%s -i %s%d -t %s -l %s --binary-itf %s -j %d -r %s -s -b" + + (isDSE ? " --dse" : ""), + n, + dc, + ipPrefix, + n, + thriftItf, + storageItf, + binaryItf, + TestUtils.findAvailablePort(), + remoteLogItf); + } } @Override From 5b3588ebe0d49e7e4ca1254b703818bc3c5a3bec Mon Sep 17 00:00:00 2001 From: Mikita Hradovich Date: Fri, 31 Jul 2026 17:30:09 +0200 Subject: [PATCH 3/3] test: make the newly-discovered CCM tests actually assert something Review follow-up on #982. Renaming these classes made them execute for the first time, but several of their assertions could not fail. Each fix below was verified live against ScyllaDB 2026.1.0. TabletsTest, two independent false-passes: - `removeTableMappings(KEYSPACE_NAME)` passed the mixed-case "tabletsTest" while TabletMap keys arrive lowercased from the server and are matched with an exact equals(), so the "empty out tablets information" step silently cleared nothing and an iteration could be satisfied by state learned in a previous one. Lowercased, as the three sibling call sites already do. - `executeOnAllHostsAndReturnIfResultHasTabletsInfo` pins the statement via setHost() and never clears it, so checkIfRoutedProperly re-executed a pinned statement and always observed exactly one coordinator -- `nodes.size() <= REPLICATION_FACTOR` could not fail. The pin is now cleared before the routing check. - Additionally, checkIfRoutedProperly now clears Statement.getLastHost() per iteration. PagingOptimizingLoadBalancingPolicy returns that host ahead of the real query plan and PagingOptimizingLatencyTracker sets it after every successful BoundStatement execution, which pinned the loop to its first coordinator for the bound-statement half of the matrix. LWTLoadBalancingTest could not distinguish PRESERVE_REPLICA_ORDER from RANDOM, for two reasons: - The framework's default keyspace is hardcoded to RF=1, so "the first replica" was trivially unique and hasSize(1) held under REGULAR routing too. initTestKeyspace() is now overridden to create an RF=3 keyspace (tablets disabled on Scylla, as elsewhere for replica-placement tests), following SingleTokenIntegrationTest's template. - Both tests re-executed one BoundStatement instance, so the paging optimisation described above pinned the coordinator after the first query -- hasSize(1) was guaranteed by that, not by the LWT path. Coordinator collection now binds a fresh statement per execution. - Added should_spread_non_serial_select_across_replicas as the control: same statement, same table, same policy, non-serial consistency level, asserting the coordinator does vary. Verified that it fails (1 coordinator) if REPLICATION_FACTOR is dropped back to 1, so the two hasSize(1) assertions are now load-bearing. ZeroTokenNodesTest asserted on a HashSet with containsExactly, which is order-sensitive; HashSet iteration order is hash-derived, so this was a latent flake. Switched to containsOnly, already used everywhere else in the file (AssertJ 1.7.1, which this module pins, has no containsExactlyInAnyOrder; for a Set containsOnly is equivalent). CCMBridge derived the instance's `isScylla` from the global scylla.version property instead of the constructor's scyllaVersion, unlike the sibling `isDSE = dseVersion != null` one line above. Now derived from the instance. Behaviour-neutral today -- Builder.scylla already defaults to the global, withScylla() has no callers, and every bridge that is actually built takes build()'s !versionConfigured branch where scyllaVersion *is* the global -- so this removes a footgun rather than fixing a live bug. Verified: TabletsTest 3/3, ZeroTokenNodesTest 7/7, LWTLoadBalancingTest 3/3 against ScyllaDB 2026.1.0. Co-Authored-By: Claude Opus 5 (1M context) --- .../com/datastax/driver/core/CCMBridge.java | 2 +- .../com/datastax/driver/core/TabletsTest.java | 18 ++- .../driver/core/ZeroTokenNodesTest.java | 9 +- .../core/policies/LWTLoadBalancingTest.java | 116 +++++++++++++++--- 4 files changed, 120 insertions(+), 25 deletions(-) diff --git a/driver-core/src/test/java/com/datastax/driver/core/CCMBridge.java b/driver-core/src/test/java/com/datastax/driver/core/CCMBridge.java index 989cb34ac43..1012033ffd4 100644 --- a/driver-core/src/test/java/com/datastax/driver/core/CCMBridge.java +++ b/driver-core/src/test/java/com/datastax/driver/core/CCMBridge.java @@ -430,7 +430,7 @@ protected CCMBridge( this.thriftPort = thriftPort; this.binaryPort = binaryPort; this.isDSE = dseVersion != null; - this.isScylla = (getGlobalScyllaVersion() != null); + this.isScylla = (scyllaVersion != null); this.jvmArgs = jvmArgs; this.nodes = nodes; this.ccmDir = Files.createTempDir(); diff --git a/driver-core/src/test/java/com/datastax/driver/core/TabletsTest.java b/driver-core/src/test/java/com/datastax/driver/core/TabletsTest.java index c77b959adf5..4b4694ed2fe 100644 --- a/driver-core/src/test/java/com/datastax/driver/core/TabletsTest.java +++ b/driver-core/src/test/java/com/datastax/driver/core/TabletsTest.java @@ -190,8 +190,13 @@ public void every_statement_should_deliver_tablet_info() { continue; } Session session = sessionEntry.getValue().get(); - // Empty out tablets information - session.getCluster().getMetadata().getTabletMap().removeTableMappings(KEYSPACE_NAME); + // Empty out tablets information. The mapping is keyed by the lowercased keyspace name, as + // reported by the server, so the key has to be lowercased here too or this is a no-op. + session + .getCluster() + .getMetadata() + .getTabletMap() + .removeTableMappings(KEYSPACE_NAME.toLowerCase()); Statement stmt; try { stmt = stmtEntry.getValue().apply(session); @@ -226,6 +231,10 @@ public void every_statement_should_deliver_tablet_info() { stmtEntry.getKey(), sessionEntry.getKey())); continue; } + // executeOnAllHostsAndReturnIfResultHasTabletsInfo pins the statement to a specific host + // while hunting for tablet info. Clear that pin, otherwise the routing check below always + // observes the pinned host and can never detect misrouting. + stmt.setHost(null); if (!checkIfRoutedProperly(session, stmt)) { testErrors.add( String.format( @@ -343,6 +352,11 @@ private static boolean checkIfRoutedProperly(Session session, Statement stmt) { int expectedNodesCount = stmt.isLWT() ? 1 : REPLICATION_FACTOR; Set nodes = new HashSet<>(); for (int i = 0; i < REPLICATION_FACTOR * 3; i++) { + // PagingOptimizingLoadBalancingPolicy returns Statement.getLastHost() ahead of the real query + // plan, and that field is set after every successful BoundStatement execution. Clearing it + // keeps the loop from being pinned to the first coordinator, which would let any routing + // behaviour satisfy the check below. + stmt.setLastHost(null); nodes.add(session.execute(stmt).getExecutionInfo().getQueriedHost()); } return nodes.size() <= expectedNodesCount; diff --git a/driver-core/src/test/java/com/datastax/driver/core/ZeroTokenNodesTest.java b/driver-core/src/test/java/com/datastax/driver/core/ZeroTokenNodesTest.java index cb537f030c1..26c0041303e 100644 --- a/driver-core/src/test/java/com/datastax/driver/core/ZeroTokenNodesTest.java +++ b/driver-core/src/test/java/com/datastax/driver/core/ZeroTokenNodesTest.java @@ -173,12 +173,17 @@ public void should_discover_zero_token_DC_when_option_is_enabled( queriedNodes.add(rs.getExecutionInfo().getQueriedHost().getEndPoint().resolve()); } + // containsOnly, not containsExactly: queriedNodes is a HashSet, whose iteration order is + // hash-derived rather than insertion order, so an order-sensitive assertion is a latent + // flake. + // AssertJ 1.7.1, pinned by this module, has no containsExactlyInAnyOrder; for a Set, + // containsOnly is equivalent to it. if (isDcAware) { assertThat(queriedNodes) - .containsExactly(ccmBridge.addressOfNode(1), ccmBridge.addressOfNode(2)); + .containsOnly(ccmBridge.addressOfNode(1), ccmBridge.addressOfNode(2)); } else { assertThat(queriedNodes) - .containsExactly( + .containsOnly( ccmBridge.addressOfNode(1), ccmBridge.addressOfNode(2), ccmBridge.addressOfNode(3), diff --git a/driver-core/src/test/java/com/datastax/driver/core/policies/LWTLoadBalancingTest.java b/driver-core/src/test/java/com/datastax/driver/core/policies/LWTLoadBalancingTest.java index 5c10d31c2c0..f9eb78bc544 100644 --- a/driver-core/src/test/java/com/datastax/driver/core/policies/LWTLoadBalancingTest.java +++ b/driver-core/src/test/java/com/datastax/driver/core/policies/LWTLoadBalancingTest.java @@ -29,9 +29,14 @@ import com.datastax.driver.core.ResultSet; import com.datastax.driver.core.Session; import com.datastax.driver.core.SimpleStatement; +import com.datastax.driver.core.TestUtils; +import com.google.common.base.Throwables; import java.net.InetSocketAddress; import java.util.HashSet; +import java.util.Objects; import java.util.Set; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.testng.annotations.Test; /** @@ -41,6 +46,13 @@ @CCMConfig(numberOfNodes = 3) public class LWTLoadBalancingTest extends CCMTestsSupport { + private static final Logger LOGGER = LoggerFactory.getLogger(LWTLoadBalancingTest.class); + + /** Equal to the node count, so that every node is a replica of every partition. */ + private static final int REPLICATION_FACTOR = 3; + + private static final int EXECUTIONS = 30; + @Override public Cluster.Builder createClusterBuilder() { return Cluster.builder() @@ -48,6 +60,40 @@ public Cluster.Builder createClusterBuilder() { new TokenAwarePolicy(new RoundRobinPolicy(), TokenAwarePolicy.ReplicaOrdering.RANDOM)); } + /** + * Override to create the keyspace with a replication factor greater than 1. The default test + * keyspace created by {@link CCMTestsSupport} is hardcoded to RF=1, and with a single replica per + * partition "the first replica" is trivially unique — every assertion below would hold under + * {@code REGULAR} routing too, so the tests could not tell {@code PRESERVE_REPLICA_ORDER} apart + * from {@code RANDOM}. + * + *

Tablets are disabled when running against Scylla: with tablets enabled, replica placement + * comes from the tablet map, which is empty until it has been learned from a misrouted query, and + * an empty replica list makes the LWT query plan fall back to the child policy — a non-replica + * coordinator on the first execution. Cassandra does not support the tablets property. + */ + @Override + protected void initTestKeyspace() { + try { + keyspace = TestUtils.generateIdentifier("ks_"); + LOGGER.debug("Using keyspace " + keyspace); + boolean isScylla = Objects.nonNull(ccm().getScyllaVersion()); + session() + .execute( + String.format( + "CREATE KEYSPACE %s WITH replication = {'class': 'NetworkTopologyStrategy'," + + " 'datacenter1': %d}" + + (isScylla ? " AND tablets = {'enabled': false}" : ""), + keyspace, + REPLICATION_FACTOR)); + useKeyspace(keyspace); + } catch (Exception e) { + errorOut(); + LOGGER.error("Could not create test keyspace", e); + Throwables.propagate(e); + } + } + @Override public void onTestContextInitialized() { execute("CREATE TABLE IF NOT EXISTS test_lwt (pk int, ck int, v int, PRIMARY KEY (pk, ck))"); @@ -65,25 +111,15 @@ public void should_route_local_serial_select_through_lwt_path() { simpleSelect.setConsistencyLevel(ConsistencyLevel.LOCAL_SERIAL); PreparedStatement preparedSelect = session.prepare(simpleSelect); - BoundStatement boundSelect = preparedSelect.bind(1, 0); // Verify statement properties assertThat(simpleSelect.isLWT()).isFalse(); assertThat(simpleSelect.getConsistencyLevel()).isEqualTo(ConsistencyLevel.LOCAL_SERIAL); - // Execute multiple times and collect coordinators — with PRESERVE_REPLICA_ORDER routing, - // the same partition key should always be routed to the same first replica. - Set coordinators = new HashSet<>(); - for (int i = 0; i < 30; i++) { - ResultSet rs = session.execute(boundSelect); - Host coordinator = rs.getExecutionInfo().getQueriedHost(); - assertThat(coordinator).isNotNull(); - coordinators.add(coordinator.getEndPoint().resolve()); - } - // With PRESERVE_REPLICA_ORDER, the first replica is deterministic for a given partition key, - // so all 30 executions should hit the same coordinator. - assertThat(coordinators).hasSize(1); + // so every execution should hit the same coordinator. Contrast with the non-serial control in + // should_spread_non_serial_select_across_replicas, which shares the same statement and policy. + assertThat(collectCoordinators(session, preparedSelect, 1)).hasSize(1); } @Test(groups = "short") @@ -95,18 +131,58 @@ public void should_route_serial_select_through_lwt_path() { simpleSelect.setConsistencyLevel(ConsistencyLevel.SERIAL); PreparedStatement preparedSelect = session.prepare(simpleSelect); - BoundStatement boundSelect = preparedSelect.bind(2, 0); - // Execute multiple times and collect coordinators + // With PRESERVE_REPLICA_ORDER, the first replica is deterministic for a given partition key. + assertThat(collectCoordinators(session, preparedSelect, 2)).hasSize(1); + } + + /** + * Control for the two tests above. This is the same statement against the same table, executed by + * the same {@code TokenAwarePolicy(RoundRobinPolicy, RANDOM)} — only the consistency level + * differs. A non-serial level takes the {@code REGULAR} routing path, which shuffles the replicas + * on every query, so the coordinator must vary. If this test ever collapses to a single + * coordinator as well, the {@code hasSize(1)} assertions above have stopped proving anything + * about {@code PRESERVE_REPLICA_ORDER}. + */ + @Test(groups = "short") + public void should_spread_non_serial_select_across_replicas() { + Session session = session(); + + SimpleStatement simpleSelect = + new SimpleStatement("SELECT * FROM test_lwt WHERE pk = ? AND ck = ?"); + simpleSelect.setConsistencyLevel(ConsistencyLevel.ONE); + + PreparedStatement preparedSelect = session.prepare(simpleSelect); + BoundStatement boundSelect = preparedSelect.bind(3, 0); + + assertThat(boundSelect.isLWT()).isFalse(); + assertThat(boundSelect.getConsistencyLevel().isSerial()).isFalse(); + + // Uniform over REPLICATION_FACTOR replicas across EXECUTIONS queries, so the probability of a + // false failure here is REPLICATION_FACTOR^(1 - EXECUTIONS). + assertThat(collectCoordinators(session, preparedSelect, 3).size()).isGreaterThan(1); + } + + /** + * Executes {@code prepared} against partition {@code pk} {@link #EXECUTIONS} times and returns + * the distinct coordinators used. + * + *

A fresh {@link BoundStatement} is bound for every execution on purpose. {@link + * PagingOptimizingLoadBalancingPolicy}, which the driver wraps around the configured policy, + * returns {@code Statement.getLastHost()} ahead of the real query plan, and that field is set on + * every successful {@code BoundStatement} execution. Reusing a single instance would therefore + * pin the coordinator after the first query and make every assertion in this class hold + * regardless of how routing actually behaves. + */ + private static Set collectCoordinators( + Session session, PreparedStatement prepared, int pk) { Set coordinators = new HashSet<>(); - for (int i = 0; i < 30; i++) { - ResultSet rs = session.execute(boundSelect); + for (int i = 0; i < EXECUTIONS; i++) { + ResultSet rs = session.execute(prepared.bind(pk, 0)); Host coordinator = rs.getExecutionInfo().getQueriedHost(); assertThat(coordinator).isNotNull(); coordinators.add(coordinator.getEndPoint().resolve()); } - - // With PRESERVE_REPLICA_ORDER, the first replica is deterministic for a given partition key. - assertThat(coordinators).hasSize(1); + return coordinators; } }