Repository navigation
feat(server): keep an uninstalling release listed until its pods are gone - #31
Conversation
huskcass
left a comment
There was a problem hiding this comment.
Blocker
internal/server/uninstalling.go:330-345(podsRemain): every pod that matches either selector is counted, whatever its phase, and this check is the only thing that ever endsuninstalling. Helm does not delete some pods: a Succeeded or Failedhelm testpod, a hook Job pod labelledapp.kubernetes.io/instance=<release>, or a pod stuck Terminating on a lost node. Any one of these keeps the releaseuninstallingfor good. After that, install, upgrade and rollback return 409 (refuseUninstalling,uninstalling.go:193), and a repeated DELETE only answersuninstall already in progress(backend_helm.go:181). The API gives no way out, so someone has to delete the pod or the plan ConfigMap by hand. Reproduced with a copy oftestdata/uninstalling/pods-gone.yamlthat has a singlephase: Succeededpod: afterdropHelmthe row is still listed as uninstalling, and a second uninstall does not change that. Suggested fix: ignore pods in a terminal phase, and give the state an exit. That could be a deadline that moves the plan tofailedwith a message, or a repeated DELETE that drops the plan once helm reports the release gone.
Verdict: the design and test coverage are good, and the restart, destroy-fails and retry paths hold up. It can merge once the stuck-uninstalling path has an exit.
— Review Golden
|
Thanks. b0f53f8: |
|
Pushed 09b0ef5 with the agreed review fixes:
The check-then-act 409 race and the writes during |
huskcass
left a comment
There was a problem hiding this comment.
No blockers. The stuck-uninstalling blocker from the previous review is resolved.
Verdict: good to merge after the #30 rebase. Finished pods are ignored, stuck Terminating pods are released by the grace + 5m rule or a NotReady/missing node, archive cleanup only touches this release's owner=swiss Secrets, and every fix has a case that fails without it.
— Review Golden
…gone A helm release used to drop out of /api/deployments as soon as helm uninstall returned, while its pods were still terminating. The plan ConfigMap is now marked uninstalling (status.yaml phase and a swiss.modelsphere.dev/phase label) before helm runs and is kept after. swissd tracks those releases in memory, rebuilds the set from the label after a restart, and drops the plan once the helm release is gone and no pod matches either release selector. The uninstall API is unchanged. An LLMService with a deletionTimestamp is reported as uninstalling too. Install, upgrade and rollback of an uninstalling release answer 409. Refs modelsphere/console#94
Helm leaves Succeeded or Failed pods behind (helm test pods, hook Job pods labelled with the release instance), so counting them kept a release in uninstalling forever. podsRemain now skips pods in a terminal phase.
…ninstall path - podsRemain keeps counting a pod with a deletionTimestamp during its grace period. It is ignored only when stuck: now is past deletionTimestamp + grace (deletionGracePeriodSeconds, else terminationGracePeriodSeconds, else 30s) + 5m, or its node is NotReady or missing. The decision is the pure podStuckTerminating, given the clock and node readiness; nodes are listed only when a matched pod is terminating. - Archive Secrets are deleted by one helper, called after helm uninstall, on a repeat DELETE, and from reapRelease once helm has dropped the release, so a restart does not leave them behind. - The deployments row omits revision when there is none; openapi.yaml no longer lists it as required. - New cases under testdata/uninstalling: terminating within grace, past grace, on a NotReady node, on a missing node, archive cleanup on repeat DELETE and after restart, and revision omitted.
…autoscale config, hold autoscaling - applyLocked refuses an uninstalling release, so the check and the uninstall marker are both made under the release's apply lock: an apply cannot overwrite the marker and an uninstall cannot start mid-apply. - A repeat DELETE on a release that is already uninstalling returns 200 even while that uninstall still holds the lock. - reapRelease removes swiss-autoscale-<release> with the plan record. - The autoscaler holds a release that is uninstalling or whose LLMService is being deleted. - A Terminating pod on a NotReady or missing node is ignored only once deletionTimestamp + grace has passed.
09b0ef5 to
9cc0284
Compare
|
Rebased on master after #30 (new head 9cc0284). Changes:
|
huskcass
left a comment
There was a problem hiding this comment.
No blockers. The earlier stuck-uninstalling blocker stays fixed after the rebase.
Verdict: good to merge. The uninstall marker and the refusal for apply, rollback and autoscale now happen under one per-release TryLock, so neither path can block the other. The autoscaler holds on uninstalling releases and on LLMService deletions, and reap removes swiss-autoscale-<release>.
— Review Golden
…tFound The operator now runs a staged teardown behind its finalizer: route removal, route settle, helm uninstall, pod termination. Waiting for that inside the DELETE request held the release lock for up to 15 minutes, so a repeat DELETE got 409. For the llmsvc backend only: - DELETE marks the plan uninstalling (when there is one), deletes the LLMService, records the uninstall as started and returns 200 with the in-progress body. A repeat DELETE answers the same. NotFound is 404 and puts the plan back. WaitGone is gone. - The reap treats an llmsvc release as done when the LLMService is NotFound, with no pod scan or helm lookup, then removes archives, the autoscale ConfigMap and the plan, and records the final audit once. A restart rebuilds the set from LLMServices with a deletionTimestamp. A recreated object stops the tracking and nothing is deleted. - Release list and status pass status.deletion through (stage, outcome, podsRemaining, deadline, message, pods, forceDeleted) and report status.phase as operatorPhase. Deleting counts as uninstalling, and the autoscaler holds on it as it does on a deletionTimestamp. The helm backend is unchanged.
After DELETE, an llmsvc release whose LLMService still exists must be listed as uninstalling and a repeat DELETE returns 200. An operator that drops the object at once passes the same check. The gone timeout for llmsvc is 6 minutes to cover route settle and pod termination.
|
Pushed 22c0740 and f2dccc2. These change only the llmsvc backend and depend on modelsphere/llm-operator#21 (staged LLMService deletion). Nothing changes for helm. 22c0740: feat(server): return from an LLMService delete at once and reap on NotFound
f2dccc2: test(e2e): accept an instant or staged LLMService delete
Checks: gofmt, |
huskcass
left a comment
There was a problem hiding this comment.
No blockers.
Verdict: good to merge. An LLMService DELETE now returns 200 once the delete is accepted, a repeat DELETE also gets 200, the reap runs once the object is NotFound, and it still works against an operator without staged deletion.
— Review Golden
Summary
A helm-backed release (plan ConfigMap) used to vanish from
GET /api/deploymentsas soon ashelm uninstallreturned, while its pods were still terminating. It now stays listed withphase: uninstallinguntil the helm release is gone and no pod matches the release, then drops out.Rebased on master after #30 (PD autoscaling). The uninstalling state now uses #30's per-release apply lock and covers the autoscaler.
status.yamlphase: uninstallingand the labelswiss.modelsphere.dev/phase=uninstalling. It is kept after helm returns. Archive Secrets are deleted after helm uninstall, and again on a repeat DELETE and from the check loop once helm has dropped the release, so a restart does not leave them behind. They are kept when the uninstall fails and while helm still holds the release.applyLocked, under the same lock, for install, upgrade, rollback and autoscale applies. An apply cannot overwrite the marker, and an uninstall cannot start mid-apply (409apply in progress). A repeat DELETE on a release that is already uninstalling answers 200, even while the first uninstall still holds the lock.Server.Run(next to the autoscale loop) checks every 5s, and so do list and status reads. After a restart the set is rebuilt from the label.exec.Lookupreports the helm release gone and nothing matchesapp.kubernetes.io/instance=<release>orapp=<release>-<chart>. Theswiss-autoscale-<release>ConfigMap is removed in the same step. Finished pods (Succeeded/Failed) don't count. A Terminating pod (deletionTimestamp set) still counts during its grace period (deletionGracePeriodSeconds, elseterminationGracePeriodSeconds, else 30s). Past that it is ignored if its node is NotReady or missing, and in any case once deletionTimestamp + grace + 5 minutes has passed. If the lookup or pod list fails, the release stays listed. If a ConfigMap delete fails, the next check retries it.metadata.deletionTimestampset (orstatus.phase: Deleting) is reported asuninstallingtoo; see the llmsvc section below.holdReason: uninstalling, no apply) for a release whose plan is uninstalling or whose LLMService has a deletionTimestamp.LLMService (llmsvc) backend
Depends on modelsphere/llm-operator#21 (staged LLMService deletion). With that change the operator runs the teardown behind its finalizer: delete the ModelRoute, wait for route settle, helm uninstall, wait for the release pods to terminate (bounded by maxWait, optional force delete), then drop the finalizer. Swiss no longer waits for any of it in the request, and does not scan pods for llmsvc releases.
DELETEmarks the plan ConfigMapuninstallingwhen there is one, deletes the LLMService, records the uninstall as started, and returns 200 right away with the in-progress body (output: uninstall already in progress, same fields as before). The 15-minuteWaitGonein the request path is gone, so a repeat DELETE during teardown gets 200 in progress instead of 409. NotFound on delete is still 404, and the plan is put back.swiss-autoscale-<release>ConfigMap and the plan ConfigMap, and records the final audit row once. A restart rebuilds the set from LLMServices that have a deletionTimestamp. If the object is recreated, tracking stops and nothing is deleted.status.phase: Deletingisuninstalling.status.deletionis passed through asdeletion(stage, outcome, podsRemaining, deadline, startedAt, message, pods, forceDeleted) on the deployments row and inllmservice.deletionon status.llmservice.operatorPhaseis the rawstatus.phase. Drift names the stage, e.g.uninstall in progress: PodsTerminating. All of these are optional, and helm rows are unchanged.status.phase: Deleting.serving.modelsphere.dev/deletion-max-wait,serving.modelsphere.dev/route-settle,serving.modelsphere.dev/force-delete-after, orserving.modelsphere.dev/deletion-policy: Orphan.Status contract (additive only): the deployments row
phase, helmplanStatus.phaseand llmsvcllmservice.phasecan now also beuninstalling.DELETE /api/releases/{ns}/{release}keeps its route and body. A repeat DELETE on an uninstalling release answers 200 withoutput: uninstall already in progress.openapi.yamlnow lists the 409 a DELETE gets while an apply holds the lock. The deployments row omitsrevisionwhen there is none (it is no longerrequiredinopenapi.yaml), so an uninstalling row does not report 0. The chart is unchanged because it already grants pod list and ConfigMap delete.UI: modelsphere/console#96 · Refs modelsphere/console#94
Testing
internal/server/testdata/uninstalling/*.yamlhas one case per file, run byuninstalling_cases_test.go. Cases: pods remain, pods gone, engine-only and instance-only pods, restart (rebuild), restart while the helm release remains, destroy fails, second uninstall, retry while helm remains, install/upgrade/rollback 409, pod list error, ConfigMap delete retry, LLMService with deletionTimestamp, finished pods ignored, terminating within grace (kept), terminating past grace / on a NotReady node / on a missing node (ignored), revision omitted, and archive cleanup on repeat DELETE and after restart.internal/autoscale/testdata/decide/guard-uninstalling.yaml,guard-llm-deleting.yaml, andinternal/server/testdata/autoscale-loop/helm-uninstalling.yaml,llm-deleting.yaml(hold, no apply).testdata/uninstalling/llmsvc-*.yaml): DELETE returns at once in progress, repeat DELETE 200, reap waits while the LLMService exists withstatus.deletionand passes it through to list and status, reap cleans up on NotFound with one final audit, delete NotFound is 404 and restores the plan, restart rebuilds from a deleting LLMService, a recreated object is not cleaned up, andstatus.phase: Deletingpassthrough. The old synchronousTestLLMUninstallandllmsvcTestWaitGoneare removed. Autoscale:guard-llm-phase-deleting.yaml,autoscale-loop/llm-phase-deleting.yaml.gofmt(clean),go build ./...,go vet ./...,go vet -tags=e2e ./...andgo test -race -count=1 ./....test/e2e: the helm deploy-and-remove spec checks that the row is listed asuninstallingwhile pods exist, then gone. The llmsvc spec passes whether the operator drops the LLMService at once or stages it: while the object exists, the row must beuninstallingand a repeat DELETE returns 200. The gone timeout for llmsvc is 6 minutes.