From 762b38fa305cd9fc339c64ab2c2cdc51ec519379 Mon Sep 17 00:00:00 2001 From: rabbitblood Date: Mon, 20 Apr 2026 10:08:17 -0700 Subject: [PATCH] fix(org-import): limit concurrent Docker provisioning to 3 (#1084) The org import fired all workspace provisioning goroutines concurrently, overwhelming Docker when creating 39+ containers. Containers timed out, leaving workspaces stuck in 'provisioning' with no schedules or hooks. Fix: - Add provisionConcurrency=3 semaphore limiting concurrent Docker ops - Increase workspaceCreatePacingMs from 50ms to 2000ms between siblings - Pass semaphore through createWorkspaceTree recursion With 39 workspaces at 3 concurrent + 2s pacing, import takes ~30s instead of timing out. Each workspace gets its full template: schedules, hooks, settings, hierarchy. Co-Authored-By: Claude Opus 4.6 (1M context) --- workspace-server/internal/handlers/org.go | 24 ++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/workspace-server/internal/handlers/org.go b/workspace-server/internal/handlers/org.go index 727c731c..45fccfad 100644 --- a/workspace-server/internal/handlers/org.go +++ b/workspace-server/internal/handlers/org.go @@ -28,7 +28,12 @@ import ( // OrgHandler manages org template import/export. // workspaceCreatePacingMs is the brief delay between sibling workspace creations // during org import. Prevents overwhelming Docker when creating many containers. -const workspaceCreatePacingMs = 50 +const workspaceCreatePacingMs = 2000 + +// provisionConcurrency limits how many Docker containers can be provisioned +// simultaneously during org import. Without this, importing 39+ workspaces +// fires 39 goroutines that all hit Docker at once, causing timeouts (#1084). +const provisionConcurrency = 3 // orgImportScheduleSQL is the upsert executed for every schedule during // org/import. Extracted to a const so TestImport_OrgScheduleSQLShape can @@ -291,9 +296,12 @@ func (h *OrgHandler) Import(c *gin.Context) { results := []map[string]interface{}{} var createErr error + // Semaphore limits concurrent Docker provisioning (#1084). + provisionSem := make(chan struct{}, provisionConcurrency) + // Recursively create workspaces for _, ws := range tmpl.Workspaces { - if err := h.createWorkspaceTree(ws, nil, tmpl.Defaults, orgBaseDir, &results); err != nil { + if err := h.createWorkspaceTree(ws, nil, tmpl.Defaults, orgBaseDir, &results, provisionSem); err != nil { createErr = err break } @@ -346,7 +354,8 @@ func (h *OrgHandler) Import(c *gin.Context) { } // createWorkspaceTree recursively creates a workspace and its children. -func (h *OrgHandler) createWorkspaceTree(ws OrgWorkspace, parentID *string, defaults OrgDefaults, orgBaseDir string, results *[]map[string]interface{}) error { +// provisionSem limits concurrent Docker container creation (#1084). +func (h *OrgHandler) createWorkspaceTree(ws OrgWorkspace, parentID *string, defaults OrgDefaults, orgBaseDir string, results *[]map[string]interface{}, provisionSem chan struct{}) error { // Apply defaults runtime := ws.Runtime if runtime == "" { @@ -652,7 +661,12 @@ func (h *OrgHandler) createWorkspaceTree(ws OrgWorkspace, parentID *string, defa } } - go h.workspace.provisionWorkspace(id, templatePath, configFiles, payload) + // #1084: limit concurrent Docker provisioning via semaphore. + provisionSem <- struct{}{} // acquire + go func(wID, tPath string, cFiles map[string][]byte, p models.CreateWorkspacePayload) { + defer func() { <-provisionSem }() // release + h.workspace.provisionWorkspace(wID, tPath, cFiles, p) + }(id, templatePath, configFiles, payload) } // Insert schedules if defined. Resolve each schedule's prompt body from @@ -792,7 +806,7 @@ func (h *OrgHandler) createWorkspaceTree(ws OrgWorkspace, parentID *string, defa // creating many containers in sequence; container provisioning runs in // goroutines so the main createWorkspaceTree returns quickly. for _, child := range ws.Children { - if err := h.createWorkspaceTree(child, &id, defaults, orgBaseDir, results); err != nil { + if err := h.createWorkspaceTree(child, &id, defaults, orgBaseDir, results, provisionSem); err != nil { return err } time.Sleep(workspaceCreatePacingMs * time.Millisecond)