Introduce minimal retry functionality as a core framework feature
This commit introduces a minimal core retry feature. It is inspired by Spring Retry, but redesigned and trimmed to the bare minimum to cover most cases. Closes gh-34716
This commit is contained in:
committed by
Sam Brannen
parent
aefdda3490
commit
3fb4a75ae4
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.springframework.core.retry;
|
||||
|
||||
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.retry.support.MaxRetryAttemptsPolicy;
|
||||
import org.springframework.util.backoff.FixedBackOff;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Tests for {@link RetryTemplate}.
|
||||
*
|
||||
* @author Mahmoud Ben Hassine
|
||||
*/
|
||||
class RetryTemplateTests {
|
||||
|
||||
@Test
|
||||
void testRetryWithSuccess() throws Exception {
|
||||
// given
|
||||
RetryCallback<String> retryCallback = new RetryCallback<>() {
|
||||
int failure;
|
||||
@Override
|
||||
public String run() throws Exception {
|
||||
if (failure++ < 2) {
|
||||
throw new Exception("Error while invoking greeting service");
|
||||
}
|
||||
return "hello world";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "greeting service";
|
||||
}
|
||||
};
|
||||
RetryTemplate retryTemplate = new RetryTemplate();
|
||||
retryTemplate.setRetryPolicy(new MaxRetryAttemptsPolicy());
|
||||
retryTemplate.setBackOffPolicy(new FixedBackOff());
|
||||
|
||||
// when
|
||||
String result = retryTemplate.execute(retryCallback);
|
||||
|
||||
// then
|
||||
assertThat(result).isEqualTo("hello world");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRetryWithFailure() {
|
||||
// given
|
||||
Exception exception = new Exception("Error while invoking greeting service");
|
||||
RetryCallback<String> retryCallback = new RetryCallback<>() {
|
||||
@Override
|
||||
public String run() throws Exception {
|
||||
throw exception;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "greeting service";
|
||||
}
|
||||
};
|
||||
RetryTemplate retryTemplate = new RetryTemplate();
|
||||
retryTemplate.setRetryPolicy(new MaxRetryAttemptsPolicy());
|
||||
retryTemplate.setBackOffPolicy(new FixedBackOff());
|
||||
|
||||
// when
|
||||
ThrowingCallable throwingCallable = () -> retryTemplate.execute(retryCallback);
|
||||
|
||||
// then
|
||||
assertThatThrownBy(throwingCallable)
|
||||
.isInstanceOf(RetryException.class)
|
||||
.hasMessage("Retry policy for callback 'greeting service' exhausted, aborting execution")
|
||||
.hasCause(exception);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRetrySpecificException() {
|
||||
// given
|
||||
class TechnicalException extends Exception {
|
||||
@java.io.Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
public TechnicalException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
final TechnicalException technicalException = new TechnicalException("Error while invoking greeting service");
|
||||
RetryCallback<String> retryCallback = new RetryCallback<>() {
|
||||
@Override
|
||||
public String run() throws TechnicalException {
|
||||
throw technicalException;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "greeting service";
|
||||
}
|
||||
};
|
||||
MaxRetryAttemptsPolicy retryPolicy = new MaxRetryAttemptsPolicy() {
|
||||
@Override
|
||||
public RetryExecution start() {
|
||||
return new RetryExecution() {
|
||||
int retryAttempts;
|
||||
@Override
|
||||
public boolean shouldRetry(Throwable throwable) {
|
||||
return this.retryAttempts++ < 3 && throwable instanceof TechnicalException;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
RetryTemplate retryTemplate = new RetryTemplate();
|
||||
retryTemplate.setRetryPolicy(retryPolicy);
|
||||
retryTemplate.setBackOffPolicy(new FixedBackOff());
|
||||
|
||||
// when
|
||||
ThrowingCallable throwingCallable = () -> retryTemplate.execute(retryCallback);
|
||||
|
||||
// then
|
||||
assertThatThrownBy(throwingCallable)
|
||||
.isInstanceOf(RetryException.class)
|
||||
.hasMessage("Retry policy for callback 'greeting service' exhausted, aborting execution")
|
||||
.hasCause(technicalException);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.springframework.core.retry.support;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.retry.RetryExecution;
|
||||
import org.springframework.core.retry.RetryListener;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for {@link CompositeRetryListener}.
|
||||
*
|
||||
* @author Mahmoud Ben Hassine
|
||||
*/
|
||||
class ComposedRetryListenerTests {
|
||||
|
||||
private final RetryListener listener1 = mock();
|
||||
private final RetryListener listener2 = mock();
|
||||
|
||||
private final CompositeRetryListener composedRetryListener = new CompositeRetryListener(Arrays.asList(listener1, listener2));
|
||||
|
||||
@Test
|
||||
void beforeRetry() {
|
||||
RetryExecution retryExecution = mock();
|
||||
this.composedRetryListener.beforeRetry(retryExecution);
|
||||
|
||||
verify(this.listener1).beforeRetry(retryExecution);
|
||||
verify(this.listener2).beforeRetry(retryExecution);
|
||||
}
|
||||
|
||||
@Test
|
||||
void onSuccess() {
|
||||
Object result = new Object();
|
||||
RetryExecution retryExecution = mock();
|
||||
this.composedRetryListener.onRetrySuccess(retryExecution, result);
|
||||
|
||||
verify(this.listener1).onRetrySuccess(retryExecution, result);
|
||||
verify(this.listener2).onRetrySuccess(retryExecution, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void onFailure() {
|
||||
Exception exception = new Exception();
|
||||
RetryExecution retryExecution = mock();
|
||||
this.composedRetryListener.onRetryFailure(retryExecution, exception);
|
||||
|
||||
verify(this.listener1).onRetryFailure(retryExecution, exception);
|
||||
verify(this.listener2).onRetryFailure(retryExecution, exception);
|
||||
}
|
||||
|
||||
@Test
|
||||
void onMaxAttempts() {
|
||||
Exception exception = new Exception();
|
||||
RetryExecution retryExecution = mock();
|
||||
this.composedRetryListener.onRetryPolicyExhaustion(retryExecution, exception);
|
||||
|
||||
verify(this.listener1).onRetryPolicyExhaustion(retryExecution, exception);
|
||||
verify(this.listener2).onRetryPolicyExhaustion(retryExecution, exception);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.springframework.core.retry.support;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.retry.RetryExecution;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link MaxRetryAttemptsPolicy}.
|
||||
*
|
||||
* @author Mahmoud Ben Hassine
|
||||
*/
|
||||
class MaxRetryAttemptsPolicyTests {
|
||||
|
||||
@Test
|
||||
void testDefaultMaxRetryAttempts() {
|
||||
// given
|
||||
MaxRetryAttemptsPolicy retryPolicy = new MaxRetryAttemptsPolicy();
|
||||
Throwable throwable = mock();
|
||||
|
||||
// when
|
||||
RetryExecution retryExecution = retryPolicy.start();
|
||||
|
||||
// then
|
||||
assertThat(retryExecution.shouldRetry(throwable)).isTrue();
|
||||
assertThat(retryExecution.shouldRetry(throwable)).isTrue();
|
||||
assertThat(retryExecution.shouldRetry(throwable)).isTrue();
|
||||
assertThat(retryExecution.shouldRetry(throwable)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInvalidMaxRetryAttempts() {
|
||||
assertThatThrownBy(() -> new MaxRetryAttemptsPolicy(-1))
|
||||
.hasMessage("Max retry attempts must be greater than zero");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.springframework.core.retry.support;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Tests for {@link MaxRetryDurationPolicy}.
|
||||
*
|
||||
* @author Mahmoud Ben Hassine
|
||||
*/
|
||||
class MaxRetryDurationPolicyTests {
|
||||
|
||||
@Test
|
||||
void testInvalidMaxRetryDuration() {
|
||||
assertThatThrownBy(() -> new MaxRetryDurationPolicy(Duration.ZERO))
|
||||
.hasMessage("Max retry duration must be positive");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.springframework.core.retry.support;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.retry.RetryExecution;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link PredicateRetryPolicy}.
|
||||
*
|
||||
* @author Mahmoud Ben Hassine
|
||||
*/
|
||||
class PredicateRetryPolicyTests {
|
||||
|
||||
@Test
|
||||
void testPredicateRetryPolicy() {
|
||||
// given
|
||||
class MyException extends Exception {
|
||||
@java.io.Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
||||
Predicate<Throwable> predicate = throwable -> throwable instanceof MyException;
|
||||
PredicateRetryPolicy retryPolicy = new PredicateRetryPolicy(predicate);
|
||||
|
||||
// when
|
||||
RetryExecution retryExecution = retryPolicy.start();
|
||||
|
||||
// then
|
||||
assertThat(retryExecution.shouldRetry(new MyException())).isTrue();
|
||||
assertThat(retryExecution.shouldRetry(new IllegalStateException())).isFalse();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user