Add Option to Avoid Use of ThreadLocal

* Add an option to store contexts in a map instead of a `ThreadLocal`
* Subclass existing tests to verify functionality
This commit is contained in:
Gary Russell
2023-08-30 10:05:08 -07:00
committed by GitHub
parent 8f985ef828
commit 99099b9c1c
6 changed files with 229 additions and 13 deletions

View File

@@ -166,13 +166,18 @@ RetryTemplate.builder()
### Using `RetryContext`
The method parameter for the `RetryCallback` is a `RetryContext`. Many callbacks ignore
the context. However, if necessary, you can use it as an attribute bag to store data for
the duration of the iteration.
The method parameter for the `RetryCallback` is a `RetryContext`.
Many callbacks ignore the context.
However, if necessary, you can use it as an attribute bag to store data for the duration of the iteration.
It also has some useful properties, such as `retryCount`.
A `RetryContext` has a parent context if there is a nested retry in progress in the same
thread. The parent context is occasionally useful for storing data that needs to be shared
between calls to execute.
A `RetryContext` has a parent context if there is a nested retry in progress in the same thread.
The parent context is occasionally useful for storing data that needs to be shared between calls to execute.
If you dont have access to the context directly, you can obtain the current context within the scope of the retries by calling `RetrySynchronizationManager.getContext()`.
By default, the context is stored in a `ThreadLocal`.
JEP 444 recommends that `ThreadLocal` should be avoided when using virtual threads, available in Java 21 and beyond.
To store the contexts in a `Map` instead of a `ThreadLocal`, call `RetrySynchronizationManager.setUseThreadLocal(false)`.
### Using `RecoveryCallback`

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2006-2022 the original author or authors.
* Copyright 2006-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.
@@ -16,6 +16,10 @@
package org.springframework.retry.support;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.lang.Nullable;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.RetryOperations;
@@ -30,6 +34,7 @@ import org.springframework.retry.RetryOperations;
* {@link RetryOperations} implementations.
*
* @author Dave Syer
* @author Gary Russell
*
*/
public final class RetrySynchronizationManager {
@@ -39,13 +44,41 @@ public final class RetrySynchronizationManager {
private static final ThreadLocal<RetryContext> context = new ThreadLocal<>();
private static final Map<Thread, RetryContext> contexts = new ConcurrentHashMap<>();
private static boolean useThreadLocal = true;
/**
* Set to false to store the context in a map (keyed by the current thread) instead of
* in a {@link ThreadLocal}. Recommended when using virtual threads.
* @param use true to use a {@link ThreadLocal} (default true).
* @since 2.0.3
*/
public static void setUseThreadLocal(boolean use) {
useThreadLocal = use;
}
/**
* Return true if contexts are held in a ThreadLocal (default) rather than a Map.
* @return the useThreadLocal
* @since 2.0.3
*/
public static boolean isUseThreadLocal() {
return useThreadLocal;
}
/**
* Public accessor for the locally enclosing {@link RetryContext}.
* @return the current retry context, or null if there isn't one
*/
@Nullable
public static RetryContext getContext() {
RetryContext result = context.get();
return result;
if (useThreadLocal) {
return context.get();
}
else {
return contexts.get(Thread.currentThread());
}
}
/**
@@ -55,10 +88,18 @@ public final class RetrySynchronizationManager {
* @param context the new context to register
* @return the old context if there was one
*/
@Nullable
public static RetryContext register(RetryContext context) {
RetryContext oldContext = getContext();
RetrySynchronizationManager.context.set(context);
return oldContext;
if (useThreadLocal) {
RetryContext oldContext = getContext();
RetrySynchronizationManager.context.set(context);
return oldContext;
}
else {
RetryContext oldContext = contexts.get(Thread.currentThread());
contexts.put(Thread.currentThread(), context);
return oldContext;
}
}
/**
@@ -66,10 +107,21 @@ public final class RetrySynchronizationManager {
* {@link RetryOperations} implementations.
* @return the old value if there was one.
*/
@Nullable
public static RetryContext clear() {
RetryContext value = getContext();
RetryContext parent = value == null ? null : value.getParent();
RetrySynchronizationManager.context.set(parent);
if (useThreadLocal) {
RetrySynchronizationManager.context.set(parent);
}
else {
if (parent != null) {
contexts.put(Thread.currentThread(), parent);
}
else {
contexts.remove(Thread.currentThread());
}
}
return value;
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 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.
* 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.retry.annotation;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.springframework.retry.support.RetrySynchronizationManager;
public class EnableRetryNoThreadLocalTests extends EnableRetryTests {
@BeforeAll
static void before() {
RetrySynchronizationManager.setUseThreadLocal(false);
}
@AfterAll
static void after() {
RetrySynchronizationManager.setUseThreadLocal(true);
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 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.
* 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.retry.annotation;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.springframework.retry.support.RetrySynchronizationManager;
/**
* @author Gary Russell
*/
public class EnableRetryWithBackoffNoThreadLocalTests extends EnableRetryWithBackoffTests {
@BeforeAll
static void before() {
RetrySynchronizationManager.setUseThreadLocal(false);
}
@AfterAll
static void after() {
RetrySynchronizationManager.setUseThreadLocal(true);
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 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.
* 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.retry.support;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.retry.RetryContext;
import static org.assertj.core.api.Assertions.assertThat;
public class RetrySynchronizationManagerNoThreadLocalTests extends RetrySynchronizationManagerTests {
@BeforeAll
static void before() {
RetrySynchronizationManager.setUseThreadLocal(false);
}
@AfterAll
static void after() {
RetrySynchronizationManager.setUseThreadLocal(true);
}
@Override
@BeforeEach
public void setUp() {
RetrySynchronizationManagerTests.clearAll();
RetryContext status = RetrySynchronizationManager.getContext();
assertThat(status).isNull();
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 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.
* 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.retry.support;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
/**
* @author Gary Russell
*/
public class RetryTemplateNoThreadLocalTests extends RetryTemplateTests {
@BeforeAll
static void before() {
RetrySynchronizationManager.setUseThreadLocal(false);
}
@AfterAll
static void after() {
RetrySynchronizationManager.setUseThreadLocal(true);
}
}