graphql-java / graphql-java/java-dataloader

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

未关闭
#266 1 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

还没有人认领这个 Issue。

keep-open
主要语言
Java
星标
525
派生
101
PR 合并指标
30 天内没有已合并 PR

描述

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();
    }
}

贡献指南

这个仓库没有索引到贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

调研方向

首先阅读 ThreadLocalStatisticsCollector,尤其是其对 ThreadLocal 的使用、getStatistics() 和 resetThread()。运行提供的 fixed-thread-pool 复现程序,以验证跳过 resetThread() 时统计信息是否会在请求之间持续存在。Done 应包括一种已确定的方式来防止请求之间的污染和保留,并包含覆盖所复现​​场景的测试。

由索引模型根据 Issue 内容生成。

评估

技术栈
java
领域
backend, observability
Issue 类型
缺陷
难度
4/5
预计耗时
3-5 天
活跃度
冷清
描述清晰度
需要澄清
新手友好度
35/100

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。