Skip to content

Commit 9178dbe

Browse files
EDsCODEclaude
andauthored
Fix orphaned worker pods: log deletion errors, eliminate TOCTOU race (#385)
Two bugs caused worker pods to remain running after the janitor marked them retired in the DB: 1. retireWorkerPod silently discarded pod deletion errors with _. If the K8s API delete failed (timeout, network blip), the pod stayed running with no indication in logs. Now logs the error. 2. retireLocalWorker had a TOCTOU race: it checked p.workers[id] then called retireWorkerWithReason which checked again. If the worker was removed between the two checks, retireLocalWorker returned true (thinking it handled it) but the pod was never deleted, and the fallback retireRuntimeWorker was never called. Fix: retireWorkerWithReason now returns bool. retireLocalWorker returns the result directly — no separate existence check. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent aab475e commit 9178dbe

2 files changed

Lines changed: 15 additions & 15 deletions

File tree

controlplane/k8s_pool.go

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -814,12 +814,14 @@ func (p *K8sWorkerPool) RetireWorker(id int) {
814814
p.retireWorkerWithReason(id, RetireReasonNormal)
815815
}
816816

817-
func (p *K8sWorkerPool) retireWorkerWithReason(id int, reason string) {
817+
// retireWorkerWithReason retires a worker and deletes its pod.
818+
// Returns true if the worker was found and retired.
819+
func (p *K8sWorkerPool) retireWorkerWithReason(id int, reason string) bool {
818820
p.mu.Lock()
819821
w, ok := p.workers[id]
820822
if !ok {
821823
p.mu.Unlock()
822-
return
824+
return false
823825
}
824826
p.markWorkerRetiredLocked(w, reason)
825827
delete(p.workers, id)
@@ -828,6 +830,7 @@ func (p *K8sWorkerPool) retireWorkerWithReason(id int, reason string) {
828830
observeControlPlaneWorkers(workerCount)
829831

830832
go p.retireWorkerPod(id, w)
833+
return true
831834
}
832835

833836
// RetireWorkerIfNoSessions retires a worker only if it has no active sessions.
@@ -1655,17 +1658,21 @@ func (p *K8sWorkerPool) ShutdownAll() {
16551658

16561659
// retireWorkerPod closes the gRPC client and deletes the worker pod.
16571660
func (p *K8sWorkerPool) retireWorkerPod(id int, w *ManagedWorker) {
1658-
slog.Info("Retiring K8s worker.", "id", id)
1661+
podName := p.workerPodName(w)
1662+
slog.Info("Retiring K8s worker.", "id", id, "pod", podName)
16591663
if w.client != nil {
16601664
_ = w.client.Close()
16611665
}
1662-
podName := p.workerPodName(w)
16631666
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
16641667
defer cancel()
1665-
_ = p.clientset.CoreV1().Pods(p.namespace).Delete(ctx, podName, metav1.DeleteOptions{
1668+
if err := p.clientset.CoreV1().Pods(p.namespace).Delete(ctx, podName, metav1.DeleteOptions{
16661669
GracePeriodSeconds: int64Ptr(10),
1667-
})
1668-
_ = p.deleteWorkerRPCSecret(ctx, podName)
1670+
}); err != nil {
1671+
slog.Warn("Failed to delete worker pod.", "id", id, "pod", podName, "error", err)
1672+
}
1673+
if err := p.deleteWorkerRPCSecret(ctx, podName); err != nil {
1674+
slog.Warn("Failed to delete worker RPC secret.", "id", id, "pod", podName, "error", err)
1675+
}
16691676
}
16701677

16711678
// idleReaper periodically retires workers that have been idle too long and

controlplane/multitenant.go

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -223,14 +223,7 @@ func SetupMultiTenant(
223223
router.sharedPool.retireClaimedWorker(&record, reason)
224224
}
225225
janitor.retireLocalWorker = func(workerID int, reason string) bool {
226-
router.sharedPool.mu.Lock()
227-
_, local := router.sharedPool.workers[workerID]
228-
router.sharedPool.mu.Unlock()
229-
if !local {
230-
return false
231-
}
232-
router.sharedPool.retireWorkerWithReason(workerID, reason)
233-
return true
226+
return router.sharedPool.retireWorkerWithReason(workerID, reason)
234227
}
235228
janitor.reconcileWarmCapacity = func() {
236229
target := router.sharedPool.WarmCapacityTarget()

0 commit comments

Comments
 (0)