graphql-java / graphql-java/java-dataloader

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

Đang mở
#266 1 bình luận 0 reaction 0 người được giao Xem trên GitHub

Chưa có ai nhận issue này.

keep-open
Ngôn ngữ chính
Java
Star
525
Fork
101
Chỉ số merge pull request
Không có pull request nào được merge trong 30 ngày

Mô tả

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

Hướng dẫn đóng góp

Chưa lập chỉ mục được hướng dẫn đóng góp cho kho mã nguồn này

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Hướng nghiên cứu

Bắt đầu bằng cách đọc ThreadLocalStatisticsCollector, đặc biệt là cách sử dụng ThreadLocal, getStatistics() và resetThread(). Chạy bản tái hiện được cung cấp với một fixed-thread-pool để xác minh liệu các thống kê có tồn tại giữa các requests khi bỏ qua resetThread() hay không. Done phải bao gồm một cách đã được quyết định để ngăn chặn việc nhiễm bẩn và lưu giữ giữa các requests, cùng với các test bao phủ kịch bản đã tái hiện.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Đánh giá

Công nghệ
java
Lĩnh vực
backend, observability
Loại issue
Lỗi
Độ khó
4/5
Thời gian dự kiến
3-5 ngày
Mức độ hoạt động
Ít trao đổi
Độ rõ ràng
Cần làm rõ
Mức phù hợp với người mới
35/100

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.