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 }