Skip to content
174 changes: 174 additions & 0 deletions driver-core/src/main/com/mongodb/internal/ExponentialBackoff.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/*
* Copyright 2008-present MongoDB, Inc.
*
* Licensed 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 com.mongodb.internal;

import com.mongodb.annotations.NotThreadSafe;

import java.util.concurrent.ThreadLocalRandom;

/**
* Implements exponential backoff with jitter for retry scenarios.
* Formula: delayMS = jitter * min(maxBackoffMs, baseBackoffMs * growthFactor^retryCount)
* where jitter is random value [0, 1).
*
* <p>This class provides factory methods for common use cases:
* <ul>
* <li>{@link #forTransactionRetry()} - For withTransaction retries (5ms base, 500ms max, 1.5 growth)</li>
* <li>{@link #forCommandRetry()} - For command retries with overload (100ms base, 10000ms max, 2.0 growth)</li>
* </ul>
*/
@NotThreadSafe
public final class ExponentialBackoff {
// Transaction retry constants (per spec)
private static final double TRANSACTION_BASE_BACKOFF_MS = 5.0;
private static final double TRANSACTION_MAX_BACKOFF_MS = 500.0;
private static final double TRANSACTION_BACKOFF_GROWTH = 1.5;

// Command retry constants (per spec)
private static final double COMMAND_BASE_BACKOFF_MS = 100.0;
private static final double COMMAND_MAX_BACKOFF_MS = 10000.0;
private static final double COMMAND_BACKOFF_GROWTH = 2.0;

private final double baseBackoffMs;
private final double maxBackoffMs;
private final double growthFactor;
private int retryCount = 0;

/**
* Creates an exponential backoff instance with specified parameters.
*
* @param baseBackoffMs Initial backoff in milliseconds
* @param maxBackoffMs Maximum backoff cap in milliseconds
* @param growthFactor Exponential growth factor (e.g., 1.5 or 2.0)
*/
public ExponentialBackoff(final double baseBackoffMs, final double maxBackoffMs, final double growthFactor) {
this.baseBackoffMs = baseBackoffMs;
this.maxBackoffMs = maxBackoffMs;
this.growthFactor = growthFactor;
}

/**
* Creates a backoff instance configured for withTransaction retries.
* Uses: 5ms base, 500ms max, 1.5 growth factor.
*
* @return ExponentialBackoff configured for transaction retries
*/
public static ExponentialBackoff forTransactionRetry() {
return new ExponentialBackoff(
TRANSACTION_BASE_BACKOFF_MS,
TRANSACTION_MAX_BACKOFF_MS,
TRANSACTION_BACKOFF_GROWTH
);
}

/**
* Creates a backoff instance configured for command retries during overload.
* Uses: 100ms base, 10000ms max, 2.0 growth factor.
*
* @return ExponentialBackoff configured for command retries
*/
public static ExponentialBackoff forCommandRetry() {
return new ExponentialBackoff(
COMMAND_BASE_BACKOFF_MS,
COMMAND_MAX_BACKOFF_MS,
COMMAND_BACKOFF_GROWTH
);
}

/**
* Calculate next backoff delay with jitter.
*
* @return delay in milliseconds
*/
public long calculateDelayMs() {
double jitter = ThreadLocalRandom.current().nextDouble();
double exponentialBackoff = baseBackoffMs * Math.pow(growthFactor, retryCount);
double cappedBackoff = Math.min(exponentialBackoff, maxBackoffMs);
retryCount++;
return Math.round(jitter * cappedBackoff);
}

/**
* Apply backoff delay by sleeping current thread.
*
* @throws InterruptedException if thread is interrupted during sleep
*/
public void applyBackoff() throws InterruptedException {
long delayMs = calculateDelayMs();
if (delayMs > 0) {
Thread.sleep(delayMs);
}
}

/**
* Check if applying backoff would exceed the retry time limit.
* @param startTimeMs start time of retry attempts
* @param maxRetryTimeMs maximum retry time allowed
* @return true if backoff would exceed limit, false otherwise
*/
// public boolean wouldExceedTimeLimit(final long startTimeMs, final long maxRetryTimeMs) {
// long elapsedMs = ClientSessionClock.INSTANCE.now() - startTimeMs;
// // Peek at next delay without incrementing counter
// double exponentialBackoff = baseBackoffMs * Math.pow(growthFactor, retryCount);
// double cappedBackoff = Math.min(exponentialBackoff, maxBackoffMs);
// long maxPossibleDelay = Math.round(cappedBackoff); // worst case with jitter=1
// return elapsedMs + maxPossibleDelay > maxRetryTimeMs;
// }

/**
* Reset retry counter for new sequence of retries.
*/
public void reset() {
retryCount = 0;
}

/**
* Get current retry count for testing.
*
* @return current retry count
*/
public int getRetryCount() {
return retryCount;
}

/**
* Get the base backoff in milliseconds.
*
* @return base backoff
*/
public double getBaseBackoffMs() {
return baseBackoffMs;
}

/**
* Get the maximum backoff in milliseconds.
*
* @return maximum backoff
*/
public double getMaxBackoffMs() {
return maxBackoffMs;
}

/**
* Get the growth factor.
*
* @return growth factor
*/
public double getGrowthFactor() {
return growthFactor;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
/*
* Copyright 2008-present MongoDB, Inc.
*
* Licensed 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 com.mongodb.internal;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class ExponentialBackoffTest {

@Test
void testTransactionRetryBackoff() {
ExponentialBackoff backoff = ExponentialBackoff.forTransactionRetry();

// Verify configuration
assertEquals(5.0, backoff.getBaseBackoffMs());
assertEquals(500.0, backoff.getMaxBackoffMs());
assertEquals(1.5, backoff.getGrowthFactor());

// First retry (i=0): delay = jitter * min(5 * 1.5^0, 500) = jitter * 5
// Since jitter is random [0,1), the delay should be between 0 and 5ms
long delay1 = backoff.calculateDelayMs();
assertTrue(delay1 >= 0 && delay1 <= 5, "First delay should be 0-5ms, got: " + delay1);

// Second retry (i=1): delay = jitter * min(5 * 1.5^1, 500) = jitter * 7.5
long delay2 = backoff.calculateDelayMs();
assertTrue(delay2 >= 0 && delay2 <= 8, "Second delay should be 0-8ms, got: " + delay2);

// Third retry (i=2): delay = jitter * min(5 * 1.5^2, 500) = jitter * 11.25
long delay3 = backoff.calculateDelayMs();
assertTrue(delay3 >= 0 && delay3 <= 12, "Third delay should be 0-12ms, got: " + delay3);

// Verify the retry count is incrementing properly
assertEquals(3, backoff.getRetryCount());
}

@Test
void testTransactionRetryBackoffRespectsMaximum() {
ExponentialBackoff backoff = ExponentialBackoff.forTransactionRetry();

// Advance to a high retry count where backoff would exceed 500ms without capping
for (int i = 0; i < 20; i++) {
backoff.calculateDelayMs();
}

// Even at high retry counts, delay should never exceed 500ms
for (int i = 0; i < 5; i++) {
long delay = backoff.calculateDelayMs();
assertTrue(delay >= 0 && delay <= 500, "Delay should be capped at 500ms, got: " + delay);
}
}

@Test
void testTransactionRetryBackoffSequenceWithExpectedValues() {
// Test that the backoff sequence follows the expected pattern with growth factor 1.5
// Expected sequence (without jitter): 5, 7.5, 11.25, 16.875, 25.3125, 37.96875, 56.953125, ...
// With jitter, actual values will be between 0 and these maxima

ExponentialBackoff backoff = ExponentialBackoff.forTransactionRetry();

double[] expectedMaxValues = {5.0, 7.5, 11.25, 16.875, 25.3125, 37.96875, 56.953125, 85.4296875,
128.14453125, 192.21679688, 288.32519531, 432.48779297, 500.0};

for (int i = 0; i < expectedMaxValues.length; i++) {
long delay = backoff.calculateDelayMs();
assertTrue(delay >= 0 && delay <= Math.round(expectedMaxValues[i]),
String.format("Retry %d: delay should be 0-%d ms, got: %d", i, Math.round(expectedMaxValues[i]), delay));
}
}

@Test
void testCommandRetryBackoff() {
ExponentialBackoff backoff = ExponentialBackoff.forCommandRetry();

// Verify configuration
assertEquals(100.0, backoff.getBaseBackoffMs());
assertEquals(10000.0, backoff.getMaxBackoffMs());
assertEquals(2.0, backoff.getGrowthFactor());

// Test sequence with growth factor 2.0
// Expected max delays: 100, 200, 400, 800, 1600, 3200, 6400, 10000 (capped)
long delay1 = backoff.calculateDelayMs();
assertTrue(delay1 >= 0 && delay1 <= 100, "First delay should be 0-100ms, got: " + delay1);

long delay2 = backoff.calculateDelayMs();
assertTrue(delay2 >= 0 && delay2 <= 200, "Second delay should be 0-200ms, got: " + delay2);

long delay3 = backoff.calculateDelayMs();
assertTrue(delay3 >= 0 && delay3 <= 400, "Third delay should be 0-400ms, got: " + delay3);

long delay4 = backoff.calculateDelayMs();
assertTrue(delay4 >= 0 && delay4 <= 800, "Fourth delay should be 0-800ms, got: " + delay4);

long delay5 = backoff.calculateDelayMs();
assertTrue(delay5 >= 0 && delay5 <= 1600, "Fifth delay should be 0-1600ms, got: " + delay5);
}

@Test
void testCommandRetryBackoffRespectsMaximum() {
ExponentialBackoff backoff = ExponentialBackoff.forCommandRetry();

// Advance to where exponential would exceed 10000ms
for (int i = 0; i < 10; i++) {
backoff.calculateDelayMs();
}

// Even at high retry counts, delay should never exceed 10000ms
for (int i = 0; i < 5; i++) {
long delay = backoff.calculateDelayMs();
assertTrue(delay >= 0 && delay <= 10000, "Delay should be capped at 10000ms, got: " + delay);
}
}

@Test
void testCustomBackoff() {
// Test with custom parameters
ExponentialBackoff backoff = new ExponentialBackoff(50.0, 2000.0, 1.8);

assertEquals(50.0, backoff.getBaseBackoffMs());
assertEquals(2000.0, backoff.getMaxBackoffMs());
assertEquals(1.8, backoff.getGrowthFactor());

// First delay: 0-50ms
long delay1 = backoff.calculateDelayMs();
assertTrue(delay1 >= 0 && delay1 <= 50, "First delay should be 0-50ms, got: " + delay1);

// Second delay: 0-90ms (50 * 1.8)
long delay2 = backoff.calculateDelayMs();
assertTrue(delay2 >= 0 && delay2 <= 90, "Second delay should be 0-90ms, got: " + delay2);
}

@Test
void testReset() {
ExponentialBackoff backoff = ExponentialBackoff.forTransactionRetry();

// Perform some retries
backoff.calculateDelayMs();
backoff.calculateDelayMs();
assertEquals(2, backoff.getRetryCount());

// Reset and verify counter is back to 0
backoff.reset();
assertEquals(0, backoff.getRetryCount());

// First delay after reset should be in the initial range again
long delay = backoff.calculateDelayMs();
assertTrue(delay >= 0 && delay <= 5, "First delay after reset should be 0-5ms, got: " + delay);
}

// @Test
// void testWouldExceedTimeLimitTransactionRetry() {
// ExponentialBackoff backoff = ExponentialBackoff.forTransactionRetry();
// long startTime = ClientSessionClock.INSTANCE.now();
//
// // Initially, should not exceed time limit
// assertFalse(backoff.wouldExceedTimeLimit(startTime, 120000));
//
// // With very little time remaining (4ms), first backoff (up to 5ms) would exceed
// long nearLimitTime = startTime - 119996; // 4ms remaining
// assertTrue(backoff.wouldExceedTimeLimit(nearLimitTime, 120000));
// }

// @Test
// void testWouldExceedTimeLimitCommandRetry() {
// ExponentialBackoff backoff = ExponentialBackoff.forCommandRetry();
// long startTime = ClientSessionClock.INSTANCE.now();
//
// // Initially, should not exceed time limit
// assertFalse(backoff.wouldExceedTimeLimit(startTime, 10000));
//
// // With 99ms remaining, first backoff (up to 100ms) would exceed
// long nearLimitTime = startTime - 9901; // 99ms remaining
// assertTrue(backoff.wouldExceedTimeLimit(nearLimitTime, 10000));
// }

@Test
void testCommandRetrySequenceMatchesSpec() {
// Test that command retry follows spec: 100ms * 2^i capped at 10000ms
ExponentialBackoff backoff = ExponentialBackoff.forCommandRetry();

double[] expectedMaxValues = {100.0, 200.0, 400.0, 800.0, 1600.0, 3200.0, 6400.0, 10000.0, 10000.0};

for (int i = 0; i < expectedMaxValues.length; i++) {
long delay = backoff.calculateDelayMs();
double expectedMax = expectedMaxValues[i];
assertTrue(delay >= 0 && delay <= Math.round(expectedMax),
String.format("Retry %d: delay should be 0-%d ms, got: %d", i, Math.round(expectedMax), delay));
}
}
}
Loading