modelcontextprotocol / modelcontextprotocol/java-sdk

HttpClient resource leak causes thread accumulation and memory exhaustion

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

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

area/client area/transport bug
Ngôn ngữ chính
Java
Star
3.7k
Fork
1.1k
Merge trung bình
1 ngày 15 giờ
Pull request đã merge (30 ngày)
9

Mô tả

Bug description

When using HttpClientStreamableHttpTransport and HttpClientSseClientTransport, the application experiences continuous accumulation of HttpClient-xxxx-SelectorManager threads that are never cleaned up, eventually leading to memory exhaustion and application instability.

The root cause is that each transport builder creates a new HttpClient instance via HttpClient.Builder.build(), but these HttpClient instances are never properly closed when the transport shuts down. Each HttpClient spawns dedicated SelectorManager threads for network I/O operations, and since OpenJDK's HttpClient lacks public APIs for resource cleanup, these threads remain active indefinitely.

Technical Details: Tracing through HttpClientStreamableHttpTransport#build() reveals that each HttpClient instantiation triggers the creation of a SelectorManager thread in the OpenJDK 17 source code:

SelectorManager(HttpClientImpl ref) throws IOException {
    super(null, null,
          "HttpClient-" + ref.id + "-SelectorManager",
          0, false);
    owner = ref;
    debug = ref.debug;
    debugtimeout = ref.debugtimeout;
    pool = ref.connectionPool();
    registrations = new ArrayList<>();
    deregistrations = new ArrayList<>();
    selector = Selector.open();
}

Source: OpenJDK 17 HttpClientImpl.java

This constructor shows how each HttpClient creates a uniquely named SelectorManager thread ("HttpClient-" + ref.id + "-SelectorManager"), which explains the observed thread naming pattern in production environments.

Environment

  • Spring MCP Version: Latest (current main branch)
  • Java Version: OpenJDK 17+ (tested on OpenJDK 17.0.14)
  • Operating System: macOS 14.6.0 (also reproducible on Linux)
  • Transport Types: HttpClientStreamableHttpTransport, HttpClientSseClientTransport
  • Related OpenJDK Issue: JDK-8308364

Steps to reproduce

  1. Create multiple HttpClientStreamableHttpTransport instances:
for (int i = 0; i < 10; i++) {
    HttpClientStreamableHttpTransport transport = HttpClientStreamableHttpTransport
        .builder("http://localhost:8080")
        .build();
    
    McpSyncClient client = McpClient.sync(transport).build();
    client.initialize();
    client.closeGracefully(); // This doesn't clean up HttpClient threads
}
  1. Monitor system threads using jstack or thread monitoring tools
  2. Observe continuous growth of HttpClient-xxxx-SelectorManager threads
  3. Repeat the process multiple times to see thread accumulation

Expected behavior

  • When transport.closeGracefully() is called, all associated HttpClient resources should be cleaned up
  • HttpClient-xxxx-SelectorManager threads should be terminated and not accumulate
  • Memory usage should remain stable across multiple transport creation/destruction cycles
  • No thread leakage should occur in long-running applications

Minimal Complete Reproducible example

import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;

public class HttpClientLeakDemo {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("Initial thread count: " + Thread.activeCount());
        
        // Create and close multiple transports
        for (int i = 0; i < 20; i++) {
            System.out.println("\n=== Creating transport " + (i + 1) + " ===");
            
            var transport = HttpClientSseClientTransport
                .builder("http://127.0.0.1:8002") // your sse mcp server base url
                .build();
            
            McpSyncClient client = McpClient.sync(transport)
                .requestTimeout(Duration.ofSeconds(5))
                .build();
            
            try {
                // This will fail but still creates the HttpClient
                client.initialize();
            } catch (Exception e) {
                System.out.println("Expected initialization failure: " + e.getMessage());
            }
            
            // Close the client - this should clean up resources but doesn't
            client.closeGracefully();
            
            System.out.println("Thread count after closing transport " + (i + 1) + ": " + Thread.activeCount());
            
            // List HttpClient threads
            Thread.getAllStackTraces().keySet().stream()
                .filter(t -> t.getName().contains("HttpClient") && t.getName().contains("SelectorManager"))
                .forEach(t -> System.out.println("  - " + t.getName()));
        }
        
        System.out.println("\nFinal thread count: " + Thread.activeCount());
        System.out.println("HttpClient SelectorManager threads are still running and will never be cleaned up!");
        
        // Force GC to confirm threads are not cleaned up
        while (true) {
            Thread.getAllStackTraces().keySet().stream()
                    .filter(t -> t.getName().contains("HttpClient") && t.getName().contains("SelectorManager"))
                    .forEach(t -> System.out.println("  - " + t.getName()));
            System.gc();
            Thread.sleep(1000);
            System.out.println("Thread count after GC: " + Thread.activeCount());
        }
    }
}

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

Mở hướng dẫn đóng góp

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 HttpClientStreamableHttpTransport#build() và builder tương ứng của HttpClientSseClientTransport, sau đó lần theo cách client.closeGracefully() đi đến quá trình shutdown của transport. So sánh vòng đời của từng HttpClient được tạo với các thread SelectorManager quan sát được. Hoàn thành khi việc tạo và đóng transport lặp đi lặp lại không còn tích lũy các thread đó hoặc làm cạn kiệt bộ nhớ.

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
networking
Loại issue
Lỗi
Độ khó
5/5
Thời gian dự kiến
Hơn một tuần
Mức độ hoạt động
Sôi nổi
Độ rõ ràng
Khá rõ ràng
Mức phù hợp với người mới
38/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.