-
Notifications
You must be signed in to change notification settings - Fork 1.3k
fix(cache): restore Dataset to Bound after CacheRuntime recovers from an outage #6162
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 2 commits
49b2a77
2e2b62a
426ed33
1594e4f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -90,12 +90,30 @@ func (e *CacheEngine) Sync(ctx cruntime.ReconcileRequestContext) (err error) { | |
| if err != nil { | ||
| return err | ||
| } | ||
| } else if permitSyncEngineStatus { | ||
| // sync dataset cache states when runtime is ready and sync permitted | ||
| e.Log.Info("sync dataset cache states") | ||
| err = e.syncDatasetCacheStates(ctx, runtime, runtimeClass) | ||
| if err != nil { | ||
| return err | ||
| } else { | ||
| dataset, getErr := utils.GetDataset(e.Client, e.name, e.namespace) | ||
| if getErr != nil { | ||
| return getErr | ||
| } | ||
|
|
||
| if dataset.Status.Phase == datav1alpha1.FailedDatasetPhase { | ||
| // the runtime recovered from a previous outage but the dataset was left in Failed | ||
| // phase because the phase is otherwise only restored to Bound by the mount flow, | ||
| // which does not run on a normal reconcile. Restore it here. UpdateDatasetStatus | ||
| // keeps this cheap: it only execs into the master pod for cache states when the | ||
| // sync limiter permits it. | ||
| e.Log.Info("runtime is ready again, restoring dataset phase from Failed to Bound") | ||
| err = e.UpdateDatasetStatus(datav1alpha1.BoundDatasetPhase, runtime, runtimeClass) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Alluxio is a useful comparison here, since it does the same recovery and is also called outside the limiter ( I measured this rather than guessing, and I want to be straight about the size of it. In a unit test, driving three not-ready to ready flaps inside a single 5s window produces 3 execs where at most 1 should happen. On a real cluster it's much milder: across five induced worker outages in 47s, the change added one exec attempt (6 versus 5 without it), because a real recovery cycle takes longer than 5s and so tends to get its own window. So this is not a production hazard, and I'm not claiming it is. Even so, I'd rather see it handled in this PR than carried forward, because the fix is small and lives in code you're already touching. Making the restore phase-only and moving the idempotence check into the helper does it, and then this call site no longer needs the current, err := utils.GetDataset(e.Client, e.name, e.namespace)
if err != nil {
return err
}
if current.Status.Phase == phase {
return nil
}
// pod exec with a 20s timeout floor, so keep it behind the limiter
if phase == datav1alpha1.BoundDatasetPhase && e.permitSync() {
cacheStates, err = e.GetCacheStates(runtime, runtimeClass)
...
}One approach I'd avoid: simply wrapping the restore in Smaller point about the current shape: on the reconcile that restores the phase, Harness and captured output, if it's useful: https://github.com/cheyang/fluid/tree/verify/cacheruntime-dataset-phase-restore/docs/verification/cacheruntime-dataset-phase-restore |
||
| if err != nil { | ||
| return err | ||
| } | ||
| } else if permitSyncEngineStatus { | ||
| // sync dataset cache states when runtime is ready and sync permitted | ||
| e.Log.Info("sync dataset cache states") | ||
| err = e.syncDatasetCacheStates(ctx, runtime, runtimeClass) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
+93
to
117
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,6 +22,7 @@ import ( | |
| "time" | ||
|
|
||
| "github.com/fluid-cloudnative/fluid/pkg/common" | ||
| "github.com/fluid-cloudnative/fluid/pkg/utils" | ||
|
|
||
| "github.com/agiledragon/gomonkey/v2" | ||
| "github.com/go-logr/logr" | ||
|
|
@@ -295,7 +296,7 @@ var _ = Describe("CacheEngine Sync Tests", Label("pkg.ddc.cache.engine.sync_test | |
| engine.Client = fake.NewClientBuilder(). | ||
| WithScheme(scheme). | ||
| WithObjects(dataset, runtimeObj, runtimeClass, configMap, masterSts, workerSts, clientDs). | ||
| WithStatusSubresource(runtimeObj). | ||
| WithStatusSubresource(dataset, runtimeObj). | ||
| Build() | ||
| }) | ||
|
|
||
|
|
@@ -338,6 +339,101 @@ var _ = Describe("CacheEngine Sync Tests", Label("pkg.ddc.cache.engine.sync_test | |
| }) | ||
| }) | ||
|
|
||
| Context("when runtime is ready but dataset was left Failed by a previous outage", func() { | ||
| BeforeEach(func() { | ||
| dataset.Status.Phase = datav1alpha1.FailedDatasetPhase | ||
| dataset.Status.Conditions = []datav1alpha1.DatasetCondition{ | ||
| { | ||
| Type: datav1alpha1.DatasetReady, | ||
| Status: corev1.ConditionFalse, | ||
| }, | ||
| } | ||
|
|
||
| masterReplicas := int32(1) | ||
| masterSts := &workloadv1alpha1.AdvancedStatefulSet{ | ||
| ObjectMeta: metav1.ObjectMeta{Name: "test-runtime-master", Namespace: "default"}, | ||
| Spec: workloadv1alpha1.AdvancedStatefulSetSpec{ | ||
| Replicas: &masterReplicas, | ||
| Template: corev1.PodTemplateSpec{ | ||
| Spec: corev1.PodSpec{ | ||
| Containers: []corev1.Container{{Name: "master", Image: "test-master:latest"}}, | ||
| }, | ||
| }, | ||
| }, | ||
| Status: workloadv1alpha1.AdvancedStatefulSetStatus{ReadyReplicas: 1, CurrentReplicas: 1, AvailableReplicas: 1}, | ||
| } | ||
|
|
||
| workerReplicas := int32(2) | ||
| workerSts := &workloadv1alpha1.AdvancedStatefulSet{ | ||
| ObjectMeta: metav1.ObjectMeta{Name: "test-runtime-worker", Namespace: "default"}, | ||
| Spec: workloadv1alpha1.AdvancedStatefulSetSpec{ | ||
| Replicas: &workerReplicas, | ||
| Template: corev1.PodTemplateSpec{ | ||
| Spec: corev1.PodSpec{ | ||
| Containers: []corev1.Container{{Name: "worker", Image: "test-worker:latest"}}, | ||
| }, | ||
| }, | ||
| }, | ||
| Status: workloadv1alpha1.AdvancedStatefulSetStatus{ReadyReplicas: 2, CurrentReplicas: 2, AvailableReplicas: 2}, | ||
| } | ||
|
|
||
| clientDs := &appsv1.DaemonSet{ | ||
| ObjectMeta: metav1.ObjectMeta{Name: "test-runtime-client", Namespace: "default"}, | ||
| Spec: appsv1.DaemonSetSpec{ | ||
| Template: corev1.PodTemplateSpec{ | ||
| Spec: corev1.PodSpec{ | ||
| Containers: []corev1.Container{{Name: "client", Image: "test-client:latest"}}, | ||
| }, | ||
| }, | ||
| }, | ||
| Status: appsv1.DaemonSetStatus{NumberReady: 0, DesiredNumberScheduled: 0}, | ||
| } | ||
|
|
||
| engine.Client = fake.NewClientBuilder(). | ||
| WithScheme(CacheEngineTestScheme). | ||
| WithObjects(dataset, runtimeObj, runtimeClass, masterSts, workerSts, clientDs). | ||
| WithStatusSubresource(dataset, runtimeObj). | ||
| Build() | ||
| }) | ||
|
|
||
| It("should restore the dataset phase to Bound", func() { | ||
| err := engine.Sync(ctx) | ||
| Expect(err).NotTo(HaveOccurred()) | ||
|
|
||
| updatedDataset := &datav1alpha1.Dataset{} | ||
| err = engine.Client.Get(context.Background(), types.NamespacedName{ | ||
| Name: "test-runtime", | ||
| Namespace: "default", | ||
| }, updatedDataset) | ||
| Expect(err).NotTo(HaveOccurred()) | ||
| Expect(updatedDataset.Status.Phase).To(Equal(datav1alpha1.BoundDatasetPhase)) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This pins the phase but not the condition. idx, cond := utils.GetDatasetCondition(updatedDataset.Status.Conditions, datav1alpha1.DatasetReady)
Expect(idx).NotTo(Equal(-1))
Expect(cond.Status).To(Equal(corev1.ConditionTrue))It would also help to seed the Dataset with a One path this case can't reach: the shared fixture leaves |
||
|
|
||
| idx, cond := utils.GetDatasetCondition(updatedDataset.Status.Conditions, datav1alpha1.DatasetReady) | ||
| Expect(idx).NotTo(Equal(-1)) | ||
| Expect(cond.Status).To(Equal(corev1.ConditionTrue)) | ||
| }) | ||
|
|
||
| Context("and the sync limiter is closed", func() { | ||
| BeforeEach(func() { | ||
| engine.syncRetryDuration = defaultSyncRetryDuration | ||
| engine.timeOfLastSync = time.Now() | ||
| }) | ||
|
|
||
| It("should still restore the dataset phase to Bound without fetching cache states", func() { | ||
| err := engine.Sync(ctx) | ||
| Expect(err).NotTo(HaveOccurred()) | ||
|
|
||
| updatedDataset := &datav1alpha1.Dataset{} | ||
| err = engine.Client.Get(context.Background(), types.NamespacedName{ | ||
| Name: "test-runtime", | ||
| Namespace: "default", | ||
| }, updatedDataset) | ||
| Expect(err).NotTo(HaveOccurred()) | ||
| Expect(updatedDataset.Status.Phase).To(Equal(datav1alpha1.BoundDatasetPhase)) | ||
| }) | ||
| }) | ||
| }) | ||
|
|
||
| Context("when runtime is ready with ReportSummary configured", func() { | ||
| var patches *gomonkey.Patches | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the
permitSync()method should be only called insyncmethod, it will make the sync logic clear.