What is your environment?
- Fluid version: master (control plane images
v1.1.0-36f0467)
- Kubernetes: v1.36.1 (ACK, 3 nodes)
- Chart:
charts/fluid/fluid from master
- Runtime involved:
CacheRuntime (curvine) + auto-created ThinRuntime for a reference Dataset
What happened?
A reference Dataset (spec.mounts[0].mountPoint: dataset://<ns>/<name>) stayed in NotBound forever, with no ThinRuntime object in the namespace at all, and no error reported anywhere on the Dataset. The Dataset had a finalizer and a phase, so nothing looked broken — it was simply never reconciled again.
Touching the Dataset (kubectl annotate dataset <ref-dataset> probe=1 --overwrite) recreated the ThinRuntime immediately and the Dataset became Bound within seconds. So the state was wedged, not slow.
This was hit while running test/gha-e2e/curvine/test.sh on a cluster where the same test names had been used by an earlier run.
Timeline (observed, with logs)
-
Leftover from an earlier run: an orphan ThinRuntime/curvine-demo-ref was pending deletion (deletionTimestamp set, finalizer thin-runtime-controller-finalizer still present). Its cleanup was blocked because thinruntime-controller was at replicas: 0 — the chart declares the runtime controllers with replicas: 0 and relies on ScaleoutRuntimeControllerOnDemand to scale them up (a helm upgrade re-applies replicas: 0, which can scale a controller down again while one of its objects is still terminating).
Warning ErrorProcessRuntime thinruntime/curvine-demo-ref
Process Runtime error Dataset.data.fluid.io "curvine-demo" not found
-
New run creates the reference Dataset (14:02:13). dataset-controller scales thinruntime-controller out on demand and adds its finalizer:
14:02:13.447 utils/runtime_checkers.go:35 Succeed in finding the object {"runtime": {"kind": "ThinRuntime", "namespace": "default", "name": "curvine-demo-ref"}}
14:02:13.455 dataset/dataset_controller.go:90 scale out the runtime controller on demand successfully {"controller": "thinruntime-controller"}
14:02:13.455 dataset/dataset_controller.go:255 Add finalizer and Requeue {"dataset": {"name":"curvine-demo-ref"}}
Note Succeed in finding the object — the Get found the terminating leftover, so CreateRuntimeForReferenceDatasetIfNotExist concluded "the runtime already exists" and did not create anything. It also tried to re-parent that dying object by overwriting its ownerReferences.
-
The terminating leftover finally disappears (14:02:39), right after the freshly scaled-out controller starts and removes the finalizer:
14:02:39.246 leaderelection.go:260 successfully acquired lease fluid-system/thin.data.fluid.io
14:02:39.663 controllers/runtime_controller.go:233 before clean up finalizer {"runtime": {"kind": "ThinRuntime", "name": "curvine-demo-ref"}}
14:02:39.678 controllers/runtime_controller.go:247 Finalizer is removed successfully
-
Nothing ever recreates it. The Dataset sits at NotBound indefinitely:
>>> checking reference dataset.status.phase==Bound (already 555s, last state: NotBound)
$ kubectl get thinruntime -n default
No resources found in default namespace.
$ kubectl get dataset -n default
NAME PHASE AGE
curvine-demo Bound 10m
curvine-demo-ref NotBound 9m10s
Root cause
Two independent gaps combine into a permanently wedged state:
A. The auto-create helper treats a terminating runtime as an existing one.
pkg/utils/dataset_runtime.go:45 CreateRuntimeForReferenceDatasetIfNotExist:
runtime, err := GetThinRuntime(client, dataset.GetName(), dataset.GetNamespace())
// 1. if err is null, which indicates that the runtime exists, then return
if err == nil {
runtimeToUpdate := runtime.DeepCopy()
runtimeToUpdate.SetOwnerReferences(...) // re-parents an object that is already dying
...
return nil
}
GetThinRuntime (pkg/utils/runtimes.go:99) is a plain Get, which happily returns objects that have a deletionTimestamp. There is no utils.HasDeletionTimestamp check (the helper exists at pkg/utils/crtl_utils.go:158), so err == nil also covers "exists, but is being deleted and will be gone in a moment".
B. Nothing re-triggers the Dataset once the runtime disappears.
DatasetReconciler.SetupWithManager (pkg/controllers/v1alpha1/dataset/dataset_controller.go:265) only does For(&datav1alpha1.Dataset{}) — there is no Owns(&ThinRuntime{}) / watch on the runtime it creates, so the deletion of the ThinRuntime produces no reconcile event for the Dataset.
reconcileDataset ends with return utils.NoRequeue() (dataset_controller.go:181) — the periodic RequeueAfterInterval(r.ResyncPeriod) right above it is only taken when needRequeue is set, and the line that would always requeue is commented out.
So after step 3 there is no event source and no resync that would ever call CreateRuntimeForReferenceDatasetIfNotExist again. The reference Dataset is stuck until a human edits it.
Contributing factor: because the chart ships runtime controllers at replicas: 0 and depends on on-demand scale-out, a helm upgrade (or any re-apply of the chart) can scale a runtime controller back to 0 while one of its objects is terminating. That object then stays in Terminating for an unbounded time with its finalizer intact — which is exactly the leftover that triggers gap A on the next run.
How to reproduce
Deterministic recipe (any runtime works; ThinRuntime for a reference Dataset shown):
- Create a reference Dataset so a ThinRuntime is auto-created and
thinruntime-controller is scaled out.
- Scale the controller down to simulate the window:
kubectl scale deploy thinruntime-controller -n fluid-system --replicas=0 (or run helm upgrade on the chart, which re-applies replicas: 0).
- Delete the reference Dataset. The ThinRuntime goes to
Terminating and stays there (finalizer, no controller).
- Recreate the reference Dataset with the same name.
dataset-controller logs Succeed in finding the object … ThinRuntime and skips creation.
- The controller gets scaled out again, removes the finalizer, and the object disappears.
- The Dataset now stays
NotBound forever. kubectl annotate dataset <name> probe=1 --overwrite unblocks it.
Impact
- Reference Datasets can wedge permanently with no error surfaced on the Dataset — the only symptom is
NotBound and a missing ThinRuntime.
- It is not self-healing: no resync, no watch, no event. Manual intervention is required.
- Most likely to bite CI/e2e (repeated runs reusing the same object names, plus chart re-installs between runs) and any environment where the fluid chart is upgraded while a runtime is being deleted.
Suggested fix
- In
CreateRuntimeForReferenceDatasetIfNotExist, treat a runtime with a deletionTimestamp as not existing yet: if utils.HasDeletionTimestamp(runtime.ObjectMeta), do not adopt/re-parent it, and requeue so the Dataset is retried once the object is really gone. Adopting a terminating object via Update should be avoided in any case.
- Add
Owns(&datav1alpha1.ThinRuntime{}) (or an explicit watch mapping runtime → Dataset) to DatasetReconciler.SetupWithManager, so that deleting the auto-created runtime re-triggers the Dataset and it is recreated.
- Optionally re-enable a bounded periodic requeue for Datasets that are not yet
Bound, as a safety net for any state that no event covers.
- Separately, consider making the on-demand scale-down refuse to scale a runtime controller to 0 while any of its CRs still have a
deletionTimestamp (and/or have the chart not fight on-demand scaling on helm upgrade), so finalizers cannot be stranded.
I can send a PR for 1 + 2 if the direction looks right.
What is your environment?
v1.1.0-36f0467)charts/fluid/fluidfrom masterCacheRuntime(curvine) + auto-createdThinRuntimefor a reference DatasetWhat happened?
A reference Dataset (
spec.mounts[0].mountPoint: dataset://<ns>/<name>) stayed inNotBoundforever, with no ThinRuntime object in the namespace at all, and no error reported anywhere on the Dataset. The Dataset had a finalizer and a phase, so nothing looked broken — it was simply never reconciled again.Touching the Dataset (
kubectl annotate dataset <ref-dataset> probe=1 --overwrite) recreated the ThinRuntime immediately and the Dataset becameBoundwithin seconds. So the state was wedged, not slow.This was hit while running
test/gha-e2e/curvine/test.shon a cluster where the same test names had been used by an earlier run.Timeline (observed, with logs)
Leftover from an earlier run: an orphan
ThinRuntime/curvine-demo-refwas pending deletion (deletionTimestamp set, finalizerthin-runtime-controller-finalizerstill present). Its cleanup was blocked becausethinruntime-controllerwas atreplicas: 0— the chart declares the runtime controllers withreplicas: 0and relies onScaleoutRuntimeControllerOnDemandto scale them up (ahelm upgradere-appliesreplicas: 0, which can scale a controller down again while one of its objects is still terminating).New run creates the reference Dataset (
14:02:13).dataset-controllerscalesthinruntime-controllerout on demand and adds its finalizer:Note
Succeed in finding the object— theGetfound the terminating leftover, soCreateRuntimeForReferenceDatasetIfNotExistconcluded "the runtime already exists" and did not create anything. It also tried to re-parent that dying object by overwriting itsownerReferences.The terminating leftover finally disappears (
14:02:39), right after the freshly scaled-out controller starts and removes the finalizer:Nothing ever recreates it. The Dataset sits at
NotBoundindefinitely:Root cause
Two independent gaps combine into a permanently wedged state:
A. The auto-create helper treats a terminating runtime as an existing one.
pkg/utils/dataset_runtime.go:45CreateRuntimeForReferenceDatasetIfNotExist:GetThinRuntime(pkg/utils/runtimes.go:99) is a plainGet, which happily returns objects that have adeletionTimestamp. There is noutils.HasDeletionTimestampcheck (the helper exists atpkg/utils/crtl_utils.go:158), soerr == nilalso covers "exists, but is being deleted and will be gone in a moment".B. Nothing re-triggers the Dataset once the runtime disappears.
DatasetReconciler.SetupWithManager(pkg/controllers/v1alpha1/dataset/dataset_controller.go:265) only doesFor(&datav1alpha1.Dataset{})— there is noOwns(&ThinRuntime{})/ watch on the runtime it creates, so the deletion of the ThinRuntime produces no reconcile event for the Dataset.reconcileDatasetends withreturn utils.NoRequeue()(dataset_controller.go:181) — the periodicRequeueAfterInterval(r.ResyncPeriod)right above it is only taken whenneedRequeueis set, and the line that would always requeue is commented out.So after step 3 there is no event source and no resync that would ever call
CreateRuntimeForReferenceDatasetIfNotExistagain. The reference Dataset is stuck until a human edits it.Contributing factor: because the chart ships runtime controllers at
replicas: 0and depends on on-demand scale-out, ahelm upgrade(or any re-apply of the chart) can scale a runtime controller back to 0 while one of its objects is terminating. That object then stays inTerminatingfor an unbounded time with its finalizer intact — which is exactly the leftover that triggers gap A on the next run.How to reproduce
Deterministic recipe (any runtime works; ThinRuntime for a reference Dataset shown):
thinruntime-controlleris scaled out.kubectl scale deploy thinruntime-controller -n fluid-system --replicas=0(or runhelm upgradeon the chart, which re-appliesreplicas: 0).Terminatingand stays there (finalizer, no controller).dataset-controllerlogsSucceed in finding the object … ThinRuntimeand skips creation.NotBoundforever.kubectl annotate dataset <name> probe=1 --overwriteunblocks it.Impact
NotBoundand a missing ThinRuntime.Suggested fix
CreateRuntimeForReferenceDatasetIfNotExist, treat a runtime with adeletionTimestampas not existing yet: ifutils.HasDeletionTimestamp(runtime.ObjectMeta), do not adopt/re-parent it, and requeue so the Dataset is retried once the object is really gone. Adopting a terminating object viaUpdateshould be avoided in any case.Owns(&datav1alpha1.ThinRuntime{})(or an explicit watch mapping runtime → Dataset) toDatasetReconciler.SetupWithManager, so that deleting the auto-created runtime re-triggers the Dataset and it is recreated.Bound, as a safety net for any state that no event covers.deletionTimestamp(and/or have the chart not fight on-demand scaling onhelm upgrade), so finalizers cannot be stranded.I can send a PR for 1 + 2 if the direction looks right.