Skip to content

Commit 7e6aa5b

Browse files
authored
fix(hibernation): clear hibernate annotation before wake fast-path (#10)
* fix(hibernation): clear hibernate annotation before wake fast-path reconcileWake's cloned-and-running fast-path returned Active without clearing vm.cocoonstack.io/hibernate. A pod already awake but carrying hibernate=true residue kept the annotation set; a subsequent Desire=Hibernate then no-ops PatchHibernateState (annotation already matches), so the CR could flip to Hibernated against a stale tag without vk-cocoon taking a fresh snapshot — a later wake clones stale/divergent state. Move the clear above the fast-path so it runs unconditionally on any wake reconcile. PatchHibernateState already no-ops when the annotation matches, so the common (already-false) case adds no extra write. Fixes #2. * chore(hibernation): drop comments from wake fix
1 parent c8cbdad commit 7e6aa5b

2 files changed

Lines changed: 58 additions & 6 deletions

File tree

hibernation/reconciler_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,58 @@ func TestReconcileWakeRecoversFromFailed(t *testing.T) {
463463
}
464464
}
465465

466+
func TestReconcileWakeClearsHibernateResidueOnFastPath(t *testing.T) {
467+
hib := &cocoonv1.CocoonHibernation{
468+
ObjectMeta: metav1.ObjectMeta{Name: "hib", Namespace: "ns", Finalizers: []string{finalizerName}},
469+
Spec: cocoonv1.CocoonHibernationSpec{
470+
Desire: cocoonv1.HibernationDesireWake,
471+
PodRef: cocoonv1.HibernationPodRef{Name: "demo-0"},
472+
},
473+
Status: cocoonv1.CocoonHibernationStatus{Phase: cocoonv1.CocoonHibernationPhaseWaking},
474+
}
475+
pod := &corev1.Pod{
476+
ObjectMeta: metav1.ObjectMeta{Name: "demo-0", Namespace: "ns"},
477+
Status: corev1.PodStatus{
478+
ContainerStatuses: []corev1.ContainerStatus{{
479+
State: corev1.ContainerState{Running: &corev1.ContainerStateRunning{}},
480+
}},
481+
},
482+
}
483+
(&meta.VMSpec{VMName: "vk-ns-demo-0", Managed: true}).Apply(pod)
484+
(&meta.VMRuntime{VMID: "vmid-live"}).Apply(pod)
485+
meta.HibernateState(true).Apply(pod)
486+
487+
scheme := testScheme(t)
488+
cli := ctrlfake.NewClientBuilder().
489+
WithScheme(scheme).
490+
WithObjects(hib, pod).
491+
WithStatusSubresource(&cocoonv1.CocoonHibernation{}).
492+
Build()
493+
r := &Reconciler{Client: cli, Scheme: scheme, Epoch: &fakeRegistry{}}
494+
495+
if _, err := r.Reconcile(t.Context(), ctrl.Request{
496+
NamespacedName: types.NamespacedName{Namespace: "ns", Name: "hib"},
497+
}); err != nil {
498+
t.Fatalf("Reconcile: %v", err)
499+
}
500+
501+
var outHib cocoonv1.CocoonHibernation
502+
if err := cli.Get(t.Context(), types.NamespacedName{Namespace: "ns", Name: "hib"}, &outHib); err != nil {
503+
t.Fatalf("get hib: %v", err)
504+
}
505+
if outHib.Status.Phase != cocoonv1.CocoonHibernationPhaseActive {
506+
t.Errorf("phase = %q, want Active (fast-path)", outHib.Status.Phase)
507+
}
508+
509+
var outPod corev1.Pod
510+
if err := cli.Get(t.Context(), types.NamespacedName{Namespace: "ns", Name: "demo-0"}, &outPod); err != nil {
511+
t.Fatalf("get pod: %v", err)
512+
}
513+
if meta.ReadHibernateState(&outPod) {
514+
t.Error("hibernate annotation must be cleared on the wake fast-path; still true")
515+
}
516+
}
517+
466518
func TestHibernateDeadlineExceeded(t *testing.T) {
467519
staleReady := metav1.Condition{
468520
Type: commonk8s.ConditionTypeReady,

hibernation/wake.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ func (r *Reconciler) reconcileWake(ctx context.Context, hib *cocoonv1.CocoonHibe
1717
logger := log.WithFunc("hibernation.Reconciler.reconcileWake")
1818
r.announceRetryFromFailed(hib, cocoonv1.HibernationDesireWake)
1919

20+
if meta.ReadHibernateState(pod) {
21+
if err := commonk8s.PatchHibernateState(ctx, r.Client, pod, false); err != nil {
22+
return ctrl.Result{}, fmt.Errorf("clear hibernate annotation: %w", err)
23+
}
24+
}
25+
2026
if vmClonedAndRunning(pod) {
2127
// Drop snapshot tag (non-fatal; stale tag gets overwritten on next hibernate).
2228
if err := r.Epoch.DeleteManifest(ctx, vmName, meta.HibernateSnapshotTag); err != nil {
@@ -29,12 +35,6 @@ func (r *Reconciler) reconcileWake(ctx context.Context, hib *cocoonv1.CocoonHibe
2935
return ctrl.Result{}, r.setPhase(ctx, hib, cocoonv1.CocoonHibernationPhaseActive, vmName)
3036
}
3137

32-
if meta.ReadHibernateState(pod) {
33-
if err := commonk8s.PatchHibernateState(ctx, r.Client, pod, false); err != nil {
34-
return ctrl.Result{}, fmt.Errorf("clear hibernate annotation: %w", err)
35-
}
36-
}
37-
3838
if phaseDeadlineExceeded(hib, cocoonv1.CocoonHibernationPhaseWaking, wakeTimeout) {
3939
if r.firstTransitionAt(hib) {
4040
observePhaseExit(hib, "timeout")

0 commit comments

Comments
 (0)