Remove ThreadLocal from RequestHandlerRetryAdvice (#8650)

Related to https://github.com/spring-projects/spring-integration/issues/8644

With virtual threads it is recommended to stay away from `ThreadLocal`
variables to avoid memory exhausting with too many virtual threads

* Fix `RequestHandlerRetryAdvice` to transfer the message context via internal
`IntegrationRetryCallback` implementation.
* Cast to this `IntegrationRetryCallback` in a newly introduced internal `IntegrationRetryListener`
to extract `messageToTry` and set it into a `RetryContext`
`ErrorMessageUtils.FAILED_MESSAGE_CONTEXT_KEY` attribute
* Deprecate a usage of an external `RetryListener` implementation of the `RequestHandlerRetryAdvice`
This commit is contained in:
Artem Bilan
2023-06-21 11:41:01 -04:00
committed by GitHub
parent 8a29c1e60b
commit 9e9bfd0c5c

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2023 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.
@@ -43,14 +43,14 @@ import org.springframework.util.Assert;
public class RequestHandlerRetryAdvice extends AbstractRequestHandlerAdvice
implements RetryListener {
private static final ThreadLocal<Message<?>> MESSAGE_HOLDER = new ThreadLocal<>();
private static final IntegrationRetryListener INTEGRATION_RETRY_LISTENER = new IntegrationRetryListener();
private RetryTemplate retryTemplate = new RetryTemplate();
private RecoveryCallback<Object> recoveryCallback;
// Stateless unless a state generator is provided
private volatile RetryStateGenerator retryStateGenerator = message -> null;
private RetryStateGenerator retryStateGenerator = message -> null;
/**
* Set the retry template. Cause traversal should be enabled in the retry policy
@@ -74,48 +74,71 @@ public class RequestHandlerRetryAdvice extends AbstractRequestHandlerAdvice
@Override
protected void onInit() {
super.onInit();
this.retryTemplate.registerListener(this);
this.retryTemplate.registerListener(INTEGRATION_RETRY_LISTENER);
}
@Override
protected Object doInvoke(final ExecutionCallback callback, Object target, final Message<?> message) {
protected Object doInvoke(ExecutionCallback callback, Object target, Message<?> message) {
IntegrationRetryCallback retryCallback = new IntegrationRetryCallback(message, callback);
RetryState retryState = this.retryStateGenerator.determineRetryState(message);
MESSAGE_HOLDER.set(message);
try {
return this.retryTemplate.execute(context -> callback.cloneAndExecute(), this.recoveryCallback, retryState);
return this.retryTemplate.execute(retryCallback, this.recoveryCallback, retryState);
}
catch (MessagingException e) {
if (e.getFailedMessage() == null) {
throw new MessagingException(message, "Failed to invoke handler", e);
catch (MessagingException ex) {
if (ex.getFailedMessage() == null) {
throw new MessagingException(message, "Failed to invoke handler", ex);
}
throw e;
throw ex;
}
catch (ThrowableHolderException e) { // NOSONAR catch and rethrow
throw e;
catch (ThrowableHolderException ex) { // NOSONAR catch and rethrow
throw ex;
}
catch (Exception e) {
throw new ThrowableHolderException(e);
}
finally {
MESSAGE_HOLDER.remove();
catch (Exception ex) {
throw new ThrowableHolderException(ex);
}
}
/**
* Set a {@link ErrorMessageUtils#FAILED_MESSAGE_CONTEXT_KEY} attribute into context.
* @param context the current {@link RetryContext}.
* @param callback the current {@link RetryCallback}.
* @param <T> the type of object returned by the callback
* @param <E> the type of exception it declares may be thrown
* @return the open state.
* @deprecated since 6.2 in favor of an internal {@link RetryListener} implementation.
* The {@link RequestHandlerRetryAdvice} must not be used as a listener for external {@link RetryTemplate}
* instances.
*/
@Deprecated(since = "6.2", forRemoval = true)
@Override
public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
context.setAttribute(ErrorMessageUtils.FAILED_MESSAGE_CONTEXT_KEY, MESSAGE_HOLDER.get());
return true;
return INTEGRATION_RETRY_LISTENER.open(context, callback);
}
@Override
public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback,
Throwable throwable) {
private static class IntegrationRetryListener implements RetryListener {
IntegrationRetryListener() {
}
@Override
public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
Assert.state(callback instanceof IntegrationRetryCallback,
"A 'RequestHandlerRetryAdvice' cannot be used as a 'RetryListener'");
context.setAttribute(ErrorMessageUtils.FAILED_MESSAGE_CONTEXT_KEY,
((IntegrationRetryCallback) callback).messageToTry);
return true;
}
}
@Override
public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback,
Throwable throwable) {
private record IntegrationRetryCallback(Message<?> messageToTry, ExecutionCallback callback)
implements RetryCallback<Object, Exception> {
@Override
public Object doWithRetry(RetryContext context) {
return this.callback.cloneAndExecute();
}
}
}