Skip to content

Commit 99099b9

Browse files
authored
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
1 parent 8f985ef commit 99099b9

File tree

6 files changed

+230
-14
lines changed

6 files changed

+230
-14
lines changed

README.md

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -166,13 +166,18 @@ RetryTemplate.builder()
166166

167167
### Using `RetryContext`
168168

169-
The method parameter for the `RetryCallback` is a `RetryContext`. Many callbacks ignore
170-
the context. However, if necessary, you can use it as an attribute bag to store data for
171-
the duration of the iteration.
172-
173-
A `RetryContext` has a parent context if there is a nested retry in progress in the same
174-
thread. The parent context is occasionally useful for storing data that needs to be shared
175-
between calls to execute.
169+
The method parameter for the `RetryCallback` is a `RetryContext`.
170+
Many callbacks ignore the context.
171+
However, if necessary, you can use it as an attribute bag to store data for the duration of the iteration.
172+
It also has some useful properties, such as `retryCount`.
173+
174+
A `RetryContext` has a parent context if there is a nested retry in progress in the same thread.
175+
The parent context is occasionally useful for storing data that needs to be shared between calls to execute.
176+
177+
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()`.
178+
By default, the context is stored in a `ThreadLocal`.
179+
JEP 444 recommends that `ThreadLocal` should be avoided when using virtual threads, available in Java 21 and beyond.
180+
To store the contexts in a `Map` instead of a `ThreadLocal`, call `RetrySynchronizationManager.setUseThreadLocal(false)`.
176181

177182
### Using `RecoveryCallback`
178183

src/main/java/org/springframework/retry/support/RetrySynchronizationManager.java

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2006-2022 the original author or authors.
2+
* Copyright 2006-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,10 @@
1616

1717
package org.springframework.retry.support;
1818

19+
import java.util.Map;
20+
import java.util.concurrent.ConcurrentHashMap;
21+
22+
import org.springframework.lang.Nullable;
1923
import org.springframework.retry.RetryCallback;
2024
import org.springframework.retry.RetryContext;
2125
import org.springframework.retry.RetryOperations;
@@ -30,6 +34,7 @@
3034
* {@link RetryOperations} implementations.
3135
*
3236
* @author Dave Syer
37+
* @author Gary Russell
3338
*
3439
*/
3540
public final class RetrySynchronizationManager {
@@ -39,13 +44,41 @@ private RetrySynchronizationManager() {
3944

4045
private static final ThreadLocal<RetryContext> context = new ThreadLocal<>();
4146

47+
private static final Map<Thread, RetryContext> contexts = new ConcurrentHashMap<>();
48+
49+
private static boolean useThreadLocal = true;
50+
51+
/**
52+
* Set to false to store the context in a map (keyed by the current thread) instead of
53+
* in a {@link ThreadLocal}. Recommended when using virtual threads.
54+
* @param use true to use a {@link ThreadLocal} (default true).
55+
* @since 2.0.3
56+
*/
57+
public static void setUseThreadLocal(boolean use) {
58+
useThreadLocal = use;
59+
}
60+
61+
/**
62+
* Return true if contexts are held in a ThreadLocal (default) rather than a Map.
63+
* @return the useThreadLocal
64+
* @since 2.0.3
65+
*/
66+
public static boolean isUseThreadLocal() {
67+
return useThreadLocal;
68+
}
69+
4270
/**
4371
* Public accessor for the locally enclosing {@link RetryContext}.
4472
* @return the current retry context, or null if there isn't one
4573
*/
74+
@Nullable
4675
public static RetryContext getContext() {
47-
RetryContext result = context.get();
48-
return result;
76+
if (useThreadLocal) {
77+
return context.get();
78+
}
79+
else {
80+
return contexts.get(Thread.currentThread());
81+
}
4982
}
5083

5184
/**
@@ -55,21 +88,40 @@ public static RetryContext getContext() {
5588
* @param context the new context to register
5689
* @return the old context if there was one
5790
*/
91+
@Nullable
5892
public static RetryContext register(RetryContext context) {
59-
RetryContext oldContext = getContext();
60-
RetrySynchronizationManager.context.set(context);
61-
return oldContext;
93+
if (useThreadLocal) {
94+
RetryContext oldContext = getContext();
95+
RetrySynchronizationManager.context.set(context);
96+
return oldContext;
97+
}
98+
else {
99+
RetryContext oldContext = contexts.get(Thread.currentThread());
100+
contexts.put(Thread.currentThread(), context);
101+
return oldContext;
102+
}
62103
}
63104

64105
/**
65106
* Clear the current context at the end of a batch - should only be used by
66107
* {@link RetryOperations} implementations.
67108
* @return the old value if there was one.
68109
*/
110+
@Nullable
69111
public static RetryContext clear() {
70112
RetryContext value = getContext();
71113
RetryContext parent = value == null ? null : value.getParent();
72-
RetrySynchronizationManager.context.set(parent);
114+
if (useThreadLocal) {
115+
RetrySynchronizationManager.context.set(parent);
116+
}
117+
else {
118+
if (parent != null) {
119+
contexts.put(Thread.currentThread(), parent);
120+
}
121+
else {
122+
contexts.remove(Thread.currentThread());
123+
}
124+
}
73125
return value;
74126
}
75127

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.retry.annotation;
18+
19+
import org.junit.jupiter.api.AfterAll;
20+
import org.junit.jupiter.api.BeforeAll;
21+
22+
import org.springframework.retry.support.RetrySynchronizationManager;
23+
24+
public class EnableRetryNoThreadLocalTests extends EnableRetryTests {
25+
26+
@BeforeAll
27+
static void before() {
28+
RetrySynchronizationManager.setUseThreadLocal(false);
29+
}
30+
31+
@AfterAll
32+
static void after() {
33+
RetrySynchronizationManager.setUseThreadLocal(true);
34+
}
35+
36+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.retry.annotation;
18+
19+
import org.junit.jupiter.api.AfterAll;
20+
import org.junit.jupiter.api.BeforeAll;
21+
22+
import org.springframework.retry.support.RetrySynchronizationManager;
23+
24+
/**
25+
* @author Gary Russell
26+
*/
27+
public class EnableRetryWithBackoffNoThreadLocalTests extends EnableRetryWithBackoffTests {
28+
29+
@BeforeAll
30+
static void before() {
31+
RetrySynchronizationManager.setUseThreadLocal(false);
32+
}
33+
34+
@AfterAll
35+
static void after() {
36+
RetrySynchronizationManager.setUseThreadLocal(true);
37+
}
38+
39+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.retry.support;
18+
19+
import org.junit.jupiter.api.AfterAll;
20+
import org.junit.jupiter.api.BeforeAll;
21+
import org.junit.jupiter.api.BeforeEach;
22+
23+
import org.springframework.retry.RetryContext;
24+
25+
import static org.assertj.core.api.Assertions.assertThat;
26+
27+
public class RetrySynchronizationManagerNoThreadLocalTests extends RetrySynchronizationManagerTests {
28+
29+
@BeforeAll
30+
static void before() {
31+
RetrySynchronizationManager.setUseThreadLocal(false);
32+
}
33+
34+
@AfterAll
35+
static void after() {
36+
RetrySynchronizationManager.setUseThreadLocal(true);
37+
}
38+
39+
@Override
40+
@BeforeEach
41+
public void setUp() {
42+
RetrySynchronizationManagerTests.clearAll();
43+
RetryContext status = RetrySynchronizationManager.getContext();
44+
assertThat(status).isNull();
45+
}
46+
47+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.retry.support;
18+
19+
import org.junit.jupiter.api.AfterAll;
20+
import org.junit.jupiter.api.BeforeAll;
21+
22+
/**
23+
* @author Gary Russell
24+
*/
25+
public class RetryTemplateNoThreadLocalTests extends RetryTemplateTests {
26+
27+
@BeforeAll
28+
static void before() {
29+
RetrySynchronizationManager.setUseThreadLocal(false);
30+
}
31+
32+
@AfterAll
33+
static void after() {
34+
RetrySynchronizationManager.setUseThreadLocal(true);
35+
}
36+
37+
}

0 commit comments

Comments
 (0)