ewerton-silva00 opened a new issue, #13544:
URL: https://github.com/apache/cloudstack/issues/13544

   ### problem
   
   ### CloudStack version
   4.22.1.0
   
   ### Environment
   - Hypervisor: KVM
   - CKS Kubernetes version: v1.36.1
   - CSI driver: csi.cloudstack.apache.org
   - Cluster type: CloudManaged (1 control node, size 4)
   
   ### Summary
   
   Deleting a CKS Kubernetes cluster can leave it stuck in `Destroying` state
   indefinitely (observed 25h+, reproduced twice) because
   `KubernetesClusterDestroyWorker` performs PV cleanup via a **blocking SSH
   exec with no timeout**, and a second, separate bug silently ignores a
   state-machine error on retry, causing repeated attempts to hang the exact
   same way instead of failing fast.
   
   ### Root cause #1 — no timeout on SSH exec used for PV cleanup
   
   `KubernetesClusterDestroyWorker.destroy()` → `destroyClusterVMs()` calls
   
`KubernetesClusterResourceModifierActionWorker.deletePVsWithReclaimPolicyDelete()`,
   which SSHes into the cluster's control-plane node and runs `kubectl` to
   delete PVs with `ReclaimPolicy=Delete` **before** any cluster VM is
   stopped/destroyed.
   
   This uses `com.cloud.utils.ssh.SshHelper.sshExecute()`, which has **no
   execution timeout** on this code path. If the remote `kubectl` command
   blocks — e.g. because a PVC is stuck in `Terminating` (its
   `kubernetes.io/pvc-protection` finalizer is waiting on a Pod that is still
   using it and was never deleted) — the SSH channel read blocks forever and
   so does the whole async destroy job.
   
   Thread dump confirming the hang (management server JVM, thread alive for
   91851s / ~25.5h):
   
   "API-Job-Executor-16" ... in Object.wait()
      java.lang.Thread.State: WAITING (on object monitor)
         at java.lang.Object.wait(Native Method)
         at java.lang.Object.wait(Object.java:338)
         at com.trilead.ssh2.channel.FifoBuffer.read(FifoBuffer.java:212)
         - locked <...> (a com.trilead.ssh2.channel.Channel)
         at com.trilead.ssh2.channel.Channel$Output.read(Channel.java:127)
         at 
com.trilead.ssh2.channel.ChannelManager.getChannelData(ChannelManager.java:935)
         at 
com.trilead.ssh2.channel.ChannelInputStream.read(ChannelInputStream.java:58)
         at 
com.trilead.ssh2.channel.ChannelInputStream.read(ChannelInputStream.java:70)
         at com.cloud.utils.ssh.SshHelper.sshExecute(SshHelper.java:292)
         at 
com.cloud.kubernetes.cluster.actionworkers.KubernetesClusterResourceModifierActionWorker.deletePVsWithReclaimPolicyDelete(KubernetesClusterResourceModifierActionWorker.java:966)
         at 
com.cloud.kubernetes.cluster.actionworkers.KubernetesClusterDestroyWorker.destroyClusterVMs(KubernetesClusterDestroyWorker.java:110)
         at 
com.cloud.kubernetes.cluster.actionworkers.KubernetesClusterDestroyWorker.destroy(KubernetesClusterDestroyWorker.java:311)
         at 
com.cloud.kubernetes.cluster.KubernetesClusterManagerImpl.destroyKubernetesCluster(KubernetesClusterManagerImpl.java:2439)
   
   TCP-level connectivity to the control node was fine throughout (SSH port
   and the k8s API port both accepted new connections instantly) — this was
   never a network/node-availability issue, purely a stuck remote command
   with an unbounded client-side wait.
   
   Symptom visible to the operator: the cluster stays `Destroying` forever in
   `listKubernetesClusters`, and the UI eventually shows a generic **"Error
   encountered while fetching async job result"** after giving up polling —
   which is misleading, since the job is not erroring, it simply never
   completes.
   
   ### Root cause #2 — state-transition failure is logged and ignored, not 
surfaced
   
   Every retry of `deleteKubernetesCluster` against a cluster already in
   `Destroying` hits:
   
   WARN  [c.c.k.c.a.KubernetesClusterDestroyWorker] Failed to transition
   state of the Kubernetes cluster : <name> in state Destroying on event
   DestroyRequested
   com.cloud.utils.fsm.NoTransitionException: Unable to transition to a new
   state from Destroying via DestroyRequested
         at 
com.cloud.kubernetes.cluster.actionworkers.KubernetesClusterActionWorker.stateTransitTo(KubernetesClusterActionWorker.java:672)
         at 
