1 /*
2 * Copyright 2025 The Netty Project
3 *
4 * The Netty Project licenses this file to you under the Apache License,
5 * version 2.0 (the "License"); you may not use this file except in compliance
6 * with the License. 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, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations
14 * under the License.
15 */
16
17 package io.netty.util;
18
19 import io.netty.util.internal.SystemPropertyUtil;
20
21 import java.io.Closeable;
22 import java.util.Map;
23 import java.util.concurrent.ConcurrentHashMap;
24 import java.util.concurrent.atomic.AtomicBoolean;
25 import java.util.concurrent.atomic.LongAdder;
26 import java.util.function.Supplier;
27
28 /**
29 * Alternative leak detector implementation for reliable and performant detection in tests.
30 *
31 * <h3>Background</h3>
32 * <p>
33 * The standard {@link ResourceLeakDetector} produces no "false positives", but this comes with tradeoffs. You either
34 * get many false negatives because only a small sample of buffers is instrumented, or you turn on paranoid detection
35 * which carries a somewhat heavy performance cost with each allocation. Additionally, paranoid detection enables
36 * detailed recording of buffer access operations with heavy performance impact. Avoiding false negatives is necessary
37 * for (unit, fuzz...) testing if bugs should lead to reliable test failures, but the performance impact can be
38 * prohibitive for some tests.
39 *
40 * <h3>The presence detector</h3>
41 * <p>
42 * The <i>leak presence detector</i> takes a different approach. It foregoes detailed tracking of allocation and
43 * modification stack traces. In return every resource is counted, so there are no false negatives where a leak would
44 * not be detected.
45 * <p>
46 * The presence detector also does not wait for an unclosed resource to be garbage collected before it's reported as
47 * leaked. This ensures that leaks are detected promptly and can be directly associated with a particular test, but it
48 * can lead to false positives. Tests that use the presence detector must shut down completely <i>before</i> checking
49 * for resource leaks. There are also complications with static fields, described below.
50 *
51 * <h3>Resource Scopes</h3>
52 * <p>
53 * A resource scope manages all resources of a set of threads over time. On allocation, a resource is assigned to a
54 * scope through the {@link #currentScope()} method. When {@link #check()} is called, or the scope is
55 * {@link ResourceScope#close() closed}, all resources in that scope must have been released.
56 * <p>
57 * By default, there is only a single "global" scope, and when {@link #check()} is called, all resources in the entire
58 * JVM must have been released. To enable parallel test execution, it may be necessary to use separate scopes for
59 * separate tests instead, so that one test can check for its own leaks while another test is still in progress. You
60 * can override {@link #currentScope()} to implement this for your test framework.
61 *
62 * <h3>Static Fields</h3>
63 * <p>
64 * While the presence detector requires that <i>all</i> resources be closed after a test, some resources kept in static
65 * fields cannot be released, or there would be false positives. To avoid this, resources created inside static
66 * initializers, specifically when the allocation stack trace contains a {@code <clinit>} method, <i>are not
67 * tracked</i>.
68 * <p>
69 * Because the presence detector does not normally capture or introspect allocation stack traces, additional
70 * cooperation is required. Any static initializer must be wrapped in a {@link #staticInitializer(Supplier)} call,
71 * which will temporarily enable stack trace introspection. For example:
72 * <pre>{@code
73 * private static final ByteBuf CRLF_BUF = LeakPresenceDetector.staticInitializer(() -> unreleasableBuffer(
74 * directBuffer(2).writeByte(CR).writeByte(LF)).asReadOnly());
75 * }</pre>
76 * <p>
77 * Since stack traces are not captured by default, it can be difficult to tell apart a real leak from a missed static
78 * initializer. You can temporarily turn on allocation stack trace capture using the
79 * {@code -Dio.netty.util.LeakPresenceDetector.trackCreationStack=true} system property.
80 *
81 * @param <T> The resource type to detect
82 */
83 public class LeakPresenceDetector<T> extends ResourceLeakDetector<T> {
84 private static final String TRACK_CREATION_STACK_PROPERTY = "io.netty.util.LeakPresenceDetector.trackCreationStack";
85 private static final boolean TRACK_CREATION_STACK =
86 SystemPropertyUtil.getBoolean(TRACK_CREATION_STACK_PROPERTY, false);
87 private static final ResourceScope GLOBAL = new ResourceScope("global");
88
89 private static int staticInitializerCount;
90
91 private static boolean inStaticInitializerSlow(StackTraceElement[] stackTrace) {
92 for (StackTraceElement element : stackTrace) {
93 if (element.getMethodName().equals("<clinit>")) {
94 return true;
95 }
96 }
97 return false;
98 }
99
100 private static boolean inStaticInitializerFast() {
101 // This plain field access is safe. The worst that can happen is that we see non-zero where we shouldn't.
102 return staticInitializerCount != 0 && inStaticInitializerSlow(Thread.currentThread().getStackTrace());
103 }
104
105 /**
106 * Wrap a static initializer so that any resources created inside the block will not be tracked. Example:
107 * <pre>{@code
108 * private static final ByteBuf CRLF_BUF = LeakPresenceDetector.staticInitializer(() -> unreleasableBuffer(
109 * directBuffer(2).writeByte(CR).writeByte(LF)).asReadOnly());
110 * }</pre>
111 * <p>
112 * Note that technically, this method does not actually care what happens inside the block. Instead, it turns on
113 * stack trace introspection at the start of the block, and turns it back off at the end. Any allocation in that
114 * interval will be checked to see whether it is part of a static initializer, and if it is, it will not be
115 * tracked.
116 *
117 * @param supplier A code block to run
118 * @return The value returned by the {@code supplier}
119 * @param <R> The supplier return type
120 */
121 public static <R> R staticInitializer(Supplier<R> supplier) {
122 if (!inStaticInitializerSlow(Thread.currentThread().getStackTrace())) {
123 throw new IllegalStateException("Not in static initializer.");
124 }
125 synchronized (LeakPresenceDetector.class) {
126 staticInitializerCount++;
127 }
128 try {
129 return supplier.get();
130 } finally {
131 synchronized (LeakPresenceDetector.class) {
132 staticInitializerCount--;
133 }
134 }
135 }
136
137 /**
138 * Create a new detector for the given resource type.
139 *
140 * @param resourceType The resource type
141 */
142 public LeakPresenceDetector(Class<?> resourceType) {
143 super(resourceType, 0);
144 }
145
146 /**
147 * This constructor should not be used directly, it is called reflectively by {@link ResourceLeakDetectorFactory}.
148 *
149 * @param resourceType The resource type
150 * @param samplingInterval Ignored
151 */
152 @Deprecated
153 @SuppressWarnings("unused")
154 public LeakPresenceDetector(Class<?> resourceType, int samplingInterval) {
155 this(resourceType);
156 }
157
158 /**
159 * This constructor should not be used directly, it is called reflectively by {@link ResourceLeakDetectorFactory}.
160 *
161 * @param resourceType The resource type
162 * @param samplingInterval Ignored
163 * @param maxActive Ignored
164 */
165 @SuppressWarnings("unused")
166 public LeakPresenceDetector(Class<?> resourceType, int samplingInterval, long maxActive) {
167 this(resourceType);
168 }
169
170 /**
171 * Get the resource scope for the current thread. This is used to assign resources to scopes, and it is used by
172 * {@link #check()} to tell which scope to check for open resources. By default, the global scope is returned.
173 *
174 * @return The resource scope to use
175 */
176 protected ResourceScope currentScope() {
177 return GLOBAL;
178 }
179
180 @Override
181 public final ResourceLeakTracker<T> track(T obj) {
182 if (inStaticInitializerFast()) {
183 return null;
184 }
185 return trackForcibly(obj);
186 }
187
188 @Override
189 public final ResourceLeakTracker<T> trackForcibly(T obj) {
190 return new PresenceTracker<>(currentScope());
191 }
192
193 @Override
194 public final boolean isRecordEnabled() {
195 return false;
196 }
197
198 /**
199 * Check the current leak presence detector scope for open resources. If any resources remain unclosed, an
200 * exception is thrown.
201 *
202 * @throws IllegalStateException If there is a leak, or if the leak detector is not a {@link LeakPresenceDetector}.
203 */
204 public static void check() {
205 // for LeakPresenceDetector, this is cheap.
206 ResourceLeakDetector<Object> detector = ResourceLeakDetectorFactory.instance()
207 .newResourceLeakDetector(Object.class);
208
209 if (!(detector instanceof LeakPresenceDetector)) {
210 throw new IllegalStateException(
211 "LeakPresenceDetector not in use. Please register it using " +
212 "-Dio.netty.customResourceLeakDetector=" + LeakPresenceDetector.class.getName());
213 }
214
215 //noinspection resource
216 ((LeakPresenceDetector<Object>) detector).currentScope().check();
217 }
218
219 private static final class PresenceTracker<T> extends AtomicBoolean implements ResourceLeakTracker<T> {
220 private final ResourceScope scope;
221
222 PresenceTracker(ResourceScope scope) {
223 super(false);
224 this.scope = scope;
225
226 scope.checkOpen();
227
228 scope.openResourceCounter.increment();
229 if (TRACK_CREATION_STACK) {
230 scope.creationStacks.put(this, new LeakCreation());
231 }
232 }
233
234 @Override
235 public void record() {
236 }
237
238 @Override
239 public void record(Object hint) {
240 }
241
242 @Override
243 public boolean close(Object trackedObject) {
244 if (compareAndSet(false, true)) {
245 scope.openResourceCounter.decrement();
246 if (TRACK_CREATION_STACK) {
247 scope.creationStacks.remove(this);
248 }
249 scope.checkOpen();
250 return true;
251 }
252 return false;
253 }
254 }
255
256 /**
257 * A resource scope keeps track of the resources for a particular set of threads. Different scopes can be checked
258 * for leaks separately, to enable parallel test execution.
259 */
260 public static final class ResourceScope implements Closeable {
261 final String name;
262 final LongAdder openResourceCounter = new LongAdder();
263 final Map<PresenceTracker<?>, Throwable> creationStacks =
264 TRACK_CREATION_STACK ? new ConcurrentHashMap<>() : null;
265 boolean closed;
266
267 /**
268 * Create a new scope.
269 *
270 * @param name The scope name, used for error reporting
271 */
272 public ResourceScope(String name) {
273 this.name = name;
274 }
275
276 void checkOpen() {
277 if (closed) {
278 throw new IllegalStateException("Resource scope '" + name + "' already closed");
279 }
280 }
281
282 void check() {
283 long n = openResourceCounter.sumThenReset();
284 if (n != 0) {
285 StringBuilder msg = new StringBuilder("Possible memory leak detected for resource scope '")
286 .append(name).append("'. ");
287 if (n < 0) {
288 msg.append("Resource count was negative: A resource previously reported as a leak was released " +
289 "after all. Please ensure that that resource is released before its test finishes.");
290 throw new IllegalStateException(msg.toString());
291 }
292 if (TRACK_CREATION_STACK) {
293 msg.append("Creation stack traces:");
294 IllegalStateException ise = new IllegalStateException(msg.toString());
295 int i = 0;
296 for (Throwable t : creationStacks.values()) {
297 ise.addSuppressed(t);
298 if (i++ > 5) {
299 break;
300 }
301 }
302 creationStacks.clear();
303 throw ise;
304 }
305 msg.append("Please use paranoid leak detection to get more information, or set " +
306 "-D" + TRACK_CREATION_STACK_PROPERTY + "=true");
307 throw new IllegalStateException(msg.toString());
308 }
309 }
310
311 /**
312 * Check whether there are any open resources left, and {@link #close()} would throw.
313 *
314 * @return {@code true} if there are open resources
315 */
316 public boolean hasOpenResources() {
317 return openResourceCounter.sum() > 0;
318 }
319
320 /**
321 * Close this scope. Closing a scope will prevent new resources from being allocated (or released) in this
322 * scope. The call also throws an exception if there are any resources left open.
323 */
324 @Override
325 public void close() {
326 closed = true;
327 check();
328 }
329 }
330
331 private static final class LeakCreation extends Throwable {
332 final Thread thread = Thread.currentThread();
333 String message;
334
335 @Override
336 public synchronized String getMessage() {
337 if (message == null) {
338 if (inStaticInitializerSlow(getStackTrace())) {
339 message = "Resource created in static initializer. Please wrap the static initializer in " +
340 "LeakPresenceDetector.staticInitializer so that this resource is excluded.";
341 } else {
342 message = "Resource created outside static initializer on thread '" + thread.getName() +
343 "', likely leak.";
344 }
345 }
346 return message;
347 }
348 }
349 }