Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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