From f75599eba9484527cd6e53317b7e8b60b069fc66 Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Wed, 29 Apr 2026 05:53:34 -0700 Subject: [PATCH] fix(workspace_crud): drop restartStates entries on workspace delete (#2269) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per-workspace `restartState` entries (introduced under the name `restartMu` pre-#2266, renamed to `restartStates` in #2266) are created via `LoadOrStore` in `workspace_restart.go` but never deleted. On a long-running platform process serving many short-lived workspaces (E2E tests, transient sandbox tenants), the sync.Map grows monotonically — ~16 bytes per workspace ever created. Fix: call `restartStates.Delete(wsID)` after stopAndRemove + ClearWorkspaceKeys for each cascaded descendant and the parent. Mirrors the existing per-ID cleanup loop. `sync.Map.Delete` is safe on absent keys, so workspaces that were never restarted (no LoadOrStore call) are no-op. This is a pre-existing leak — #2266 did not introduce it; just renamed the holder. Filing as a separate commit to keep the change minimal and reviewable. Closes #2269 Co-Authored-By: Claude Opus 4.7 (1M context) --- workspace-server/internal/handlers/workspace_crud.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/workspace-server/internal/handlers/workspace_crud.go b/workspace-server/internal/handlers/workspace_crud.go index 043ece50..7a58832e 100644 --- a/workspace-server/internal/handlers/workspace_crud.go +++ b/workspace-server/internal/handlers/workspace_crud.go @@ -441,6 +441,12 @@ func (h *WorkspaceHandler) Delete(c *gin.Context) { for _, descID := range descendantIDs { stopAndRemove(descID) db.ClearWorkspaceKeys(cleanupCtx, descID) + // #2269: drop the per-workspace restartState entry so it + // doesn't accumulate across the platform's lifetime. The + // LoadOrStore that creates the entry (workspace_restart.go) + // has no companion remove path; without this Delete, every + // short-lived workspace leaks ~16 bytes forever. + restartStates.Delete(descID) // Detach broadcaster ctx for the same reason as the cleanup // above — RecordAndBroadcast does an INSERT INTO // structure_events + Redis Publish. If the canvas hangs up, @@ -453,6 +459,7 @@ func (h *WorkspaceHandler) Delete(c *gin.Context) { stopAndRemove(id) db.ClearWorkspaceKeys(cleanupCtx, id) + restartStates.Delete(id) // #2269: same as descendants above h.broadcaster.RecordAndBroadcast(cleanupCtx, "WORKSPACE_REMOVED", id, map[string]interface{}{ "cascade_deleted": len(descendantIDs),