@@ -73,7 +73,6 @@ func (p *Provisioner) EnsureCapacity(ctx context.Context, workspaceID string, ne
7373 workspaceID ).Scan (& available )
7474
7575 if available >= needed {
76- // Find a server with capacity
7776 var serverIP string
7877 p .pool .QueryRow (ctx ,
7978 `SELECT ip_address FROM workspace_servers
@@ -84,6 +83,17 @@ func (p *Provisioner) EnsureCapacity(ctx context.Context, workspaceID string, ne
8483 return serverIP , nil
8584 }
8685
86+ // Re-activate idle servers before provisioning new ones
87+ var idleIP string
88+ err := p .pool .QueryRow (ctx ,
89+ `UPDATE workspace_servers SET state = 'ready', updated_at = now()
90+ WHERE id = (SELECT id FROM workspace_servers WHERE workspace_id = $1 AND state = 'idle' LIMIT 1)
91+ RETURNING ip_address` , workspaceID ).Scan (& idleIP )
92+ if err == nil {
93+ slog .Info ("infra: re-activated idle server" , "workspace" , workspaceID , "ip" , idleIP )
94+ return idleIP , nil
95+ }
96+
8797 // Check max servers limit
8898 var serverCount int
8999 p .pool .QueryRow (ctx ,
@@ -150,10 +160,24 @@ func (p *Provisioner) DestroyServer(ctx context.Context, serverID string) error
150160 }
151161
152162 slog .Info ("infra: destroying server" , "server" , serverID , "hetzner_id" , hetznerID )
153-
154- if err := p .hetzner .DeleteServer (ctx , hetznerID ); err != nil {
155- slog .Error ("infra: failed to destroy server" , "error" , err )
156- return err
163+ p .pool .Exec (ctx ,
164+ `UPDATE workspace_servers SET state = 'destroying', updated_at = now() WHERE id = $1` , serverID )
165+
166+ // Retry Hetzner delete up to 3 times
167+ var lastErr error
168+ for attempt := 0 ; attempt < 3 ; attempt ++ {
169+ if err := p .hetzner .DeleteServer (ctx , hetznerID ); err != nil {
170+ lastErr = err
171+ slog .Warn ("infra: destroy attempt failed" , "server" , serverID , "attempt" , attempt + 1 , "error" , err )
172+ time .Sleep (time .Duration (attempt + 1 ) * 2 * time .Second )
173+ continue
174+ }
175+ lastErr = nil
176+ break
177+ }
178+ if lastErr != nil {
179+ slog .Error ("infra: failed to destroy server after retries" , "server" , serverID , "error" , lastErr )
180+ return lastErr
157181 }
158182
159183 p .pool .Exec (ctx ,
@@ -203,6 +227,28 @@ func (p *Provisioner) checkIdleServers(ctx context.Context) {
203227 }
204228}
205229
230+ // DestroyWorkspaceServers destroys all servers belonging to a workspace (called on workspace deletion).
231+ func (p * Provisioner ) DestroyWorkspaceServers (ctx context.Context , workspaceID string ) error {
232+ rows , err := p .pool .Query (ctx ,
233+ `SELECT id, hetzner_id, name FROM workspace_servers
234+ WHERE workspace_id = $1 AND state NOT IN ('destroyed', 'destroying')` , workspaceID )
235+ if err != nil {
236+ return err
237+ }
238+ defer rows .Close ()
239+
240+ for rows .Next () {
241+ var serverID , name string
242+ var hetznerID int64
243+ rows .Scan (& serverID , & hetznerID , & name )
244+ slog .Info ("infra: destroying workspace server" , "server" , name , "workspace" , workspaceID )
245+ p .hetzner .DeleteServer (ctx , hetznerID )
246+ p .pool .Exec (ctx ,
247+ `UPDATE workspace_servers SET state = 'destroyed', destroyed_at = now(), updated_at = now() WHERE id = $1` , serverID )
248+ }
249+ return nil
250+ }
251+
206252// IncrementContainer finds a server with capacity for this workspace and increments its count.
207253// Returns the server ID so it can be decremented later.
208254func (p * Provisioner ) IncrementContainer (ctx context.Context , workspaceID string ) (string , error ) {
0 commit comments