[aether-resolver] IJI-781 Create retry implementations

* Disabled - no retries, one attempt to perform a job
* With exponential back off

(cherry picked from commit 115a930ea325d799c36a9d2847dea90b5559fc6f)

IJ-MR-20326

GitOrigin-RevId: 44af9f2964732d33d962b48d6732df9c3e81693c
This commit is contained in:
Vladislav Yaroshchuk
2021-12-31 04:30:38 +03:00
committed by intellij-monorepo-bot
parent bcc925993f
commit 6034e60ed0
2 changed files with 216 additions and 0 deletions

View File

@@ -0,0 +1,120 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.idea.maven.aether;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import java.util.Random;
/**
* Retry utils can be used to set up dependency resolver
*/
public final class RetryProvider {
/* Exponential backoff retry requirements */
private static final Random RANDOM = new Random();
private static final double EXP_BACKOFF_FACTOR = 2;
private static final double EXP_BACKOFF_JITTER = 0.3;
private static final Retry DISABLED_SINGLETON = new Retry() {
@Override
public <R> R retry(@NotNull ThrowingSupplier<? extends R> supplier, @NotNull Logger logger) throws Exception {
return supplier.get();
}
};
/**
* Get disabled Retry.
*
* @return Retry implementation that tries only once to perform a task.
*/
public static Retry disabled() {
return DISABLED_SINGLETON;
}
/**
* Get retry with exponential back off.
*
* @param initialDelayMs Delay before the first retry after fail in milliseconds.
* @param backoffLimitMs Limit of delay should not grow upper than in milliseconds.
* @param maxAttempts Max attempts to do a job.
* @return Retry implementation with delay between attempts.
*/
public static Retry withExponentialBackOff(long initialDelayMs, long backoffLimitMs, int maxAttempts) {
if (initialDelayMs <= 0 || backoffLimitMs <= 0 || maxAttempts <= 0) {
throw new IllegalArgumentException(
"Wrong arguments provided: initialDelayMs=" + initialDelayMs + "backoffLimitMs=" + backoffLimitMs + " maxAttempts=" + maxAttempts);
}
return new Retry() {
@Override
public <R> R retry(@NotNull ThrowingSupplier<? extends R> supplier, @NotNull Logger logger) throws Exception {
return exponentialBackOffRetry(initialDelayMs, backoffLimitMs, maxAttempts, supplier, logger);
}
};
}
/**
* Utility class - should not be instantiated
*/
private RetryProvider() {
}
/**
* Retry with exponential back off implementation.
*
* @param initialDelayMs Delay before the first retry after fail in milliseconds.
* @param backoffLimitMs Limit of delay should not grow upper than in milliseconds.
* @param maxAttempts Max attempts to do a job.
* @param supplier Supplies that does some possibly throwing work.
* @param logger Messages logger.
* @param <R> Supplier result type.
* @return Result from supplier.
* @throws Exception An error the job thrown if attempts limit exceeded.
*/
private static <R> R exponentialBackOffRetry(long initialDelayMs,
long backoffLimitMs,
int maxAttempts,
@NotNull ThrowingSupplier<? extends R> supplier,
@NotNull Logger logger) throws Exception {
long effectiveDelay = initialDelayMs;
for (int i = 1; i <= maxAttempts; i++) {
try {
return supplier.get();
}
catch (Exception e) {
if (i == maxAttempts) {
logger.info("Retry attempts limit exceeded, tried " + maxAttempts + " times. Cause: " + e.getMessage());
throw e;
}
logger.info("Attempt " + i + " of " + maxAttempts + " failed, retrying in " + effectiveDelay + "ms. Cause: " + e.getMessage());
effectiveDelay = exponentialBackOff(effectiveDelay, backoffLimitMs);
}
}
throw new RuntimeException("Should not be reached");
}
/**
* Exponential back off for retry. Sleeps current thread for {@code effectiveDelayMs},
* calculates next delay.
*
* @param effectiveDelayMs Effective delay to sleep.
* @param backoffLimitMs Limit of delay should not grow upper than in milliseconds.
* @return Next effective delay.
*/
private static long exponentialBackOff(long effectiveDelayMs, long backoffLimitMs) {
try {
Thread.sleep(effectiveDelayMs);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new RuntimeException("Unexpected thread interrupt", ex);
}
long nextRawDelay = (long)Math.min(effectiveDelayMs * EXP_BACKOFF_FACTOR, backoffLimitMs);
long jitter = (long)(RANDOM.nextDouble() * nextRawDelay * EXP_BACKOFF_JITTER);
long jitterSign = RANDOM.nextBoolean() ? 1 : -1;
return nextRawDelay + jitter * jitterSign;
}
}

View File

@@ -0,0 +1,96 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.idea.maven.aether;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.jetbrains.idea.maven.aether.RetryProvider.disabled;
import static org.jetbrains.idea.maven.aether.RetryProvider.withExponentialBackOff;
import static org.junit.jupiter.api.Assertions.*;
class RetryProviderTest {
private final Logger logger = LoggerFactory.getLogger(RetryProviderTest.class);
private final Retry retryDisabled = disabled();
private final Retry retryWithExpBackOff = withExponentialBackOff(1000, 5000, 5);
@Test
public void disabled_testIsSingleton() {
Retry first = disabled();
Retry second = disabled();
assertSame(first, second);
}
@Test
public void disabled_testReturnsCorrectValue() throws Exception {
int expected = 42;
int actual = retryDisabled.retry(() -> expected, logger);
assertEquals(expected, actual);
}
@Test
public void disabled_testCanBeReused() throws Exception {
int expected = 42;
int ignored = retryDisabled.retry(() -> expected, logger);
int actual = retryDisabled.retry(() -> expected, logger);
assertEquals(expected, actual);
}
@Test
public void disabled_testRethrowsException() {
String expected = "Value42";
assertThrows(Exception.class, () -> retryDisabled.retry(() -> {
throw new Exception(expected);
}, logger), expected);
}
@Test
public void expBackOff_testThrowsOnIllegalArguments() {
assertThrows(IllegalArgumentException.class, () -> withExponentialBackOff(0, 1, 1));
assertThrows(IllegalArgumentException.class, () -> withExponentialBackOff(1, 0, 1));
assertThrows(IllegalArgumentException.class, () -> withExponentialBackOff(1, 1, 0));
}
@Test
public void expBackOff_testOneAttempt() throws Exception {
int expected = 42;
int actual = retryWithExpBackOff.retry(() -> expected, logger);
assertEquals(expected, actual);
}
@Test
public void expBackOff_testRetry() throws Exception {
int attempts = retryWithExpBackOff.retry(new ThrowingSupplier<Integer>() {
private int attempts = 0;
@Override
public Integer get() throws Exception {
if (attempts == 0) {
/* Simulate a fail */
++attempts;
throw new Exception();
}
return attempts;
}
}, logger);
assertTrue(attempts > 0, "attempts > 0");
}
@Test
public void expBackOff_testCanBeReused() throws Exception {
int expected = 42;
int ignored = retryWithExpBackOff.retry(() -> expected, logger);
int actual = retryWithExpBackOff.retry(() -> expected, logger);
assertEquals(expected, actual);
}
@Test
public void expBackOff_testRethrowsException() {
String expected = "Value42";
assertThrows(Exception.class, () -> retryWithExpBackOff.retry(() -> {
throw new Exception(expected);
}, logger), expected);
}
}