3
3
import com .github .benmanes .caffeine .cache .Cache ;
4
4
import com .github .benmanes .caffeine .cache .Caffeine ;
5
5
import com .github .benmanes .caffeine .cache .LoadingCache ;
6
+ import com .github .benmanes .caffeine .cache .Scheduler ;
7
+ import groovy .lang .GroovyClassLoader ;
6
8
import groovy .lang .GroovyShell ;
9
+ import hudson .util .ClassLoaderSanityThreadFactory ;
10
+ import hudson .util .DaemonThreadFactory ;
11
+ import hudson .util .NamingThreadFactory ;
7
12
import java .net .URL ;
13
+ import java .security .AccessControlContext ;
14
+ import java .security .ProtectionDomain ;
8
15
import java .time .Duration ;
9
16
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 ;
10
22
import java .util .function .Supplier ;
11
23
import java .util .logging .Level ;
12
24
import java .util .logging .Logger ;
@@ -23,6 +35,51 @@ class SandboxResolvingClassLoader extends ClassLoader {
23
35
24
36
private static final Logger LOGGER = Logger .getLogger (SandboxResolvingClassLoader .class .getName ());
25
37
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
+
26
83
static final LoadingCache <ClassLoader , Cache <String , Class <?>>> parentClassCache = makeParentCache (true );
27
84
28
85
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
98
155
private static <T > LoadingCache <ClassLoader , Cache <String , T >> makeParentCache (boolean weakValuesInnerCache ) {
99
156
// The outer cache has weak keys, so that we do not leak class loaders, but strong values, because the
100
157
// 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 ();
102
163
// The inner cache has strong keys, since they are just strings, and expires entries 15 minutes after they are
103
164
// 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 ));
108
174
if (weakValuesInnerCache ) {
109
175
innerBuilder .weakValues ();
110
176
}
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.
111
187
112
188
return outerBuilder .build (parentLoader -> innerBuilder .build ());
113
189
}
0 commit comments