This is an automated email from the ASF dual-hosted git repository.
lukasz-antoniak pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra-analytics.git
The following commit(s) were added to refs/heads/trunk by this push:
new be5080e3 CASSANALYTICS-179: Fix total timeout calculation for
getAllNodeSettings
be5080e3 is described below
commit be5080e3f907a65b8acc5ce7e9c4203978d88fde
Author: Lukasz Antoniak <[email protected]>
AuthorDate: Thu Jul 9 09:33:18 2026 +0200
CASSANALYTICS-179: Fix total timeout calculation for getAllNodeSettings
---
CHANGES.txt | 1 +
.../spark/bulkwriter/CassandraClusterInfo.java | 11 ++-
.../spark/bulkwriter/CassandraClusterInfoTest.java | 80 ++++++++++++++++++++++
3 files changed, 86 insertions(+), 6 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index 7834ba38..1d645a06 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,6 @@
0.5.0
-----
+ * Fix total timeout calculation for getAllNodeSettings (CASSANALYTICS-179)
* Support vector data type (CASSANALYTICS-26)
* CDC batch-write mixing a CDC-enabled and CDC-disabled table drops the CDC
table's mutation (CASSANALYTICS-182)
* CdcState.ReplicaCountSerializer map-size overflow corrupts persisted CDC
state (CASSANALYTICS-184)
diff --git
a/cassandra-analytics-core/src/main/java/org/apache/cassandra/spark/bulkwriter/CassandraClusterInfo.java
b/cassandra-analytics-core/src/main/java/org/apache/cassandra/spark/bulkwriter/CassandraClusterInfo.java
index 1b1a4017..3f7523bc 100644
---
a/cassandra-analytics-core/src/main/java/org/apache/cassandra/spark/bulkwriter/CassandraClusterInfo.java
+++
b/cassandra-analytics-core/src/main/java/org/apache/cassandra/spark/bulkwriter/CassandraClusterInfo.java
@@ -502,12 +502,11 @@ public class CassandraClusterInfo implements ClusterInfo,
Closeable
+ "Cassandra version is
pre-computed on driver and broadcast to executors.");
}
- // Worst-case, the http client is configured for 1 worker pool.
- // In that case, each future can take the full retry delay * number of
retries,
- // and each instance will be processed serially.
- final long totalTimeout = conf.getSidecarRequestMaxRetryDelayMillis() *
- conf.getSidecarRequestRetries() *
- allNodeSettingFutures.size();
+ // Each of the retry attempts can take up to the request timeout plus
the max delay
+ // before the next retry. Requests to all instances run in parallel,
so the cluster-wide
+ // wait is bounded by a single node's worst case.
+ final long totalTimeout =
(TimeUnit.SECONDS.toMillis(conf.getSidecarRequestTimeoutSeconds()) +
conf.getSidecarRequestMaxRetryDelayMillis())
+ * conf.getSidecarRequestRetries();
List<NodeSettings> allNodeSettings =
FutureUtils.bestEffortGet(allNodeSettingFutures,
totalTimeout,
TimeUnit.MILLISECONDS);
diff --git
a/cassandra-analytics-core/src/test/java/org/apache/cassandra/spark/bulkwriter/CassandraClusterInfoTest.java
b/cassandra-analytics-core/src/test/java/org/apache/cassandra/spark/bulkwriter/CassandraClusterInfoTest.java
index 5ed4c522..99b06712 100644
---
a/cassandra-analytics-core/src/test/java/org/apache/cassandra/spark/bulkwriter/CassandraClusterInfoTest.java
+++
b/cassandra-analytics-core/src/test/java/org/apache/cassandra/spark/bulkwriter/CassandraClusterInfoTest.java
@@ -21,12 +21,22 @@ package org.apache.cassandra.spark.bulkwriter;
import java.time.Duration;
import java.time.Instant;
+import java.util.ArrayList;
import java.util.Collections;
+import java.util.List;
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Stream;
import com.google.common.collect.ImmutableMap;
+import com.google.common.util.concurrent.Uninterruptibles;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import o.a.c.sidecar.client.shaded.common.response.NodeSettings;
import o.a.c.sidecar.client.shaded.common.response.TimeSkewResponse;
import org.apache.cassandra.spark.bulkwriter.token.TokenRangeMapping;
import org.apache.cassandra.spark.exception.TimeSkewTooLargeException;
@@ -70,11 +80,55 @@ public class CassandraClusterInfoTest
"clusterId=null");
}
+ static Stream<Arguments> sidecarResponseDelays()
+ {
+ return Stream.of(
+ Arguments.of((Object) new int[] {100, 200, 300}), // all responses
within deadline
+ Arguments.of((Object) new int[] {500, 3000}) // single timeout
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("sidecarResponseDelays")
+ @Timeout(value = 2300, unit = TimeUnit.MILLISECONDS) // set timeout
slightly higher than deadline of (1000 + 100) * 2
+ void testSuccessfulGetAllNodeSettings(int[] responseDelayMillis)
+ {
+ BulkSparkConf conf = mockBulkSparkWithSidecarConf(1, 100, 2);
+ try (CassandraClusterInfo ci = new
MockClusterInfoForNodeSettings(conf, responseDelayMillis))
+ {
+ assertThatNoException()
+ .describedAs("Accept when at least one node responds within total
timeout")
+ .isThrownBy(ci::getAllNodeSettings);
+ }
+ }
+
+ @Test
+ void testTimeoutGetAllNodeSettings()
+ {
+ BulkSparkConf conf = mockBulkSparkWithSidecarConf(1, 100, 2);
+ try (CassandraClusterInfo ci = new
MockClusterInfoForNodeSettings(conf, 3000, 3300))
+ {
+ assertThatThrownBy(ci::getAllNodeSettings)
+ .describedAs("Raise error when no responses received within
timeout")
+ .isExactlyInstanceOf(RuntimeException.class)
+ .hasMessage("Unable to determine the node settings. 0/2 instances
available.");
+ }
+ }
+
public static CassandraClusterInfo mockClusterInfoForTimeSkewTest(int
allowanceMinutes, Instant remoteNow)
{
return new MockClusterInfoForTimeSkew(allowanceMinutes, remoteNow);
}
+ private BulkSparkConf mockBulkSparkWithSidecarConf(int
requestTimeoutSeconds, long maxRetryDelayMillis, int retryCount)
+ {
+ BulkSparkConf conf = mock(BulkSparkConf.class);
+
when(conf.getSidecarRequestTimeoutSeconds()).thenReturn(requestTimeoutSeconds);
+
when(conf.getSidecarRequestMaxRetryDelayMillis()).thenReturn(maxRetryDelayMillis);
+ when(conf.getSidecarRequestRetries()).thenReturn(retryCount);
+ return conf;
+ }
+
private static class MockClusterInfoForTimeSkew extends
CassandraClusterInfo
{
private CassandraContext cassandraContext;
@@ -107,4 +161,30 @@ public class CassandraClusterInfoTest
when(cassandraContext.sidecarPort()).thenReturn(9043);
}
}
+
+ private static class MockClusterInfoForNodeSettings extends
CassandraClusterInfo
+ {
+ MockClusterInfoForNodeSettings(BulkSparkConf conf, int...
responseDelayMillis)
+ {
+ super(conf);
+
+ allNodeSettingFutures.clear();
+ List<CompletableFuture<NodeSettings>> futures = new
ArrayList<>(responseDelayMillis.length);
+ for (int delay : responseDelayMillis)
+ {
+ CompletableFuture<NodeSettings> future =
CompletableFuture.supplyAsync(() -> {
+ Uninterruptibles.sleepUninterruptibly(delay,
TimeUnit.MILLISECONDS);
+ return mock(NodeSettings.class);
+ });
+ futures.add(future);
+ }
+ allNodeSettingFutures.addAll(futures);
+ }
+
+ @Override
+ protected CassandraContext buildCassandraContext()
+ {
+ return mock(CassandraContext.class);
+ }
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]