Skip to content

Commit 36852b2

Browse files
committed
fix(dataset): recreate the ThinRuntime of a reference dataset stuck in NotBound
A reference dataset (spec.mounts[0].mountPoint: dataset://<ns>/<name>) could stay in NotBound forever, without any ThinRuntime and without any error being surfaced. Two independent gaps combined to wedge it: 1. CreateRuntimeForReferenceDatasetIfNotExist treated a terminating ThinRuntime as an existing one. GetThinRuntime is a plain Get, so it also returns objects which already have a deletionTimestamp, e.g. a leftover runtime whose controller was scaled to 0 and could not remove its finalizer. The helper concluded that the runtime already exists, tried to adopt the dying object by overwriting its ownerReferences and returned success, so no runtime was ever created once the object finally disappeared. A runtime which is being deleted is now reported as an error (which is not a conflict error, so it is not swallowed by the surrounding retry.RetryOnConflict), letting the caller requeue until the object is really gone and a fresh runtime can be created. 2. The dataset controller did not watch the ThinRuntime it creates, so nothing re-triggered the dataset after the runtime vanished. The controller now Owns() the ThinRuntime. The owner based watch resolves a dependent through the kind and apiVersion of its ownerReference. Both were read from the dataset's TypeMeta, which a typed client may hand back empty, so they now fall back to the well-known values of the Dataset CRD. The chart role for the dataset controller already allows watching thinruntimes and the generated config/rbac/role.yaml is unchanged by the added kubebuilder marker. While verifying the change, the pre-existing spec "should requeue after the resync period when runtime scaleout returns an error" turned out to be flaky (~1% of runs, depending on binary layout): it reset the patch installed by BeforeEach and applied a second patch on the very same function, and gomonkey leaves the first patch in effect in a measurable share of such reset-then-reapply sequences (2217 out of 20000 iterations of an isolated probe). The spec then observed a successful scaleout and no requeue. It now switches behaviour through a variable which the single patch delegates to, instead of patching twice; 500 consecutive runs are green. Fixes #6136 Signed-off-by: cheyang <cheyang@163.com>
1 parent 44cfae9 commit 36852b2

5 files changed

Lines changed: 159 additions & 20 deletions

File tree

pkg/controllers/v1alpha1/dataset/dataset_controller.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ type reconcileRequestContext struct {
6666

6767
// +kubebuilder:rbac:groups=data.fluid.io,resources=datasets,verbs=get;list;watch;create;update;patch;delete
6868
// +kubebuilder:rbac:groups=data.fluid.io,resources=datasets/status,verbs=get;update;patch
69+
// +kubebuilder:rbac:groups=data.fluid.io,resources=thinruntimes,verbs=get;list;watch;create;update;patch;delete
6970

7071
func (r *DatasetReconciler) Reconcile(context context.Context, req ctrl.Request) (ctrl.Result, error) {
7172
ctx := reconcileRequestContext{
@@ -266,6 +267,9 @@ func (r *DatasetReconciler) SetupWithManager(mgr ctrl.Manager, options controlle
266267
return ctrl.NewControllerManagedBy(mgr).
267268
WithOptions(options).
268269
For(&datav1alpha1.Dataset{}).
270+
// Watch the ThinRuntime created for a reference dataset, so that the owning dataset is
271+
// reconciled again (and the runtime is re-created) once the runtime is deleted or lost.
272+
Owns(&datav1alpha1.ThinRuntime{}).
269273
Complete(r)
270274
}
271275

pkg/controllers/v1alpha1/dataset/dataset_controller_ut_test.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,21 @@ import (
4242
var _ = Describe("Dataset Controller Unit", func() {
4343
var scheme *runtime.Scheme
4444
var scaleoutPatch *gomonkey.Patches
45+
// scaleoutResult is what the patched deploy.ScaleoutRuntimeControllerOnDemand returns. A spec which
46+
// needs another result assigns this variable instead of patching the function a second time, because
47+
// resetting and re-applying a patch on the very same function is not reliable.
48+
var scaleoutResult func() (string, bool, error)
4549

4650
BeforeEach(func() {
4751
scheme = runtime.NewScheme()
4852
Expect(datav1alpha1.AddToScheme(scheme)).NotTo(HaveOccurred())
4953
Expect(corev1.AddToScheme(scheme)).To(Succeed())
54+
scaleoutResult = func() (string, bool, error) {
55+
return "", false, nil
56+
}
5057
scaleoutPatch = gomonkey.ApplyFunc(deploy.ScaleoutRuntimeControllerOnDemand,
5158
func(client.Client, types.NamespacedName, logr.Logger) (string, bool, error) {
52-
return "", false, nil
59+
return scaleoutResult()
5360
})
5461
})
5562

@@ -209,11 +216,9 @@ var _ = Describe("Dataset Controller Unit", func() {
209216
})
210217

211218
It("should requeue after the resync period when runtime scaleout returns an error", func() {
212-
scaleoutPatch.Reset()
213-
scaleoutPatch = gomonkey.ApplyFunc(deploy.ScaleoutRuntimeControllerOnDemand,
214-
func(client.Client, types.NamespacedName, logr.Logger) (string, bool, error) {
215-
return "", false, fmt.Errorf("scaleout failed")
216-
})
219+
scaleoutResult = func() (string, bool, error) {
220+
return "", false, fmt.Errorf("scaleout failed")
221+
}
217222

218223
dataset := &datav1alpha1.Dataset{
219224
ObjectMeta: metav1.ObjectMeta{

pkg/controllers/v1alpha1/dataset/dataset_reconciler_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,81 @@ var _ = Describe("DatasetReconciler (fake client)", func() {
251251
Expect(result).To(Equal(ctrl.Result{}))
252252
})
253253

254+
It("creates the ThinRuntime with a controller ownerReference the dataset controller can watch", func() {
255+
// The dataset controller watches the ThinRuntime it creates via Owns(), which resolves
256+
// the owner by the kind and the apiVersion of the ownerReference, so both must be set.
257+
ds := datav1alpha1.Dataset{
258+
ObjectMeta: metav1.ObjectMeta{
259+
Name: "ref-ds-owner",
260+
Namespace: "default",
261+
UID: types.UID("ref-ds-owner-uid"),
262+
Finalizers: []string{finalizer},
263+
},
264+
Spec: datav1alpha1.DatasetSpec{
265+
Mounts: []datav1alpha1.Mount{
266+
{Name: "m1", MountPoint: "dataset://default/physical-ds"},
267+
},
268+
},
269+
Status: datav1alpha1.DatasetStatus{Phase: datav1alpha1.NotBoundDatasetPhase},
270+
}
271+
r := newTestReconciler(&ds)
272+
ctx := makeReconcileCtx(r, ds)
273+
274+
_, err := r.reconcileDataset(ctx, false)
275+
Expect(err).NotTo(HaveOccurred())
276+
277+
thinRuntime := &datav1alpha1.ThinRuntime{}
278+
Expect(r.Get(ctx, types.NamespacedName{Namespace: "default", Name: "ref-ds-owner"}, thinRuntime)).To(Succeed())
279+
Expect(thinRuntime.OwnerReferences).To(HaveLen(1))
280+
Expect(thinRuntime.OwnerReferences[0].Kind).To(Equal(datav1alpha1.Datasetkind))
281+
Expect(thinRuntime.OwnerReferences[0].APIVersion).To(Equal(datav1alpha1.GroupVersion.String()))
282+
Expect(thinRuntime.OwnerReferences[0].UID).To(Equal(ds.UID))
283+
Expect(thinRuntime.OwnerReferences[0].Controller).NotTo(BeNil())
284+
Expect(*thinRuntime.OwnerReferences[0].Controller).To(BeTrue())
285+
})
286+
287+
It("returns error when the ThinRuntime of the reference dataset is still terminating", func() {
288+
// A leftover ThinRuntime stuck in Terminating must not be treated as a usable runtime:
289+
// the reconcile has to fail so that it is retried once the object is really gone,
290+
// instead of leaving the dataset silently without any runtime.
291+
now := metav1.Now()
292+
ds := datav1alpha1.Dataset{
293+
ObjectMeta: metav1.ObjectMeta{
294+
Name: "ref-ds-terminating",
295+
Namespace: "default",
296+
UID: types.UID("ref-ds-terminating-uid"),
297+
Finalizers: []string{finalizer},
298+
},
299+
Spec: datav1alpha1.DatasetSpec{
300+
Mounts: []datav1alpha1.Mount{
301+
{Name: "m1", MountPoint: "dataset://default/physical-ds"},
302+
},
303+
},
304+
Status: datav1alpha1.DatasetStatus{Phase: datav1alpha1.NotBoundDatasetPhase},
305+
}
306+
// The finalizer keeps the terminating runtime in the fake client's tracker.
307+
terminatingRuntime := datav1alpha1.ThinRuntime{
308+
ObjectMeta: metav1.ObjectMeta{
309+
Name: "ref-ds-terminating",
310+
Namespace: "default",
311+
DeletionTimestamp: &now,
312+
Finalizers: []string{"thin-runtime-controller-finalizer"},
313+
},
314+
}
315+
r := newTestReconciler(&ds, &terminatingRuntime)
316+
ctx := makeReconcileCtx(r, ds)
317+
318+
result, err := r.reconcileDataset(ctx, false)
319+
Expect(err).To(HaveOccurred())
320+
Expect(err.Error()).To(ContainSubstring("terminating"))
321+
Expect(result).To(Equal(ctrl.Result{}))
322+
323+
// The terminating runtime must not be adopted by the dataset.
324+
stored := &datav1alpha1.ThinRuntime{}
325+
Expect(r.Get(ctx, types.NamespacedName{Namespace: "default", Name: "ref-ds-terminating"}, stored)).To(Succeed())
326+
Expect(stored.OwnerReferences).To(BeEmpty())
327+
})
328+
254329
It("returns error when CreateRuntimeForReferenceDatasetIfNotExist fails", func() {
255330
ds := datav1alpha1.Dataset{
256331
ObjectMeta: metav1.ObjectMeta{

pkg/utils/dataset_runtime.go

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package utils
1818

1919
import (
2020
"context"
21+
"fmt"
2122
"reflect"
2223

2324
datav1alpha1 "github.com/fluid-cloudnative/fluid/api/v1alpha1"
@@ -41,6 +42,29 @@ func GetRuntimeByCategory(runtimes []datav1alpha1.Runtime, category common.Categ
4142
return -1, nil
4243
}
4344

45+
// datasetControllerOwnerReference builds the controller ownerReference which points to the given dataset.
46+
// Kind and APIVersion come from the dataset's TypeMeta, and fall back to the well-known values of the Dataset
47+
// CRD when it is empty, which a typed client may hand back depending on how the object was read. The owner
48+
// based watch of the dataset controller resolves a dependent through those two fields, so they must be set.
49+
func datasetControllerOwnerReference(dataset *datav1alpha1.Dataset) metav1.OwnerReference {
50+
kind := dataset.GetObjectKind().GroupVersionKind().Kind
51+
if len(kind) == 0 {
52+
kind = datav1alpha1.Datasetkind
53+
}
54+
apiVersion := dataset.APIVersion
55+
if len(apiVersion) == 0 {
56+
apiVersion = datav1alpha1.GroupVersion.String()
57+
}
58+
59+
return metav1.OwnerReference{
60+
Kind: kind,
61+
APIVersion: apiVersion,
62+
Name: dataset.GetName(),
63+
UID: dataset.GetUID(),
64+
Controller: ptr.To(true),
65+
}
66+
}
67+
4468
// CreateRuntimeForReferenceDatasetIfNotExist creates runtime for ReferenceDataset
4569
func CreateRuntimeForReferenceDatasetIfNotExist(client client.Client, dataset *datav1alpha1.Dataset) (err error) {
4670
err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
@@ -49,15 +73,17 @@ func CreateRuntimeForReferenceDatasetIfNotExist(client client.Client, dataset *d
4973
dataset.GetNamespace())
5074
// 1. if err is null, which indicates that the runtime exists, then return
5175
if err == nil {
76+
// 1.1 The runtime is being deleted, it can neither be adopted nor be re-created for now.
77+
// Return an error (not a conflict error, so retry.RetryOnConflict won't swallow it) to
78+
// let the caller requeue until the terminating runtime is really gone.
79+
if HasDeletionTimestamp(runtime.ObjectMeta) {
80+
return fmt.Errorf("the thinRuntime %s/%s is terminating, wait for it to be deleted before creating a new one for the reference dataset",
81+
runtime.GetNamespace(), runtime.GetName())
82+
}
83+
5284
runtimeToUpdate := runtime.DeepCopy()
5385
runtimeToUpdate.SetOwnerReferences([]metav1.OwnerReference{
54-
{
55-
Kind: dataset.GetObjectKind().GroupVersionKind().Kind,
56-
APIVersion: dataset.APIVersion,
57-
Name: dataset.GetName(),
58-
UID: dataset.GetUID(),
59-
Controller: ptr.To(true),
60-
}})
86+
datasetControllerOwnerReference(dataset)})
6187
if !reflect.DeepEqual(runtimeToUpdate, runtime) {
6288
err = client.Update(context.TODO(), runtimeToUpdate)
6389
return err
@@ -72,13 +98,7 @@ func CreateRuntimeForReferenceDatasetIfNotExist(client client.Client, dataset *d
7298
Name: dataset.Name,
7399
Namespace: dataset.Namespace,
74100
OwnerReferences: []metav1.OwnerReference{
75-
{
76-
Kind: dataset.GetObjectKind().GroupVersionKind().Kind,
77-
APIVersion: dataset.APIVersion,
78-
Name: dataset.GetName(),
79-
UID: dataset.GetUID(),
80-
Controller: ptr.To(true),
81-
},
101+
datasetControllerOwnerReference(dataset),
82102
},
83103
Labels: map[string]string{
84104
common.LabelAnnotationDatasetId: GetDatasetId(dataset.GetNamespace(), dataset.GetName(), string(dataset.GetUID())),

pkg/utils/dataset_runtime_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ func mockThreeRuntimes(index int, category common.Category) []datav1alpha1.Runti
8484

8585
func TestCreateRuntimeForReferenceDatasetIfNotExist(t *testing.T) {
8686

87+
deletionTimestamp := v1.Now()
8788
thinRuntimes := []*datav1alpha1.ThinRuntime{
8889
{
8990
ObjectMeta: v1.ObjectMeta{
@@ -104,6 +105,16 @@ func TestCreateRuntimeForReferenceDatasetIfNotExist(t *testing.T) {
104105
Name: "ThinRuntimeExistWithOwnerReference",
105106
Namespace: "default",
106107
},
108+
}, {
109+
// A leftover runtime which is stuck in Terminating, e.g. because its controller is
110+
// scaled to 0 and can not remove the finalizer. The finalizer is also required to keep
111+
// the object in the fake client's tracker once a deletionTimestamp is set.
112+
ObjectMeta: v1.ObjectMeta{
113+
Name: "ThinRuntimeTerminating",
114+
Namespace: "default",
115+
DeletionTimestamp: &deletionTimestamp,
116+
Finalizers: []string{"thin-runtime-controller-finalizer"},
117+
},
107118
},
108119
}
109120
objs := []runtime.Object{}
@@ -148,6 +159,18 @@ func TestCreateRuntimeForReferenceDatasetIfNotExist(t *testing.T) {
148159
},
149160
},
150161
wantErr: false,
162+
}, {
163+
// The runtime of the same name is still terminating, it can neither be adopted nor be
164+
// re-created, so an error is expected to make the caller requeue.
165+
name: "ThinRuntimeTerminating",
166+
dataset: &datav1alpha1.Dataset{
167+
ObjectMeta: v1.ObjectMeta{
168+
Name: "ThinRuntimeTerminating",
169+
Namespace: "default",
170+
UID: "5b7bd2c9-e6e8-4c1e-9c9a-5d2b6ff6bd11",
171+
},
172+
},
173+
wantErr: true,
151174
},
152175
}
153176
for _, tt := range tests {
@@ -157,4 +180,16 @@ func TestCreateRuntimeForReferenceDatasetIfNotExist(t *testing.T) {
157180
}
158181
})
159182
}
183+
184+
// The terminating runtime must be left untouched, especially it must not be adopted by the dataset.
185+
terminatingRuntime, err := GetThinRuntime(fakeClient, "ThinRuntimeTerminating", "default")
186+
if err != nil {
187+
t.Fatalf("failed to get the terminating thinRuntime: %v", err)
188+
}
189+
if !HasDeletionTimestamp(terminatingRuntime.ObjectMeta) {
190+
t.Errorf("expected the thinRuntime ThinRuntimeTerminating to be still terminating")
191+
}
192+
if len(terminatingRuntime.GetOwnerReferences()) != 0 {
193+
t.Errorf("expected no ownerReference set on the terminating thinRuntime, but got %v", terminatingRuntime.GetOwnerReferences())
194+
}
160195
}

0 commit comments

Comments
 (0)