This is an automated email from the ASF dual-hosted git repository.
dengliming pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git
The following commit(s) were added to refs/heads/master by this push:
new d396e0fc1e fix: clean up restore-created mcp sessions after request to
prevent memory leak (#6644) (#7039)
d396e0fc1e is described below
commit d396e0fc1e698f5d5a684e0221e6d15eb4eacd4f
Author: wy471x <[email protected]>
AuthorDate: Thu Sep 17 16:28:33 2026 +0800
fix: clean up restore-created mcp sessions after request to prevent memory
leak (#6644) (#7039)
Add a doFinally cleanup hook to createSessionAndRestoreId so the
McpServerSession, StreamableHttpSessionTransport, and ShenyuMcpExchangeHolder
entry created for an unknown/stale sessionId are removed after the one-shot
request completes, mirroring createTemporarySessionAndProcess. Previously these
entries stayed in sessions/sessionTransports forever, allowing unbounded memory
growth.
Adds testStaleSessionRestoreCleansUpCreatedSession to verify the
restore-created session, transport, and exchange mapping are removed and that
follow-up requests are handled by a fresh restore.
close #6644
Co-authored-by: Liming Deng <[email protected]>
---
...henyuStreamableHttpServerTransportProvider.java | 10 ++++--
...uStreamableHttpServerTransportProviderTest.java | 40 ++++++++++++++++++++++
2 files changed, 48 insertions(+), 2 deletions(-)
diff --git
a/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/transport/ShenyuStreamableHttpServerTransportProvider.java
b/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/transport/ShenyuStreamableHttpServerTransportProvider.java
index 3ba4371c63..8773277d3d 100644
---
a/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/transport/ShenyuStreamableHttpServerTransportProvider.java
+++
b/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/transport/ShenyuStreamableHttpServerTransportProvider.java
@@ -452,10 +452,11 @@ public class ShenyuStreamableHttpServerTransportProvider
implements McpServerTra
* This method handles scenarios where a client provides a session ID that
no longer
* exists on the server (e.g., server restart, session timeout, network
disconnection).
* A new session is created using the MCP framework, which generates its
own session ID.
- * The client receives the new session ID for subsequent requests.
+ * The session is only used to process the current request and is cleaned
up afterwards,
+ * so an unknown or stale session ID cannot leave orphaned sessions in the
maps.
* Important: The MCP framework generates its own session IDs, so the
* client's requested session ID may differ from the actual session ID
returned.
- * The response includes the actual session ID that should be used for
future requests.
+ * The response includes the actual session ID used to process this
request.
*
* @param exchange the server web exchange
* @param message the JSON-RPC message
@@ -479,6 +480,11 @@ public class ShenyuStreamableHttpServerTransportProvider
implements McpServerTra
initializeSessionDirectly(newSession, actualSessionId);
newTransport.resetCapturedMessage();
return processWithExistingSession(newSession, actualSessionId,
message, messageId)
+ .doFinally(signalType -> {
+ LOGGER.debug("Cleaning up restored session: {}
(signal: {})", actualSessionId, signalType);
+ removeSession(actualSessionId);
+ ShenyuMcpExchangeHolder.remove(actualSessionId);
+ })
.map(result -> {
if (!actualSessionId.equals(requestedSessionId)) {
LOGGER.info("Returning actual session ID {}
instead of requested ID {}", actualSessionId, requestedSessionId);
diff --git
a/shenyu-plugin/shenyu-plugin-mcp-server/src/test/java/org/apache/shenyu/plugin/mcp/server/transport/ShenyuStreamableHttpServerTransportProviderTest.java
b/shenyu-plugin/shenyu-plugin-mcp-server/src/test/java/org/apache/shenyu/plugin/mcp/server/transport/ShenyuStreamableHttpServerTransportProviderTest.java
index 7d3bddb70b..2e17944c90 100644
---
a/shenyu-plugin/shenyu-plugin-mcp-server/src/test/java/org/apache/shenyu/plugin/mcp/server/transport/ShenyuStreamableHttpServerTransportProviderTest.java
+++
b/shenyu-plugin/shenyu-plugin-mcp-server/src/test/java/org/apache/shenyu/plugin/mcp/server/transport/ShenyuStreamableHttpServerTransportProviderTest.java
@@ -36,6 +36,7 @@ import
org.springframework.web.reactive.result.view.ViewResolver;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
+import java.lang.reflect.Field;
import java.time.Duration;
import java.util.Collections;
import java.util.List;
@@ -45,7 +46,9 @@ import java.util.Objects;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
@@ -172,6 +175,43 @@ class ShenyuStreamableHttpServerTransportProviderTest {
assertEquals("", notificationResponse.getBodyAsString().block());
}
+ /**
+ * A request carrying a session ID that no longer exists on the server
must not
+ * leave the restore-created session, transport, or exchange mapping
behind after
+ * the request completes.
+ */
+ @Test
+ void testStaleSessionRestoreCleansUpCreatedSession() throws Exception {
+ ShenyuStreamableHttpServerTransportProvider provider =
providerWithRealSessions();
+
+ MockServerHttpResponse response = performRequest(provider,
postRequest(TOOLS_LIST_REQUEST_BODY, "stale-session-1"));
+ assertEquals(HttpStatus.OK, response.getStatusCode());
+ assertTrue(response.getBodyAsString().block().contains("\"tools\""));
+ final String actualSessionId =
response.getHeaders().getFirst(SESSION_ID_HEADER);
+ assertNotNull(actualSessionId);
+
+ assertEquals(0, readMap(provider, "sessions").size());
+ assertEquals(0, readMap(provider, "sessionTransports").size());
+ assertNull(ShenyuMcpExchangeHolder.get(actualSessionId));
+
+ // The restored session is not reusable, so a follow-up request with
the
+ // returned session ID is handled by a fresh restore instead of the
same session.
+ MockServerHttpResponse followUp = performRequest(provider,
postRequest(TOOLS_LIST_REQUEST_BODY, actualSessionId));
+ assertEquals(HttpStatus.OK, followUp.getStatusCode());
+ assertTrue(followUp.getBodyAsString().block().contains("\"tools\""));
+ assertNotEquals(actualSessionId,
followUp.getHeaders().getFirst(SESSION_ID_HEADER));
+ assertEquals(0, readMap(provider, "sessions").size());
+ assertEquals(0, readMap(provider, "sessionTransports").size());
+ }
+
+ @SuppressWarnings("unchecked")
+ private Map<String, ?> readMap(final
ShenyuStreamableHttpServerTransportProvider provider, final String fieldName)
+ throws Exception {
+ final Field field = provider.getClass().getDeclaredField(fieldName);
+ field.setAccessible(true);
+ return (Map<String, ?>) field.get(provider);
+ }
+
private ShenyuStreamableHttpServerTransportProvider
providerWithRealSessions() {
ShenyuStreamableHttpServerTransportProvider provider =
new ShenyuStreamableHttpServerTransportProvider(new
ObjectMapper(), "/mcp/streamablehttp");