This is an automated email from the ASF dual-hosted git repository.

dcapwell pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 386183fce1 Fix off-by-one bug in exponential backoff for repair retry 
config
386183fce1 is described below

commit 386183fce11707f176b736d92e50cb75ad0680b8
Author: Nivy Kani <[email protected]>
AuthorDate: Thu Jan 8 14:27:36 2026 -0800

    Fix off-by-one bug in exponential backoff for repair retry config
    
    patch by Nivy Kani; reviewed by David Capwell, Jyothsna Konisa for 
CASSANDRA-21102
---
 CHANGES.txt                                        |  1 +
 .../apache/cassandra/service/RetryStrategy.java    |  2 +-
 .../apache/cassandra/service/TimeoutStrategy.java  |  3 +-
 .../cassandra/service/TimeoutStrategyTest.java     | 75 ++++++++++++++++++++++
 4 files changed, 79 insertions(+), 2 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 577b2ee1b5..7937add486 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
 5.1
+ * Fix off-by-one bug in exponential backoff for repair retry config 
(CASSANDRA-21102)
  * Move training parameters for Zstd dictionary compression to CQL 
(CASSANDRA-21078)
  * Add configuration for sorted imports in source files (CASSANDRA-17925)
  * Change the eager reference counting of compression dictionaries to lazy 
(CASSANDRA-21074)
diff --git a/src/java/org/apache/cassandra/service/RetryStrategy.java 
b/src/java/org/apache/cassandra/service/RetryStrategy.java
index ba1b01dfc8..e11c507bf5 100644
--- a/src/java/org/apache/cassandra/service/RetryStrategy.java
+++ b/src/java/org/apache/cassandra/service/RetryStrategy.java
@@ -66,7 +66,7 @@ import static 
org.apache.cassandra.utils.Clock.Global.nanoTime;
  * <li> dynamic constant    {@code pX() * constant}
  * <li> dynamic linear      {@code pX() * constant * attempts}
  * <li> dynamic exponential {@code pX() * constant ^ attempts}
- *
+ * <li> Note: for dynamic exponential, attempts is subtracted by 1, such that 
times begin at {@code pX() * constant}.
  * e.g.
  * <li> {@code 10ms <= p50(rw)*0.66...p99(rw)}
  * <li> {@code 10ms <= p95(rw)*1.8^attempts <= 100ms}
diff --git a/src/java/org/apache/cassandra/service/TimeoutStrategy.java 
b/src/java/org/apache/cassandra/service/TimeoutStrategy.java
index 0c2e64d49a..68be0553d7 100644
--- a/src/java/org/apache/cassandra/service/TimeoutStrategy.java
+++ b/src/java/org/apache/cassandra/service/TimeoutStrategy.java
@@ -98,7 +98,8 @@ public class TimeoutStrategy implements WaitStrategy
         default LatencyModifier identity() { return (l, a) -> l; }
         default LatencyModifier multiply(double constant) { return (l, a) -> 
saturatedCast(l * constant); }
         default LatencyModifier multiplyByAttempts(double multiply) { return 
(l, a) -> saturatedCast(l * multiply * a); }
-        default LatencyModifier multiplyByAttemptsExp(double base) { return 
(l, a) -> saturatedCast(l * pow(base, a)); }
+        // Ensure attempts is non-negative before subtracting 1.
+        default LatencyModifier multiplyByAttemptsExp(double base) { return 
(l, a) -> saturatedCast(l * pow(base, max(0, (max(a, 0) - 1)))); }
     }
 
     public interface Wait
diff --git a/test/unit/org/apache/cassandra/service/TimeoutStrategyTest.java 
b/test/unit/org/apache/cassandra/service/TimeoutStrategyTest.java
new file mode 100644
index 0000000000..fe3b7d8186
--- /dev/null
+++ b/test/unit/org/apache/cassandra/service/TimeoutStrategyTest.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.service;
+
+import java.util.concurrent.TimeUnit;
+
+import org.assertj.core.api.Assertions;
+import org.junit.Test;
+
+public class TimeoutStrategyTest
+{
+
+    @Test
+    public void testParseLatencyModifierExponential()
+    {
+        long expectedBaseLatencyMicros = TimeUnit.MILLISECONDS.toMicros(30);
+        String spec = "30ms * 2^attempts";
+        TimeoutStrategy.Wait w = TimeoutStrategy.parseWait(spec, 
TimeoutStrategy.LatencySourceFactory.none());
+
+        // Attempt 1: baseLatency * 2^(1-1) = baseLatency * 1
+        Assertions.assertThat(w.getMicros(1))
+                .isEqualTo(expectedBaseLatencyMicros);
+
+        // Attempt 2: baseLatency * 2^(2-1) = baseLatency * 2
+        Assertions.assertThat(w.getMicros(2))
+                .isEqualTo(expectedBaseLatencyMicros * 2);
+
+        // Attempt 3: baseLatency * 2^(3-1) = baseLatency * 4
+        Assertions.assertThat(w.getMicros(3))
+                .isEqualTo(expectedBaseLatencyMicros * 4);
+
+        // Edge case to check for 0 or negative attempts: max(0, -1) = 0
+        Assertions.assertThat(w.getMicros(0))
+                .isEqualTo(expectedBaseLatencyMicros);
+
+        Assertions.assertThat(w.getMicros(Integer.MIN_VALUE))
+                .isEqualTo(expectedBaseLatencyMicros);
+    }
+
+    @Test
+    public void testParseLatencyModifierFractionalBaseExponential()
+    {
+        long expectedBaseLatencyMicros = TimeUnit.MILLISECONDS.toMicros(30);
+        String spec = "30ms * 1.5^attempts";
+        TimeoutStrategy.Wait w = TimeoutStrategy.parseWait(spec, 
TimeoutStrategy.LatencySourceFactory.none());
+
+        // Attempt 1: baseLatency * 1.5^(1-1) = baseLatency * 1
+        Assertions.assertThat(w.getMicros(1))
+                .isEqualTo((int) expectedBaseLatencyMicros);
+
+        // Attempt 2: baseLatency * 1.5^(2-1) = baseLatency * 1.5
+        Assertions.assertThat(w.getMicros(2))
+                .isEqualTo((int) (expectedBaseLatencyMicros * 1.5));
+
+        // Attempt 3: baseLatency * 1.5^(3-1) = baseLatency * 2.25
+        Assertions.assertThat(w.getMicros(3))
+                .isEqualTo((int) (expectedBaseLatencyMicros * 2.25));
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to