Fix loadConcurrency slot leak on forced cancel - #10264
Conversation
👷 Deploy request for velero pending review.Visit the deploys page to approve it
|
746117c to
afffbab
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
When a DataUpload/DataDownload is being deleted, or its cancel isn't handled within the cancel delay window, tryCancelDataUpload/ tryCancelDataDownload mark the CR Canceled directly, without calling closeDataPath. That skipped removing the microServiceBRWatcher from dataPathMgr and canceling its context, so the watcher goroutine could run forever waiting on pod/event channels that would never fire again, permanently holding a loadConcurrency slot on the node. Call closeDataPath before cleaning up exposer resources so the data path instance and its concurrency slot are always released when a data upload/download is force-canceled. Fixes velero-io#10207 Signed-off-by: lubronzhan <lubron.zhan@broadcom.com> AI-Tool-Used: Claude Code AI-Tool-Use-Level: Category 2 (Medium) AI-Code-Category: Category 1 (Production)
afffbab to
eca6422
Compare
| // success update | ||
| r.metrics.RegisterDataUploadCancel(r.nodeName) | ||
| // release the data path instance so its loadConcurrency slot isn't held forever | ||
| r.closeDataPath(ctx, du.Name) |
There was a problem hiding this comment.
dataPathMgr is local to each node-agent, but any node’s reconciler can win the forced-cancel status update above. If a non-owning node marks this DataUpload as Canceled, this call finds no local data path; meanwhile, the owning node subsequently sees a terminal CR and removes the finalizer without closing its watcher. The original watcher and loadConcurrency slot can therefore still leak. Please ensure an InProgress forced cancellation is finalized by du.Status.Node, or have terminal-state reconciliation close any matching local data path before removing the finalizer. The same issue applies to DataDownload. A multi-node regression test where only the owning reconciler has the watcher would cover this race.
This is as the expectation. Actually, we cannot call the closeDataPath at this place, otherwise, the data mover pod will left as orphan. |
Got it. Then the problem is somehow the DataUPload CR disappeared before the data mover pod act on it? Then in this case, should we consider handle this special case? By adding a DeleteFunc maybe? |
Ohk, there might be a race between |
The previous commit released the local data path directly from tryCancelDataUpload/tryCancelDataDownload when a DataUpload/ DataDownload was force-canceled. Review feedback pointed out this can abandon a still-running data-mover pod, since releasing the watcher there doesn't wait for confirmation that the pod actually stopped. Revert that approach and instead react to the deletion from inside the data-mover pod itself: the pod's own informer for its DataUpload/DataDownload gains a DeleteFunc alongside the existing Spec.Cancel-driven UpdateFunc, so a CR deleted directly (e.g. its finalizer force-removed) still causes the pod to cancel and exit cleanly, regardless of which node's reconciler performed the deletion. node-agent's existing watcher then observes the pod's own termination and releases its dataPathMgr entry through the normal completion callback, so the loadConcurrency slot is never held forever. This introduces a narrow, low-severity race between the new DeleteFunc and the existing UpdateFunc trigger, documented in code comments on handleDataUploadDelete/handleDataDownloadDelete: in the rare case both fire before the pod's data path was ever created, a redundant resultSignal send can block until the pod process exits moments later regardless. This is a same-process, sub-second goroutine leak with no effect on the operation's outcome or on node-agent's own cleanup. Signed-off-by: lubronzhan <lubron.zhan@broadcom.com> AI-Tool-Used: Claude Code AI-Tool-Use-Level: Category 2 (Medium) AI-Code-Category: Category 1 (Production)
bbcb1f6 to
293339e
Compare
|
I think we still need to figure out some questions:
As my understanding and the expectation:
|
Summary
microServiceBRWatchergoroutine in node-agent (and theloadConcurrencyslot it holds indataPathMgr) could outlive the deletion of itsDataUpload/DataDownloadCR, since the actual data-mover pod it's waiting on never terminated.pkg/datamover) only reacts to cancellation viaSpec.Cancelturningtrue(its informer only registers anUpdateFunc). If the CR is deleted directly instead — e.g. its finalizer is force-removed, bypassing the normalSpec.Cancelhandshake — the pod's informer gets aDeleteevent it silently ignores, the pod keeps running obliviously, node-agent's watcher never sees it terminate, and thedataPathMgrentry/loadConcurrencyslot leak forever.An earlier version of this PR calledPer review feedback, that was reverted: releasing the local watcher there doesn't wait for confirmation the pod actually stopped, which can abandon a still-running data-mover pod.closeDataPathdirectly fromtryCancelDataUpload/tryCancelDataDownloadin the node-agent controllers.DeleteFunchandler alongside the existingUpdateFuncon the pod's ownDataUpload/DataDownloadinformer (pkg/datamover/backup_micro_service.go,restore_micro_service.go). If the CR is deleted while the pod is stillInProgress, the pod now cancels and exits cleanly through the same path as a normal cancel, regardless of which node's reconciler performed the deletion. node-agent's existing watcher then observes the pod's real termination and releases itsdataPathMgrentry through the normal completion callback — no changes needed on the node-agent controller side.Known trade-off (documented in code comments)
Adding a second, independent trigger (
DeleteFunc) alongside the existingSpec.Cancel-drivenUpdateFuncopens a narrow race: if both fire for the same object before its data path was ever created, a redundantresultSignalsend can block until the pod process exits, which happens within moments regardless viaShutdown()/funcExitWithMessage. This is a same-process, sub-second goroutine leak with no effect on the operation's outcome or on node-agent's own cleanup (which watches the pod's actual phase, not this internal channel) — considered acceptable given the added complexity a guard would require versus this bounded, effectively unobservable impact.Test plan
TestHandleDataUploadDelete/TestHandleDataDownloadDeletecovering: direct object, informer tombstone, wrong name, wrong phase, and non-DataUpload/DataDownload objects.DeleteFuncwiring and pass with it.go build ./...andgo test ./pkg/controller/... ./pkg/datamover/... ./pkg/datapath/...pass (one pre-existing, unrelatedTestAPIsenvtest failure present onmaintoo).AI-Tool-Used: Claude Code
AI-Tool-Use-Level: Category 2 (Medium)
AI-Code-Category: Category 1 (Production)