graphql-java / graphql-java/java-dataloader

[Bug] Memory Leak and Stats Pollution via unmanaged ThreadLocal in ThreadLocalStatisticsCollector

Offen
#266 1 Kommentar 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen

Dieses Issue hat noch niemand übernommen.

keep-open
Vorherrschende Sprache
Java
Sterne
525
Forks
101
PR-Merge-Kennzahlen
Keine gemergten PRs in 30 T.

Beschreibung

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:

  1. Data Pollution: Subsequent requests processed by the dirty thread will inherit the accumulated statistics of previous requests, leading to completely distorted metrics.
  2. 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):

import org.dataloader.stats.ThreadLocalStatisticsCollector;
import java.util.concurrent.*;

public class UTLReproduction {
    public static void main(String[] args) throws Exception {
        ThreadLocalStatisticsCollector collector = new ThreadLocalStatisticsCollector();
        // Simulate a web server with a reusable thread pool
        ExecutorService threadPool = 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 thread
        threadPool.submit(() -> {
            long loadCount = 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();
    }
}

Beitragsleitfaden

Für dieses Repository ist kein Beitragsleitfaden indexiert

Erste Schritte

  1. Lies das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreib ins Issue, dass du es übernimmst — das erspart doppelte Arbeit.
  3. Forke das Repository und arbeite in einem Branch.
  4. Öffne einen Pull Request, der die Issue-Nummer nennt.

Rechercherichtung

Beginne mit dem Lesen von ThreadLocalStatisticsCollector, insbesondere seiner Verwendung von ThreadLocal sowie getStatistics() und resetThread(). Führe die bereitgestellte Reproduktion mit einem festen Thread-Pool aus, um zu überprüfen, ob Statistiken zwischen Requests bestehen bleiben, wenn resetThread() übersprungen wird. Done sollte eine festgelegte Vorgehensweise zur Verhinderung von Verschmutzung und Beibehaltung über Requests hinweg enthalten, mit Tests, die das reproduzierte Szenario abdecken.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Bewertung

Tech-Stack
java
Bereich
backend, observability
Issue-Typ
Bug
Schwierigkeit
4/5
Geschätzter Aufwand
3-5 Tage
Aktivitätsstatus
Ruhig
Klarheit
Muss geklärt werden
Anfängerfreundlichkeit
35/100

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.