mirror of
https://github.com/penpot/penpot.git
synced 2026-05-11 19:13:49 +00:00
SSE sessions were never included in the periodic inactivity timeout
checker, so a stale connection whose TCP close event never fired would
retain its SSEServerTransport and McpServer indefinitely.
Changes:
- Add lastActiveTime: number to the sseTransports entry type
- Initialise lastActiveTime at SSE session creation (GET /sse)
- Refresh lastActiveTime on every incoming message (POST /messages)
- Extend startSessionTimeoutChecker() to sweep and forcibly close SSE
sessions idle for more than SESSION_TIMEOUT_MINUTES, mirroring the
existing Streamable HTTP logic
- Update the checker log to count both transport maps
The existing res.on('close') cleanup path is preserved unchanged:
it remains the primary cleanup for normal disconnections; the timer
is a safety net for zombie sessions only.
Closes #9432
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
This commit is contained in:
parent
b54fa2f11c
commit
58ca0a16ba
@ -13,6 +13,7 @@
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix MCP SSE sessions leaking memory on zombie connections by adding inactivity timeout parity with Streamable HTTP sessions (by @bitloi) [Github #9432](https://github.com/penpot/penpot/issues/9432)
|
||||
- Fix missing `labels.open` translation (by @MilosM348) [Github #9320](https://github.com/penpot/penpot/pull/9320)
|
||||
- Harden Nginx responses with standard security headers and hide upstream `X-Powered-By` headers
|
||||
- Fix plugin API `shape.fills` and `shape.strokes` arrays being read-only [Github #8357](https://github.com/penpot/penpot/issues/8357)
|
||||
|
||||
@ -45,7 +45,7 @@ class ToolInfo {
|
||||
|
||||
export class PenpotMcpServer {
|
||||
/**
|
||||
* Timeout, in minutes, for idle Streamable HTTP sessions before they are automatically closed and removed.
|
||||
* Timeout, in minutes, for idle sessions (Streamable HTTP and SSE) before they are automatically closed and removed.
|
||||
*/
|
||||
private static readonly SESSION_TIMEOUT_MINUTES = 60;
|
||||
|
||||
@ -65,7 +65,10 @@ export class PenpotMcpServer {
|
||||
private readonly sessionContext = new AsyncLocalStorage<SessionContext>();
|
||||
|
||||
private readonly streamableTransports: Record<string, StreamableSession> = {};
|
||||
private readonly sseTransports: Record<string, { transport: SSEServerTransport; userToken?: string }> = {};
|
||||
private readonly sseTransports: Record<
|
||||
string,
|
||||
{ transport: SSEServerTransport; userToken?: string; lastActiveTime: number }
|
||||
> = {};
|
||||
|
||||
public readonly host: string;
|
||||
public readonly port: number;
|
||||
@ -179,7 +182,7 @@ export class PenpotMcpServer {
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a periodic timer that closes and removes Streamable HTTP sessions that have been
|
||||
* Starts a periodic timer that closes and removes Streamable HTTP and SSE sessions that have been
|
||||
* idle for longer than {@link SESSION_TIMEOUT_MINUTES}.
|
||||
*/
|
||||
private startSessionTimeoutChecker(): void {
|
||||
@ -195,8 +198,18 @@ export class PenpotMcpServer {
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
for (const [id, session] of Object.entries(this.sseTransports)) {
|
||||
if (now - session.lastActiveTime > timeoutMs) {
|
||||
this.logger.info(`Closing stale SSE session ${id}`);
|
||||
session.transport.close();
|
||||
delete this.sseTransports[id];
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
this.logger.info(
|
||||
`Removed ${removed} stale session(s); total sessions remaining: ${Object.keys(this.streamableTransports).length}`
|
||||
`Removed ${removed} stale session(s); total sessions remaining: ${
|
||||
Object.keys(this.streamableTransports).length + Object.keys(this.sseTransports).length
|
||||
}`
|
||||
);
|
||||
}, checkIntervalMs);
|
||||
}
|
||||
@ -262,7 +275,7 @@ export class PenpotMcpServer {
|
||||
|
||||
await this.sessionContext.run({ userToken }, async () => {
|
||||
const transport = new SSEServerTransport("/messages", res);
|
||||
this.sseTransports[transport.sessionId] = { transport, userToken };
|
||||
this.sseTransports[transport.sessionId] = { transport, userToken, lastActiveTime: Date.now() };
|
||||
|
||||
const server = this.createMcpServer();
|
||||
await server.connect(transport);
|
||||
@ -281,6 +294,7 @@ export class PenpotMcpServer {
|
||||
const session = this.sseTransports[sessionId];
|
||||
|
||||
if (session) {
|
||||
session.lastActiveTime = Date.now();
|
||||
await this.sessionContext.run({ userToken: session.userToken }, async () => {
|
||||
await session.transport.handlePostMessage(req, res, req.body);
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user