View Javadoc
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 }