Skip to content

Commit 1cdf06e

Browse files
committed
GROOVY-9381: add ScopedLocal
1 parent 716e5cf commit 1cdf06e

5 files changed

Lines changed: 1327 additions & 42 deletions

File tree

src/main/java/groovy/concurrent/AsyncContext.java

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121
import groovy.lang.Closure;
2222

23+
import org.apache.groovy.runtime.async.ScopedLocal;
24+
2325
import java.util.Collections;
2426
import java.util.LinkedHashMap;
2527
import java.util.Map;
@@ -50,21 +52,24 @@
5052
* {@code null} as a value removes the key.
5153
*
5254
* <h2>Thread safety</h2>
53-
* Each thread owns its own {@code AsyncContext} instance via a
54-
* {@link ThreadLocal}. Instance methods ({@link #put}, {@link #get},
55+
* Each thread owns its own {@code AsyncContext} instance via
56+
* {@link ScopedLocal}. On JDK&nbsp;25+, this leverages
57+
* {@code ScopedValue} for optimal virtual-thread performance;
58+
* on earlier JDKs it falls back to {@code ThreadLocal}.
59+
* Instance methods ({@link #put}, {@link #get},
5560
* {@link #remove}) are <em>not</em> synchronized — they are only called
5661
* on the owning thread. Static methods ({@link #withSnapshot},
57-
* {@link #capture}) follow a save-and-restore pattern protected by
58-
* {@code try/finally}, ensuring the previous context is always reinstated
59-
* even if the action throws. This design prevents stale context from
60-
* leaking to thread-pool threads between task executions.
62+
* {@link #capture}) use a scope-based binding pattern, ensuring the
63+
* previous context is always reinstated even if the action throws.
64+
* This design prevents stale context from leaking to thread-pool
65+
* threads between task executions.
6166
*
6267
* @since 6.0.0
6368
*/
6469
public final class AsyncContext {
6570

66-
private static final ThreadLocal<AsyncContext> CURRENT =
67-
ThreadLocal.withInitial(AsyncContext::new);
71+
private static final ScopedLocal<AsyncContext> CURRENT =
72+
ScopedLocal.withInitial(AsyncContext::new);
6873

6974
private final Map<String, Object> values;
7075

@@ -120,13 +125,8 @@ public static Snapshot capture() {
120125
public static <T> T withSnapshot(Snapshot snapshot, Supplier<T> supplier) {
121126
Objects.requireNonNull(snapshot, "snapshot must not be null");
122127
Objects.requireNonNull(supplier, "supplier must not be null");
123-
AsyncContext previous = CURRENT.get();
124-
CURRENT.set(new AsyncContext(new LinkedHashMap<>(snapshot.values), true));
125-
try {
126-
return supplier.get();
127-
} finally {
128-
CURRENT.set(previous);
129-
}
128+
AsyncContext restored = new AsyncContext(new LinkedHashMap<>(snapshot.values), true);
129+
return CURRENT.where(restored, supplier);
130130
}
131131

132132
/**

src/main/java/groovy/concurrent/AsyncScope.java

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import groovy.lang.Closure;
2222
import org.apache.groovy.runtime.async.AsyncSupport;
2323
import org.apache.groovy.runtime.async.GroovyPromise;
24+
import org.apache.groovy.runtime.async.ScopedLocal;
2425

2526
import java.util.ArrayList;
2627
import java.util.List;
@@ -74,20 +75,21 @@
7475
* {@link #async(Closure)} and {@link #close()} cannot race: no child
7576
* can be registered after the scope is marked closed.</p>
7677
*
77-
* <h2>ThreadLocal management</h2>
78-
* <p>The {@link #withCurrent(AsyncScope, Supplier)} method uses a
79-
* save-and-restore pattern with a {@code try}/{@code finally} block,
80-
* guaranteeing that the previous scope binding is restored even if
81-
* the supplier throws. Thread-pool threads therefore never retain
82-
* stale scope references across task boundaries.</p>
78+
* <h2>Thread-scoped state management</h2>
79+
* <p>The {@link #withCurrent(AsyncScope, Supplier)} method uses
80+
* {@link ScopedLocal} to manage the current scope binding. On
81+
* JDK&nbsp;25+, this automatically leverages {@code ScopedValue} for
82+
* optimal virtual-thread performance; on earlier JDKs it falls back to
83+
* a {@code ThreadLocal} with a save-and-restore pattern inside
84+
* {@code try}/{@code finally} blocks.</p>
8385
*
8486
* @see Awaitable
8587
* @see AsyncSupport
8688
* @since 6.0.0
8789
*/
8890
public class AsyncScope implements AutoCloseable {
8991

90-
private static final ThreadLocal<AsyncScope> CURRENT = new ThreadLocal<>();
92+
private static final ScopedLocal<AsyncScope> CURRENT = ScopedLocal.newInstance();
9193

9294
/**
9395
* Pruning threshold: completed children are purged when the list
@@ -137,7 +139,7 @@ public AsyncScope() {
137139
* @return the current scope, or {@code null}
138140
*/
139141
public static AsyncScope current() {
140-
return CURRENT.get();
142+
return CURRENT.orElse(null);
141143
}
142144

143145
/**
@@ -151,21 +153,7 @@ public static AsyncScope current() {
151153
*/
152154
public static <T> T withCurrent(AsyncScope scope, Supplier<T> supplier) {
153155
Objects.requireNonNull(supplier, "supplier must not be null");
154-
AsyncScope previous = CURRENT.get();
155-
if (scope == null) {
156-
CURRENT.remove();
157-
} else {
158-
CURRENT.set(scope);
159-
}
160-
try {
161-
return supplier.get();
162-
} finally {
163-
if (previous == null) {
164-
CURRENT.remove();
165-
} else {
166-
CURRENT.set(previous);
167-
}
168-
}
156+
return CURRENT.where(scope, supplier);
169157
}
170158

171159
/**

0 commit comments

Comments
 (0)