mirror of
https://github.com/penpot/penpot.git
synced 2026-09-18 18:06:14 +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
|
### :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)
|
- 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
|
- 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)
|
- 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 {
|
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;
|
private static readonly SESSION_TIMEOUT_MINUTES = 60;
|
||||||
|
|
||||||
@ -65,7 +65,10 @@ export class PenpotMcpServer {
|
|||||||
private readonly sessionContext = new AsyncLocalStorage<SessionContext>();
|
private readonly sessionContext = new AsyncLocalStorage<SessionContext>();
|
||||||
|
|
||||||
private readonly streamableTransports: Record<string, StreamableSession> = {};
|
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 host: string;
|
||||||
public readonly port: number;
|
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}.
|
* idle for longer than {@link SESSION_TIMEOUT_MINUTES}.
|
||||||
*/
|
*/
|
||||||
private startSessionTimeoutChecker(): void {
|
private startSessionTimeoutChecker(): void {
|
||||||
@ -195,8 +198,18 @@ export class PenpotMcpServer {
|
|||||||
removed++;
|
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(
|
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);
|
}, checkIntervalMs);
|
||||||
}
|
}
|
||||||
@ -262,7 +275,7 @@ export class PenpotMcpServer {
|
|||||||
|
|
||||||
await this.sessionContext.run({ userToken }, async () => {
|
await this.sessionContext.run({ userToken }, async () => {
|
||||||
const transport = new SSEServerTransport("/messages", res);
|
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();
|
const server = this.createMcpServer();
|
||||||
await server.connect(transport);
|
await server.connect(transport);
|
||||||
@ -281,6 +294,7 @@ export class PenpotMcpServer {
|
|||||||
const session = this.sseTransports[sessionId];
|
const session = this.sseTransports[sessionId];
|
||||||
|
|
||||||
if (session) {
|
if (session) {
|
||||||
|
session.lastActiveTime = Date.now();
|
||||||
await this.sessionContext.run({ userToken: session.userToken }, async () => {
|
await this.sessionContext.run({ userToken: session.userToken }, async () => {
|
||||||
await session.transport.handlePostMessage(req, res, req.body);
|
await session.transport.handlePostMessage(req, res, req.body);
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user