Skip to content

Commit 0431701

Browse files
authored
Merge pull request #543 from basil/cacheExecutor2
[JENKINS-72325] Define an executor and scheduler for `SandboxResolvingClassLoader`
2 parents 67ca3d5 + 648a16b commit 0431701

File tree

1 file changed

+81
-5
lines changed

1 file changed

+81
-5
lines changed

src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SandboxResolvingClassLoader.java

Lines changed: 81 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,22 @@
33
import com.github.benmanes.caffeine.cache.Cache;
44
import com.github.benmanes.caffeine.cache.Caffeine;
55
import com.github.benmanes.caffeine.cache.LoadingCache;
6+
import com.github.benmanes.caffeine.cache.Scheduler;
7+
import groovy.lang.GroovyClassLoader;
68
import groovy.lang.GroovyShell;
9+
import hudson.util.ClassLoaderSanityThreadFactory;
10+
import hudson.util.DaemonThreadFactory;
11+
import hudson.util.NamingThreadFactory;
712
import java.net.URL;
13+
import java.security.AccessControlContext;
14+
import java.security.ProtectionDomain;
815
import java.time.Duration;
916
import java.util.Optional;
17+
import java.util.concurrent.Executor;
18+
import java.util.concurrent.Executors;
19+
import java.util.concurrent.ForkJoinPool;
20+
import java.util.concurrent.ForkJoinWorkerThread;
21+
import java.util.concurrent.ThreadFactory;
1022
import java.util.function.Supplier;
1123
import java.util.logging.Level;
1224
import java.util.logging.Logger;
@@ -23,6 +35,51 @@ class SandboxResolvingClassLoader extends ClassLoader {
2335

2436
private static final Logger LOGGER = Logger.getLogger(SandboxResolvingClassLoader.class.getName());
2537

38+
/**
39+
* Care must be taken to avoid leaking instances of {@link GroovyClassLoader} when computing the cached value.
40+
* This can happen in several ways, depending on the Caffeine configuration:
41+
*
42+
* <ul>
43+
* <li>In its default configuration, Caffeine uses {@link ForkJoinPool#commonPool} as its {@link Executor}.
44+
* As of recent Java versions, {@link ForkJoinPool} can capture a reference to {@link GroovyClassLoader} by
45+
* creating a {@link ForkJoinWorkerThread} whose {@link Thread#inheritedAccessControlContext} refers to an
46+
* {@link AccessControlContext} whose {@link ProtectionDomain} refers to {@link GroovyClassLoader}.
47+
* <li>When Caffeine is configured with an {@link Executor} returned by {@link Executors#newCachedThreadPool},
48+
* that {@link Executor} can capture a reference to {@link GroovyClassLoader} by creating a {@link Thread}
49+
* whose {@link Thread#inheritedAccessControlContext} refers to an {@link AccessControlContext} whose {@link
50+
* ProtectionDomain} refers to {@link GroovyClassLoader}. Additionally, when the thread pool's {@link
51+
* ThreadFactory} is not wrapped by {@link ClassLoaderSanityThreadFactory}, the {@link Executor} can sometimes
52+
* create {@link Thread} instances whose {@link Thread#contextClassLoader} refers to {@link
53+
* GroovyClassLoader}.
54+
* </ul>
55+
*
56+
* As of <a href="https://openjdk.org/jeps/411">JEP-411</a>, {@link Thread#inheritedAccessControlContext} is
57+
* deprecated for removal, but in the meantime we must contend with this issue. We therefore create a dedicated
58+
* {@link Executors#newSingleThreadExecutor}, which is safe for use with Caffeine from a memory perspective because:
59+
*
60+
* <ul>
61+
* <li>In contrast to {@link ForkJoinPool#commonPool}, the thread is eagerly created and avoids references to
62+
* {@link GroovyClassLoader} in {@link Thread#inheritedAccessControlContext}.
63+
* <li>In contrast to {@link Executors#newCachedThreadPool}, the thread is eagerly created and avoids references
64+
* to {@link GroovyClassLoader} in {@link Thread#inheritedAccessControlContext}.
65+
* <li>In contrast to {@link Executors#newCachedThreadPool}, the thread is eagerly created and avoids references
66+
* to {@link GroovyClassLoader} in {@link Thread#contextClassLoader}, thereby avoiding the need for {@link
67+
* ClassLoaderSanityThreadFactory}.
68+
* </ul>
69+
*
70+
* A single-threaded {@link Executor} is safe for use with Caffeine from a CPU perspective because <a
71+
* href="https://stackoverflow.com/a/68105121">the cache's work is implemented with cheap O(1) algorithms</a>.
72+
*
73+
* <p>In the medium term, once {@link Thread#inheritedAccessControlContext} is removed upstream, we could possibly
74+
* switch to a combination of {@link Executors#newCachedThreadPool} and {@link ClassLoaderSanityThreadFactory}.
75+
*
76+
* <p>In the long term, a listener should be added to inform this class when dynamically installed plugins become
77+
* available, as described in the comments to {@link #makeParentCache(boolean)}, in which case the use of Caffeine
78+
* could possibly be removed entirely.
79+
*/
80+
private static final Executor cacheExecutor = Executors.newSingleThreadExecutor(new NamingThreadFactory(
81+
new DaemonThreadFactory(), SandboxResolvingClassLoader.class.getName() + ".cacheExecutor"));
82+
2683
static final LoadingCache<ClassLoader, Cache<String, Class<?>>> parentClassCache = makeParentCache(true);
2784

2885
static final LoadingCache<ClassLoader, Cache<String, Optional<URL>>> parentResourceCache = makeParentCache(false);
@@ -98,16 +155,35 @@ private static <T> T load(LoadingCache<ClassLoader, Cache<String, T>> cache, Str
98155
private static <T> LoadingCache<ClassLoader, Cache<String, T>> makeParentCache(boolean weakValuesInnerCache) {
99156
// The outer cache has weak keys, so that we do not leak class loaders, but strong values, because the
100157
// inner caches are only referenced by the outer cache internally.
101-
Caffeine<Object, Object> outerBuilder = Caffeine.newBuilder().recordStats().weakKeys();
158+
Caffeine<Object, Object> outerBuilder = Caffeine.newBuilder()
159+
.executor(cacheExecutor)
160+
.scheduler(Scheduler.systemScheduler())
161+
.recordStats()
162+
.weakKeys();
102163
// The inner cache has strong keys, since they are just strings, and expires entries 15 minutes after they are
103164
// added to the cache, so that classes defined by dynamically installed plugins become available even if there
104-
// were negative cache hits prior to the installation (ideally this would be done with a listener). The values
105-
// for the inner cache may be weak if needed, for example parentClassCache uses weak values to avoid leaking
106-
// classes and their loaders.
107-
Caffeine<Object, Object> innerBuilder = Caffeine.newBuilder().recordStats().expireAfterWrite(Duration.ofMinutes(15));
165+
// were negative cache hits prior to the installation (ideally this would be done with a listener, in which case
166+
// this two-level Caffeine cache could possibly be replaced by something based on ClassValue, like
167+
// org.kohsuke.stapler.ClassLoaderValue). The values for the inner cache may be weak if needed; for example,
168+
// parentClassCache uses weak values to avoid leaking classes and their loaders.
169+
Caffeine<Object, Object> innerBuilder = Caffeine.newBuilder()
170+
.executor(cacheExecutor)
171+
.scheduler(Scheduler.systemScheduler())
172+
.recordStats()
173+
.expireAfterWrite(Duration.ofMinutes(15));
108174
if (weakValuesInnerCache) {
109175
innerBuilder.weakValues();
110176
}
177+
// In both cases above, note that by default Caffeine does not perform cleanup and evict values "automatically"
178+
// or instantly after a value expires. Instead, it performs small amounts of maintenance work after write
179+
// operations (or occasionally after read operations if writes are rare). When Caffeine is configured with its
180+
// default Executor of ForkJoinPool#commonPool, it immediately schedules an asynchronous eviction event after
181+
// such write operations; however, when using a custom executor, a scheduler is required in order to run the
182+
// maintenance activity in the near future rather than deferring it to a subsequent cache operation. Since
183+
// Caffeine does not define a default scheduler, we explicitly configure its scheduler to the recommended
184+
// dedicated system-wide Scheduler#systemScheduler. This preserves, as closely as possible, Caffeine's behavior
185+
// when using ForkJoinPool#commonPool. See
186+
// com.github.benmanes.caffeine.cache.BoundedLocalCache#rescheduleCleanUpIfIncomplete for details.
111187

112188
return outerBuilder.build(parentLoader -> innerBuilder.build());
113189
}

0 commit comments

Comments
 (0)