com.cloud.kubernetes.cluster.actionworkers.KubernetesClusterDestroyWorker.destroy(KubernetesClusterDestroyWorker.java:310)
   
   This exception is only logged as a `WARN` — execution continues into a
   **brand new** attempt at PV cleanup / VM teardown, instead of failing the
   API call immediately or resuming/checking the existing in-flight job. In
   practice this means: once a cluster gets wedged by root cause #1, every
   subsequent delete click by the operator just reproduces the identical
   hang, with no useful error surfaced and no indication that a previous job
   is already stuck.
   
   ### versions
   
   Apache CloudStack 4.22.1.0
   
   ### The steps to reproduce the bug
   
   ### Steps to reproduce
   
   1. Create a CKS cluster with at least one PVC provisioned via the
      CloudStack CSI driver, bound to a running Pod.
   2. Delete that PVC directly (`kubectl delete pvc <name>`) while the Pod is
      still running and using it. It will sit in `Terminating` forever
      (expected k8s behavior — `pvc-protection` finalizer waiting on the
      Pod).
   3. Call `deleteKubernetesCluster` on the cluster.
   4. Observe: the async job never completes. `jstack` the management server
      and confirm the `API-Job-Executor-N` thread for that job is blocked in
      `SshHelper.sshExecute` → `deletePVsWithReclaimPolicyDelete`.
   5. Retry `deleteKubernetesCluster` — observe the same
      `NoTransitionException` WARN followed by the exact same hang.
   
   ### Expected behavior
   
   - SSH-based remote command execution for PV cleanup should have a
     configurable timeout, after which the cleanup step fails cleanly (and,
     ideally, falls back to destroying the VMs anyway rather than blocking
     the whole cluster teardown on stuck in-cluster storage state).
   - A `NoTransitionException` on a destroy retry should cause the API call
     to fail fast with a clear message (e.g. "a destroy operation is already
     in progress for this cluster, job <jobid>"), not silently proceed to
     re-run the entire destroy workflow.
   
   ### Workaround used
   
   No workaround inside CloudStack itself was available short of a code fix.
   Had to: SSH directly into the control-plane node (using the cluster's own
   node keypair) to find and manually resolve the stuck PVC/Pod, restart
   `cloudstack-management` to kill the wedged thread, then retry the delete.
   This is not something a typical operator/admin without JVM-level access
   (thread dumps, log correlation across `apilog.log` /
   `management-server.log`) could reasonably self-diagnose.
   
   ### What to do about it?
   
   **Fix #1 — bound the SSH exec used for PV cleanup**
   
   
`KubernetesClusterResourceModifierActionWorker.deletePVsWithReclaimPolicyDelete()`
   should call `SshHelper.sshExecute(...)` with an explicit timeout instead of
   the unbounded read used today. Concretely:
   
   - Add a timeout parameter (config key, e.g.
     `cloud.kubernetes.cluster.pv.cleanup.timeout`, default something like
     120s) and pass it through to the SSH exec call.
   - On timeout, log a clear WARN/ERROR ("PV cleanup on node X timed out
     after Ns, proceeding with VM teardown regardless") and **continue** into
     `destroyClusterVMs()`'s VM stop/expunge steps rather than blocking the
     whole job. Losing best-effort PV cleanup on a wedged node is strictly
     better than an unkillable job — the VMs (and their disks) are about to
     be destroyed anyway, so a CSI-level PV delete that didn't get to run is
     not a correctness problem for the *cluster* teardown, only a possible
     orphaned-volume cleanup task afterward (worth its own follow-up: a
     "list volumes with no owning VM/cluster" admin command would help here).
   - `trilead.ssh2` supports read timeouts on the channel/session directly
     (`Connection.connect(..., timeout)` for connect, and
     `InputStream.read` respects the underlying socket's `SO_TIMEOUT` if the
     session is opened with one) — this should be a small, contained change
     in `SshHelper`, not a rearchitecture.
   
   **Fix #2 — fail fast instead of silently retrying a wedged destroy**
   
   In `KubernetesClusterActionWorker.stateTransitTo()` / the call site in
   `KubernetesClusterDestroyWorker.destroy()`, a `NoTransitionException`
   should not be caught-and-continue. Concretely:
   
   - If the cluster is already in `Destroying` (or any non-terminal state
     that isn't a valid source for `DestroyRequested`), the API command
     should throw `InvalidParameterValueException` (or similar) back to the
     caller immediately, ideally including the existing job's UUID if one is
     still tracked, e.g.: *"Cluster <name> already has a destroy operation
     in progress (job <jobid>), started at <timestamp>. Wait for it to
     complete or check its status directly."*
   - This alone would have turned this incident from "silent 25h hang,
     repeated on every retry" into "immediate, actionable error on the very
     first retry."
   
   **Suggested priority:** Fix #2 is small/low-risk and should be
   straightforward to backport across supported branches. Fix #1 is the
   actual root cause of the indefinite hang and is more valuable long-term,
   but touches shared SSH plumbing (`SshHelper`) used elsewhere in the
   codebase, so it deserves a bit more test coverage/review.
   
   Happy to submit a PR for either if that's useful — flagging here first
   since I don't have full context on branch/version support policy for this
   project.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to