mirror of
https://github.com/tiennm99/java-design-patterns.git
synced 2026-09-03 12:21:56 +00:00
deps: Refactor dependencies (#3224)
* remove spring dep move junit, logging, mockito under dep mgmt * upgrade anti-corruption-layer deps * async method invocation * balking, bloc * bridge to bytecode * caching * callback - cqrs * component - health check * hexagonal - metadata mapping * rest of the patterns * remove checkstyle, take spotless into use
This commit is contained in:
@@ -37,19 +37,19 @@ import org.slf4j.LoggerFactory;
|
||||
* operations that our application performs involving remote systems. The calling code should remain
|
||||
* decoupled from implementations.
|
||||
*
|
||||
* <p>{@link FindCustomer} is a business operation that looks up a customer's record and returns
|
||||
* its ID. Imagine its job is performed by looking up the customer in our local database and
|
||||
* returning its ID. We can pass {@link CustomerNotFoundException} as one of its {@link
|
||||
* <p>{@link FindCustomer} is a business operation that looks up a customer's record and returns its
|
||||
* ID. Imagine its job is performed by looking up the customer in our local database and returning
|
||||
* its ID. We can pass {@link CustomerNotFoundException} as one of its {@link
|
||||
* FindCustomer#FindCustomer(java.lang.String, com.iluwatar.retry.BusinessException...) constructor
|
||||
* parameters} in order to simulate not finding the customer.
|
||||
*
|
||||
* <p>Imagine that, lately, this operation has experienced intermittent failures due to some weird
|
||||
* corruption and/or locking in the data. After retrying a few times the customer is found. The
|
||||
* database is still, however, expected to always be available. While a definitive solution is
|
||||
* found to the problem, our engineers advise us to retry the operation a set number of times with a
|
||||
* set delay between retries, although not too many retries otherwise the end user will be left
|
||||
* waiting for a long time, while delays that are too short will not allow the database to recover
|
||||
* from the load.
|
||||
* database is still, however, expected to always be available. While a definitive solution is found
|
||||
* to the problem, our engineers advise us to retry the operation a set number of times with a set
|
||||
* delay between retries, although not too many retries otherwise the end user will be left waiting
|
||||
* for a long time, while delays that are too short will not allow the database to recover from the
|
||||
* load.
|
||||
*
|
||||
* <p>To keep the calling code as decoupled as possible from this workaround, we have implemented
|
||||
* the retry mechanism as a {@link BusinessOperation} named {@link Retry}.
|
||||
@@ -91,32 +91,34 @@ public final class App {
|
||||
}
|
||||
|
||||
private static void errorWithRetry() throws Exception {
|
||||
final var retry = new Retry<>(
|
||||
new FindCustomer("123", new CustomerNotFoundException(NOT_FOUND)),
|
||||
3, //3 attempts
|
||||
100, //100 ms delay between attempts
|
||||
e -> CustomerNotFoundException.class.isAssignableFrom(e.getClass())
|
||||
);
|
||||
final var retry =
|
||||
new Retry<>(
|
||||
new FindCustomer("123", new CustomerNotFoundException(NOT_FOUND)),
|
||||
3, // 3 attempts
|
||||
100, // 100 ms delay between attempts
|
||||
e -> CustomerNotFoundException.class.isAssignableFrom(e.getClass()));
|
||||
op = retry;
|
||||
final var customerId = op.perform();
|
||||
LOG.info(String.format(
|
||||
"However, retrying the operation while ignoring a recoverable error will eventually yield "
|
||||
+ "the result %s after a number of attempts %s", customerId, retry.attempts()
|
||||
));
|
||||
LOG.info(
|
||||
String.format(
|
||||
"However, retrying the operation while ignoring a recoverable error will eventually yield "
|
||||
+ "the result %s after a number of attempts %s",
|
||||
customerId, retry.attempts()));
|
||||
}
|
||||
|
||||
private static void errorWithRetryExponentialBackoff() throws Exception {
|
||||
final var retry = new RetryExponentialBackoff<>(
|
||||
new FindCustomer("123", new CustomerNotFoundException(NOT_FOUND)),
|
||||
6, //6 attempts
|
||||
30000, //30 s max delay between attempts
|
||||
e -> CustomerNotFoundException.class.isAssignableFrom(e.getClass())
|
||||
);
|
||||
final var retry =
|
||||
new RetryExponentialBackoff<>(
|
||||
new FindCustomer("123", new CustomerNotFoundException(NOT_FOUND)),
|
||||
6, // 6 attempts
|
||||
30000, // 30 s max delay between attempts
|
||||
e -> CustomerNotFoundException.class.isAssignableFrom(e.getClass()));
|
||||
op = retry;
|
||||
final var customerId = op.perform();
|
||||
LOG.info(String.format(
|
||||
"However, retrying the operation while ignoring a recoverable error will eventually yield "
|
||||
+ "the result %s after a number of attempts %s", customerId, retry.attempts()
|
||||
));
|
||||
LOG.info(
|
||||
String.format(
|
||||
"However, retrying the operation while ignoring a recoverable error will eventually yield "
|
||||
+ "the result %s after a number of attempts %s",
|
||||
customerId, retry.attempts()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,11 +31,9 @@ import java.io.Serial;
|
||||
* occurred. Its use is reserved as a "catch-all" for cases where no other subtype captures the
|
||||
* specificity of the error condition in question. Calling code is not expected to be able to handle
|
||||
* this error and should be reported to the maintainers immediately.
|
||||
*
|
||||
*/
|
||||
public class BusinessException extends Exception {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 6235833142062144336L;
|
||||
@Serial private static final long serialVersionUID = 6235833142062144336L;
|
||||
|
||||
/**
|
||||
* Ctor.
|
||||
|
||||
@@ -37,7 +37,7 @@ public interface BusinessOperation<T> {
|
||||
*
|
||||
* @return the return value
|
||||
* @throws BusinessException if the operation fails. Implementations are allowed to throw more
|
||||
* specific subtypes depending on the error conditions
|
||||
* specific subtypes depending on the error conditions
|
||||
*/
|
||||
T perform() throws BusinessException;
|
||||
}
|
||||
|
||||
@@ -32,12 +32,10 @@ import java.io.Serial;
|
||||
* <p>The severity of this error is bounded by its context: was the search for the customer
|
||||
* triggered by an input from some end user, or were the search parameters pulled from your
|
||||
* database?
|
||||
*
|
||||
*/
|
||||
public final class CustomerNotFoundException extends BusinessException {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = -6972888602621778664L;
|
||||
@Serial private static final long serialVersionUID = -6972888602621778664L;
|
||||
|
||||
/**
|
||||
* Ctor.
|
||||
|
||||
@@ -26,13 +26,9 @@ package com.iluwatar.retry;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
/**
|
||||
* Catastrophic error indicating that we have lost connection to our database.
|
||||
*
|
||||
*/
|
||||
/** Catastrophic error indicating that we have lost connection to our database. */
|
||||
public final class DatabaseNotAvailableException extends BusinessException {
|
||||
@Serial
|
||||
private static final long serialVersionUID = -3750769625095997799L;
|
||||
@Serial private static final long serialVersionUID = -3750769625095997799L;
|
||||
|
||||
/**
|
||||
* Ctor.
|
||||
|
||||
@@ -34,13 +34,13 @@ import java.util.List;
|
||||
* <p>This is an imaginary operation that, for some imagined input, returns the ID for a customer.
|
||||
* However, this is a "flaky" operation that is supposed to fail intermittently, but for the
|
||||
* purposes of this example it fails in a programmed way depending on the constructor parameters.
|
||||
*
|
||||
*/
|
||||
|
||||
public record FindCustomer(String customerId, Deque<BusinessException> errors) implements BusinessOperation<String> {
|
||||
public record FindCustomer(String customerId, Deque<BusinessException> errors)
|
||||
implements BusinessOperation<String> {
|
||||
public FindCustomer(String customerId, BusinessException... errors) {
|
||||
this(customerId, new ArrayDeque<>(List.of(errors)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String perform() throws BusinessException {
|
||||
if (!this.errors.isEmpty()) {
|
||||
|
||||
@@ -47,19 +47,15 @@ public final class Retry<T> implements BusinessOperation<T> {
|
||||
/**
|
||||
* Ctor.
|
||||
*
|
||||
* @param op the {@link BusinessOperation} to retry
|
||||
* @param op the {@link BusinessOperation} to retry
|
||||
* @param maxAttempts number of times to retry
|
||||
* @param delay delay (in milliseconds) between attempts
|
||||
* @param delay delay (in milliseconds) between attempts
|
||||
* @param ignoreTests tests to check whether the remote exception can be ignored. No exceptions
|
||||
* will be ignored if no tests are given
|
||||
* will be ignored if no tests are given
|
||||
*/
|
||||
@SafeVarargs
|
||||
public Retry(
|
||||
BusinessOperation<T> op,
|
||||
int maxAttempts,
|
||||
long delay,
|
||||
Predicate<Exception>... ignoreTests
|
||||
) {
|
||||
BusinessOperation<T> op, int maxAttempts, long delay, Predicate<Exception>... ignoreTests) {
|
||||
this.op = op;
|
||||
this.maxAttempts = maxAttempts;
|
||||
this.delay = delay;
|
||||
@@ -101,7 +97,7 @@ public final class Retry<T> implements BusinessOperation<T> {
|
||||
try {
|
||||
Thread.sleep(this.delay);
|
||||
} catch (InterruptedException f) {
|
||||
//ignore
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
} while (true);
|
||||
|
||||
@@ -49,18 +49,17 @@ public final class RetryExponentialBackoff<T> implements BusinessOperation<T> {
|
||||
/**
|
||||
* Ctor.
|
||||
*
|
||||
* @param op the {@link BusinessOperation} to retry
|
||||
* @param op the {@link BusinessOperation} to retry
|
||||
* @param maxAttempts number of times to retry
|
||||
* @param ignoreTests tests to check whether the remote exception can be ignored. No exceptions
|
||||
* will be ignored if no tests are given
|
||||
* will be ignored if no tests are given
|
||||
*/
|
||||
@SafeVarargs
|
||||
public RetryExponentialBackoff(
|
||||
BusinessOperation<T> op,
|
||||
int maxAttempts,
|
||||
long maxDelay,
|
||||
Predicate<Exception>... ignoreTests
|
||||
) {
|
||||
Predicate<Exception>... ignoreTests) {
|
||||
this.op = op;
|
||||
this.maxAttempts = maxAttempts;
|
||||
this.maxDelay = maxDelay;
|
||||
@@ -104,7 +103,7 @@ public final class RetryExponentialBackoff<T> implements BusinessOperation<T> {
|
||||
var delay = Math.min(testDelay, this.maxDelay);
|
||||
Thread.sleep(delay);
|
||||
} catch (InterruptedException f) {
|
||||
//ignore
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
} while (true);
|
||||
|
||||
@@ -30,22 +30,15 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link FindCustomer}.
|
||||
*
|
||||
*/
|
||||
/** Unit tests for {@link FindCustomer}. */
|
||||
class FindCustomerTest {
|
||||
/**
|
||||
* Returns the given result with no exceptions.
|
||||
*/
|
||||
/** Returns the given result with no exceptions. */
|
||||
@Test
|
||||
void noExceptions() throws Exception {
|
||||
assertThat(new FindCustomer("123").perform(), is("123"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws the given exception.
|
||||
*/
|
||||
/** Throws the given exception. */
|
||||
@Test
|
||||
void oneException() {
|
||||
var findCustomer = new FindCustomer("123", new BusinessException("test"));
|
||||
@@ -59,20 +52,20 @@ class FindCustomerTest {
|
||||
*/
|
||||
@Test
|
||||
void resultAfterExceptions() throws Exception {
|
||||
final var op = new FindCustomer(
|
||||
"123",
|
||||
new CustomerNotFoundException("not found"),
|
||||
new DatabaseNotAvailableException("not available")
|
||||
);
|
||||
final var op =
|
||||
new FindCustomer(
|
||||
"123",
|
||||
new CustomerNotFoundException("not found"),
|
||||
new DatabaseNotAvailableException("not available"));
|
||||
try {
|
||||
op.perform();
|
||||
} catch (CustomerNotFoundException e) {
|
||||
//ignore
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
op.perform();
|
||||
} catch (DatabaseNotAvailableException e) {
|
||||
//ignore
|
||||
// ignore
|
||||
}
|
||||
|
||||
assertThat(op.perform(), is("123"));
|
||||
|
||||
@@ -30,28 +30,23 @@ import static org.hamcrest.MatcherAssert.assertThat;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link Retry}.
|
||||
*
|
||||
*/
|
||||
/** Unit tests for {@link Retry}. */
|
||||
class RetryExponentialBackoffTest {
|
||||
/**
|
||||
* Should contain all errors thrown.
|
||||
*/
|
||||
/** Should contain all errors thrown. */
|
||||
@Test
|
||||
void errors() {
|
||||
final var e = new BusinessException("unhandled");
|
||||
final var retry = new RetryExponentialBackoff<String>(
|
||||
() -> {
|
||||
throw e;
|
||||
},
|
||||
2,
|
||||
0
|
||||
);
|
||||
final var retry =
|
||||
new RetryExponentialBackoff<String>(
|
||||
() -> {
|
||||
throw e;
|
||||
},
|
||||
2,
|
||||
0);
|
||||
try {
|
||||
retry.perform();
|
||||
} catch (BusinessException ex) {
|
||||
//ignore
|
||||
// ignore
|
||||
}
|
||||
|
||||
assertThat(retry.errors(), hasItem(e));
|
||||
@@ -64,17 +59,17 @@ class RetryExponentialBackoffTest {
|
||||
@Test
|
||||
void attempts() {
|
||||
final var e = new BusinessException("unhandled");
|
||||
final var retry = new RetryExponentialBackoff<String>(
|
||||
() -> {
|
||||
throw e;
|
||||
},
|
||||
2,
|
||||
0
|
||||
);
|
||||
final var retry =
|
||||
new RetryExponentialBackoff<String>(
|
||||
() -> {
|
||||
throw e;
|
||||
},
|
||||
2,
|
||||
0);
|
||||
try {
|
||||
retry.perform();
|
||||
} catch (BusinessException ex) {
|
||||
//ignore
|
||||
// ignore
|
||||
}
|
||||
|
||||
assertThat(retry.attempts(), is(1));
|
||||
@@ -87,18 +82,18 @@ class RetryExponentialBackoffTest {
|
||||
@Test
|
||||
void ignore() {
|
||||
final var e = new CustomerNotFoundException("customer not found");
|
||||
final var retry = new RetryExponentialBackoff<String>(
|
||||
() -> {
|
||||
throw e;
|
||||
},
|
||||
2,
|
||||
0,
|
||||
ex -> CustomerNotFoundException.class.isAssignableFrom(ex.getClass())
|
||||
);
|
||||
final var retry =
|
||||
new RetryExponentialBackoff<String>(
|
||||
() -> {
|
||||
throw e;
|
||||
},
|
||||
2,
|
||||
0,
|
||||
ex -> CustomerNotFoundException.class.isAssignableFrom(ex.getClass()));
|
||||
try {
|
||||
retry.perform();
|
||||
} catch (BusinessException ex) {
|
||||
//ignore
|
||||
// ignore
|
||||
}
|
||||
|
||||
assertThat(retry.attempts(), is(2));
|
||||
|
||||
@@ -30,29 +30,24 @@ import static org.hamcrest.MatcherAssert.assertThat;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link Retry}.
|
||||
*
|
||||
*/
|
||||
/** Unit tests for {@link Retry}. */
|
||||
class RetryTest {
|
||||
|
||||
/**
|
||||
* Should contain all errors thrown.
|
||||
*/
|
||||
/** Should contain all errors thrown. */
|
||||
@Test
|
||||
void errors() {
|
||||
final var e = new BusinessException("unhandled");
|
||||
final var retry = new Retry<String>(
|
||||
() -> {
|
||||
throw e;
|
||||
},
|
||||
2,
|
||||
0
|
||||
);
|
||||
final var retry =
|
||||
new Retry<String>(
|
||||
() -> {
|
||||
throw e;
|
||||
},
|
||||
2,
|
||||
0);
|
||||
try {
|
||||
retry.perform();
|
||||
} catch (BusinessException ex) {
|
||||
//ignore
|
||||
// ignore
|
||||
}
|
||||
|
||||
assertThat(retry.errors(), hasItem(e));
|
||||
@@ -65,17 +60,17 @@ class RetryTest {
|
||||
@Test
|
||||
void attempts() {
|
||||
final var e = new BusinessException("unhandled");
|
||||
final var retry = new Retry<String>(
|
||||
() -> {
|
||||
throw e;
|
||||
},
|
||||
2,
|
||||
0
|
||||
);
|
||||
final var retry =
|
||||
new Retry<String>(
|
||||
() -> {
|
||||
throw e;
|
||||
},
|
||||
2,
|
||||
0);
|
||||
try {
|
||||
retry.perform();
|
||||
} catch (BusinessException ex) {
|
||||
//ignore
|
||||
// ignore
|
||||
}
|
||||
|
||||
assertThat(retry.attempts(), is(1));
|
||||
@@ -88,21 +83,20 @@ class RetryTest {
|
||||
@Test
|
||||
void ignore() {
|
||||
final var e = new CustomerNotFoundException("customer not found");
|
||||
final var retry = new Retry<String>(
|
||||
() -> {
|
||||
throw e;
|
||||
},
|
||||
2,
|
||||
0,
|
||||
ex -> CustomerNotFoundException.class.isAssignableFrom(ex.getClass())
|
||||
);
|
||||
final var retry =
|
||||
new Retry<String>(
|
||||
() -> {
|
||||
throw e;
|
||||
},
|
||||
2,
|
||||
0,
|
||||
ex -> CustomerNotFoundException.class.isAssignableFrom(ex.getClass()));
|
||||
try {
|
||||
retry.perform();
|
||||
} catch (BusinessException ex) {
|
||||
//ignore
|
||||
// ignore
|
||||
}
|
||||
|
||||
assertThat(retry.attempts(), is(2));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user