Skip to content

Commit

Permalink
Added RetryPolicy api
Browse files Browse the repository at this point in the history
RetryPolicy Proposal
  • Loading branch information
alex268 authored Jan 22, 2024
2 parents b67d3a7 + 93ca5ac commit e55364b
Show file tree
Hide file tree
Showing 7 changed files with 358 additions and 0 deletions.
30 changes: 30 additions & 0 deletions core/src/main/java/tech/ydb/core/ErrorPolicy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package tech.ydb.core;

/**
* Recipes should use the configured error policy to decide how to retry
* errors like unsuccessful {@link tech.ydb.core.StatusCode}.
*
* @author Aleksandr Gorshenin
* @param <T> Type of errors to check
*/
public interface ErrorPolicy<T> {

/**
* Returns true if the given value should be retried
*
* @param value value to check
* @return true if value is retryable
*/
boolean isRetryable(T value);

/**
* Returns true if the given exception should be retried
* Usually exceptions are never retried, but some policies can implement more difficult logic
*
* @param ex exception to check
* @return true if exception is retryable
*/
default boolean isRetryable(Exception ex) {
return false;
}
}
23 changes: 23 additions & 0 deletions core/src/main/java/tech/ydb/core/RetryPolicy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package tech.ydb.core;

/**
* Abstracts the policy to use when retrying some actions
*
* @author Aleksandr Gorshenin
*/
public interface RetryPolicy {
/**
* Called when an operation is failed for some reason to determine if it should be retried.
* And if so, returns the delay to make the next retry attempt after
*
* @param retryCount the number of times retried so far (0 the first time)
* @param elapsedTimeMs the elapsed time in ms since the operation was attempted
* @return delay for the next retry
* <ul>
* <li>Positive number N - operation must be retried in N milliseconds </li>
* <li>Zero : operation must be retried immediately </li>
* <li>Negative number : retry is not allowed, operation must be failed </li>
* </ul>
*/
long nextRetryMs(int retryCount, long elapsedTimeMs);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package tech.ydb.core.retry;

import java.util.concurrent.ThreadLocalRandom;

import tech.ydb.core.RetryPolicy;

/**
*
* @author Aleksandr Gorshenin
*/
public abstract class ExponentialBackoffRetry implements RetryPolicy {
private final long backoffMs;
private final int backoffCeiling;

protected ExponentialBackoffRetry(long backoffMs, int backoffCeiling) {
this.backoffMs = backoffMs;
this.backoffCeiling = backoffCeiling;
}

protected long backoffTimeMillis(int retryNumber) {
int slots = 1 << Math.min(retryNumber, backoffCeiling);
long delay = backoffMs * slots;
return delay + ThreadLocalRandom.current().nextLong(delay);
}

/**
* Return current base of backoff delays
* @return backoff base duration in milliseconds
*/
public long getBackoffMillis() {
return backoffMs;
}

/**
* Return current maximal level of backoff exponent
* @return maximal level of backoff exponent
*/
public int getBackoffCeiling() {
return backoffCeiling;
}
}
36 changes: 36 additions & 0 deletions core/src/main/java/tech/ydb/core/retry/RetryForever.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package tech.ydb.core.retry;

import tech.ydb.core.RetryPolicy;

/**
*
* @author Aleksandr Gorshenin
*/
public class RetryForever implements RetryPolicy {
private final long intervalMs;

public RetryForever(long intervalMs) {
this.intervalMs = intervalMs;
}

@Override
public long nextRetryMs(int retryCount, long elapsedTimeMs) {
return intervalMs;
}

/**
* Return current interval of retries
* @return retry interval in milliseconds
*/
public long getIntervalMillis() {
return intervalMs;
}

/**
* Create new retry policy with specified retry interval
* @param ms new interval in milliseconds
* @return updated retry policy */
public RetryForever withIntervalMs(long ms) {
return new RetryForever(ms);
}
}
54 changes: 54 additions & 0 deletions core/src/main/java/tech/ydb/core/retry/RetryNTimes.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package tech.ydb.core.retry;

/**
*
* @author Aleksandr Gorshenin
*/
public class RetryNTimes extends ExponentialBackoffRetry {
private final int maxRetries;

public RetryNTimes(int maxRetries, long backoffMs, int backoffCeiling) {
super(backoffMs, backoffCeiling);
this.maxRetries = maxRetries;
}

@Override
public long nextRetryMs(int retryCount, long elapsedTimeMs) {
if (retryCount >= maxRetries) {
return -1;
}
return backoffTimeMillis(retryCount);
}

/**
* Return maximal count of retries
* @return maximal count of retries
*/
public int getMaxRetries() {
return maxRetries;
}

/**
* Create new retry policy with specified max retries count
* @param maxRetries new value of max count of retries
* @return updated retry policy */
public RetryNTimes withMaxRetries(int maxRetries) {
return new RetryNTimes(maxRetries, getBackoffMillis(), getBackoffCeiling());
}

/**
* Create new retry policy with specified backoff duration
* @param ms new backoff duration in milliseconds
* @return updated retry policy */
public RetryNTimes withBackoffMs(long ms) {
return new RetryNTimes(maxRetries, ms, getBackoffCeiling());
}

/**
* Create new retry policy with specified backoff ceiling
* @param ceiling new backoff ceiling
* @return updated retry policy */
public RetryNTimes withBackoffCeiling(int ceiling) {
return new RetryNTimes(maxRetries, getBackoffMillis(), ceiling);
}
}
55 changes: 55 additions & 0 deletions core/src/main/java/tech/ydb/core/retry/RetryUntilElapsed.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package tech.ydb.core.retry;

/**
*
* @author Aleksandr Gorshenin
*/
public class RetryUntilElapsed extends ExponentialBackoffRetry {
private final long maxElapsedMs;

public RetryUntilElapsed(long maxElapsedMs, long backoffMs, int backoffCeiling) {
super(backoffMs, backoffCeiling);
this.maxElapsedMs = maxElapsedMs;
}

@Override
public long nextRetryMs(int retryCount, long elapsedTimeMs) {
if (maxElapsedMs <= elapsedTimeMs) {
return -1;
}
long backoff = backoffTimeMillis(retryCount);
return (elapsedTimeMs + backoff < maxElapsedMs) ? backoff : maxElapsedMs - elapsedTimeMs;
}

/**
* Return maximal count of elapsed milliseconds
* @return maximal count of elapsed milliseconds
*/
public long getMaxElapsedMillis() {
return maxElapsedMs;
}

/**
* Create new retry policy with specified count of elapsed milliseconds
* @param maxElapsedMs new value of max elapsed milliseconds
* @return updated retry policy */
public RetryUntilElapsed withMaxElapsedMs(long maxElapsedMs) {
return new RetryUntilElapsed(maxElapsedMs, getBackoffMillis(), getBackoffCeiling());
}

/**
* Create new retry policy with specified backoff duration
* @param ms new backoff duration
* @return new retry policy */
public RetryUntilElapsed withBackoffMs(long ms) {
return new RetryUntilElapsed(maxElapsedMs, ms, getBackoffCeiling());
}

/**
* Create new retry policy with specified backoff ceiling
* @param ceiling new backoff ceiling
* @return new retry policy */
public RetryUntilElapsed withBackoffCeiling(int ceiling) {
return new RetryUntilElapsed(maxElapsedMs, getBackoffMillis(), ceiling);
}
}
119 changes: 119 additions & 0 deletions core/src/test/java/tech/ydb/core/retry/RetryPoliciesTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package tech.ydb.core.retry;

import org.junit.Assert;
import org.junit.Test;

/**
*
* @author Aleksandr Gorshenin
*/
public class RetryPoliciesTest {

@Test
public void foreverRetryTest() {
RetryForever policy = new RetryForever(1234);

Assert.assertEquals(1234, policy.nextRetryMs(0, 0));
Assert.assertEquals(1234, policy.nextRetryMs(Integer.MAX_VALUE, 0));
Assert.assertEquals(1234, policy.nextRetryMs(0, Integer.MAX_VALUE));
Assert.assertEquals(1234, policy.nextRetryMs(Integer.MAX_VALUE, Integer.MAX_VALUE));
}

@Test
public void foreverUpdateTest() {
RetryForever policy = new RetryForever(50);
Assert.assertEquals(50, policy.getIntervalMillis());
Assert.assertEquals(50, policy.nextRetryMs(0, 0));

RetryForever updated = policy.withIntervalMs(150);
Assert.assertEquals(150, updated.getIntervalMillis());
Assert.assertEquals(150, updated.nextRetryMs(0, 0));
}

@Test
public void zeroRetriesTest() {
RetryNTimes policy = new RetryNTimes(0, 100, 3);

Assert.assertEquals(-1, policy.nextRetryMs(0, 0));
Assert.assertEquals(-1, policy.nextRetryMs(Integer.MAX_VALUE, 0));
Assert.assertEquals(-1, policy.nextRetryMs(0, Integer.MAX_VALUE));
Assert.assertEquals(-1, policy.nextRetryMs(Integer.MAX_VALUE, Integer.MAX_VALUE));
}

@Test
public void nRetriesTest() {
RetryNTimes policy = new RetryNTimes(5, 100, 3);

assertDuration(100, 200, policy.nextRetryMs(0, 0));
assertDuration(200, 400, policy.nextRetryMs(1, 150));
assertDuration(400, 800, policy.nextRetryMs(2, 400));
assertDuration(800, 1600, policy.nextRetryMs(3, 1600));
assertDuration(800, 1600, policy.nextRetryMs(4, 2800));

Assert.assertEquals(-1, policy.nextRetryMs(5, 4000));
}

@Test
public void updateNRetriesTest() {
RetryNTimes policy = new RetryNTimes(5, 100, 3);

Assert.assertEquals(5, policy.getMaxRetries());
Assert.assertEquals(100, policy.getBackoffMillis());
Assert.assertEquals(3, policy.getBackoffCeiling());
assertDuration(100, 200, policy.nextRetryMs(0, 0));

RetryNTimes updated = policy.withMaxRetries(4).withBackoffMs(150).withBackoffCeiling(1);

Assert.assertEquals(4, updated.getMaxRetries());
Assert.assertEquals(150, updated.getBackoffMillis());
Assert.assertEquals(1, updated.getBackoffCeiling());
assertDuration(150, 300, updated.nextRetryMs(0, 0));
}

@Test
public void zeroElapsedTest() {
RetryUntilElapsed policy = new RetryUntilElapsed(0, 100, 3);

Assert.assertEquals(-1, policy.nextRetryMs(0, 0));
Assert.assertEquals(-1, policy.nextRetryMs(Integer.MAX_VALUE, 0));
Assert.assertEquals(-1, policy.nextRetryMs(0, Integer.MAX_VALUE));
Assert.assertEquals(-1, policy.nextRetryMs(Integer.MAX_VALUE, Integer.MAX_VALUE));
}

@Test
public void untilElapsedTest() {
RetryUntilElapsed policy = new RetryUntilElapsed(2500, 50, 3);

assertDuration(50, 100, policy.nextRetryMs(0, 0));
assertDuration(100, 200, policy.nextRetryMs(1, 75));
assertDuration(200, 400, policy.nextRetryMs(2, 225));
assertDuration(400, 800, policy.nextRetryMs(3, 525));
assertDuration(400, 800, policy.nextRetryMs(4, 1125));
assertDuration(400, 800, policy.nextRetryMs(5, 1725));

Assert.assertEquals(175, policy.nextRetryMs(6, 2325));
Assert.assertEquals(-1, policy.nextRetryMs(7, 2500));
}

@Test
public void updateElapsedTest() {
RetryUntilElapsed policy = new RetryUntilElapsed(2500, 50, 3);

Assert.assertEquals(2500, policy.getMaxElapsedMillis());
Assert.assertEquals(50, policy.getBackoffMillis());
Assert.assertEquals(3, policy.getBackoffCeiling());
assertDuration(50, 100, policy.nextRetryMs(0, 0));

RetryUntilElapsed updated = policy.withMaxElapsedMs(1000).withBackoffMs(100).withBackoffCeiling(1);

Assert.assertEquals(1000, updated.getMaxElapsedMillis());
Assert.assertEquals(100, updated.getBackoffMillis());
Assert.assertEquals(1, updated.getBackoffCeiling());
assertDuration(100, 200, updated.nextRetryMs(0, 0));
}

private void assertDuration(long from, long to, long ms) {
Assert.assertTrue(from <= ms);
Assert.assertTrue(to >= ms);
}
}

0 comments on commit e55364b

Please sign in to comment.