You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Bug] Memory Leak and Stats Pollution via unmanaged ThreadLocal in ThreadLocalStatisticsCollector #266
Describe the bug
There is a potential Unintentional ThreadLocal Leak (UTL) in ThreadLocalStatisticsCollector. The class relies on consumers manually calling resetThread() at "request boundaries" to clear the ThreadLocal<SimpleStatisticsCollector>.
However, in typical GraphQL environments (reactive streams, async gateways, or standard thread pools), threads are heavily reused. If an unhandled exception occurs or a developer forgets to call resetThread(), the SimpleStatisticsCollector object is permanently retained by the worker thread.
Impact:
Data Pollution: Subsequent requests processed by the dirty thread will inherit the accumulated statistics of previous requests, leading to completely distorted metrics.
Memory Leak (Type I UTL): As more threads in the pool retain these un-cleared statistics objects over time, the heap usage will grow linearly, potentially leading to an OutOfMemoryError in high-throughput applications.
To Reproduce
Here is a simplified code example demonstrating the data pollution in a thread-pool environment when resetThread() is missed (e.g., bypassed due to an exception):
importorg.dataloader.stats.ThreadLocalStatisticsCollector;
importjava.util.concurrent.*;
publicclassUTLReproduction {
publicstaticvoidmain(String[] args) throwsException {
ThreadLocalStatisticsCollectorcollector = newThreadLocalStatisticsCollector();
// Simulate a web server with a reusable thread poolExecutorServicethreadPool = Executors.newFixedThreadPool(1);
// Request 1: Execution completes but resetThread() is missed (e.g. exception thrown)threadPool.submit(() -> {
collector.incrementLoadCount();
// Developer forgets to put collector.resetThread() in a finally block
}).get();
// Request 2: A new incoming request reuses the same dirty threadthreadPool.submit(() -> {
longloadCount = collector.getStatistics().getLoadCount();
// BUG: loadCount is 1 instead of 0! The new request is polluted by Request 1.System.out.println("New Request Load Count (Expected 0): " + loadCount);
}).get();
threadPool.shutdown();
}
}
Hello, this issue has been inactive for 60 days, so we're marking it as stale. If you would like to continue this discussion, please comment within the next 30 days or we'll close the issue.
Activity
github-actions commentedon Apr 24, 2026
Hello, this issue has been inactive for 60 days, so we're marking it as stale. If you would like to continue this discussion, please comment within the next 30 days or we'll close the issue.