Skip to content

Commit b6b9622

Browse files
committed
feat(core): detect not converging migration from remaining data stall
Signed-off-by: Daniil Antoshin <daniil.antoshin@flant.com>
1 parent f8ee47e commit b6b9622

6 files changed

Lines changed: 77 additions & 6 deletions

File tree

images/virtualization-artifact/pkg/controller/vmop/migration/internal/handler/lifecycle.go

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,16 @@ func (h LifecycleHandler) syncOperationComplete(ctx context.Context, vmop *v1alp
318318
return err
319319
}
320320

321+
autoConverge := h.resolveAutoConverge(vmop)
322+
323+
if reason == vmopcondition.ReasonSyncing {
324+
record := migrationprogress.BuildRecord(vmop, mig, autoConverge, time.Now())
325+
if h.progressStrategy != nil && h.progressStrategy.IsNotConverging(record) {
326+
reason = vmopcondition.ReasonNotConverging
327+
msg = "Migration is not converging: data remaining is not decreasing at maximum throttle"
328+
}
329+
}
330+
321331
vmop.Status.Phase = v1alpha2.VMOPPhaseInProgress
322332
if reason == vmopcondition.ReasonTargetScheduling {
323333
vmop.Status.Phase = v1alpha2.VMOPPhasePending
@@ -603,8 +613,8 @@ func (h LifecycleHandler) calculateMigrationProgress(
603613
return progressTargetPreparing
604614
case vmopcondition.ReasonTargetDiskError:
605615
return progressTargetPreparing
606-
case vmopcondition.ReasonSyncing:
607-
record := migrationprogress.BuildRecord(vmop, mig, time.Now())
616+
case vmopcondition.ReasonSyncing, vmopcondition.ReasonNotConverging:
617+
record := migrationprogress.BuildRecord(vmop, mig, h.resolveAutoConverge(vmop), time.Now())
608618
return h.progressStrategy.SyncProgress(record)
609619
case vmopcondition.ReasonSourceSuspended:
610620
h.forgetProgress(vmop)
@@ -624,6 +634,16 @@ func (h LifecycleHandler) calculateMigrationProgress(
624634
}
625635
}
626636

637+
func (h LifecycleHandler) resolveAutoConverge(vmop *v1alpha2.VirtualMachineOperation) bool {
638+
if vmop == nil {
639+
return false
640+
}
641+
if vmop.Spec.Force != nil && *vmop.Spec.Force {
642+
return true
643+
}
644+
return false
645+
}
646+
627647
func (h LifecycleHandler) forgetProgress(vmop *v1alpha2.VirtualMachineOperation) {
628648
if h.progressStrategy == nil || vmop == nil {
629649
return

images/virtualization-artifact/pkg/controller/vmop/migration/internal/handler/lifecycle_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ func (s *progressStrategyStub) SyncProgress(_ migrationprogress.Record) int32 {
5555
return s.value
5656
}
5757

58+
func (s *progressStrategyStub) IsNotConverging(_ migrationprogress.Record) bool {
59+
return false
60+
}
61+
5862
func (s *progressStrategyStub) Forget(uid types.UID) {
5963
s.forgotten = append(s.forgotten, uid)
6064
}

images/virtualization-artifact/pkg/controller/vmop/migration/internal/progress/mapper.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,12 @@ import (
2626

2727
const unknownMetric = -1.0
2828

29-
func BuildRecord(vmop *v1alpha2.VirtualMachineOperation, mig *virtv1.VirtualMachineInstanceMigration, now time.Time) Record {
29+
func BuildRecord(vmop *v1alpha2.VirtualMachineOperation, mig *virtv1.VirtualMachineInstanceMigration, autoConverge bool, now time.Time) Record {
3030
record := Record{
3131
Now: now,
3232
StartedAt: now,
3333
PreviousProgress: previousProgress(vmop),
34+
AutoConverge: autoConverge,
3435
DataTotalMiB: unknownMetric,
3536
DataProcessedMiB: unknownMetric,
3637
DataRemainingMiB: unknownMetric,

images/virtualization-artifact/pkg/controller/vmop/migration/internal/progress/mapper_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import (
3131
func TestBuildRecord_NilVMOPAndMigration(t *testing.T) {
3232
now := time.Unix(1710000000, 0)
3333

34-
record := BuildRecord(nil, nil, now)
34+
record := BuildRecord(nil, nil, false, now)
3535

3636
if !record.StartedAt.Equal(now) {
3737
t.Fatalf("expected StartedAt=%v, got %v", now, record.StartedAt)
@@ -54,7 +54,7 @@ func TestBuildRecord_UsesVMOPCreationTimestampAndPreviousProgress(t *testing.T)
5454
Status: v1alpha2.VirtualMachineOperationStatus{Progress: ptr.To[int32](42)},
5555
}
5656

57-
record := BuildRecord(vmop, nil, now)
57+
record := BuildRecord(vmop, nil, false, now)
5858

5959
if record.OperationUID != vmop.UID {
6060
t.Fatalf("expected OperationUID=%s, got %s", vmop.UID, record.OperationUID)
@@ -90,7 +90,7 @@ func TestBuildRecord_UsesMigrationState(t *testing.T) {
9090
},
9191
}
9292

93-
record := BuildRecord(nil, mig, now)
93+
record := BuildRecord(nil, mig, false, now)
9494

9595
if record.Phase != virtv1.MigrationRunning {
9696
t.Fatalf("expected Phase=%s, got %s", virtv1.MigrationRunning, record.Phase)

images/virtualization-artifact/pkg/controller/vmop/migration/internal/progress/progress.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ const (
3333
iterativeCeiling = 90.0
3434
thresholdFactor = 0.05
3535

36+
notConvergingWindow = 10 * time.Second
37+
3638
bulkTimeRate = 0.55
3739
iterBaseTimeRate = 0.022
3840
iterThrottleRate = 0.0012
@@ -49,6 +51,7 @@ const (
4951

5052
type Strategy interface {
5153
SyncProgress(record Record) int32
54+
IsNotConverging(record Record) bool
5255
Forget(uid types.UID)
5356
}
5457

@@ -64,6 +67,7 @@ type Record struct {
6467
HasThrottle bool
6568
AutoConvergeThrottle uint32
6669
Throttle float64
70+
AutoConverge bool
6771
DataTotalMiB float64
6872
DataProcessedMiB float64
6973
DataRemainingMiB float64
@@ -149,6 +153,7 @@ func (p *Progress) SyncProgress(record Record) int32 {
149153
}
150154

151155
updateMetricState(record, &state)
156+
updateMinRemaining(record, &state)
152157
state.Progress = next
153158
state.LastUpdatedAt = record.Now
154159
state.LastIteration = record.Iteration
@@ -161,6 +166,27 @@ func (p *Progress) SyncProgress(record Record) int32 {
161166
return next
162167
}
163168

169+
func (p *Progress) IsNotConverging(record Record) bool {
170+
if p == nil || p.store == nil || record.OperationUID == "" {
171+
return false
172+
}
173+
174+
state, ok := p.store.Load(record.OperationUID)
175+
if !ok || !state.Iterative {
176+
return false
177+
}
178+
179+
if !isAtMaxThrottle(record) {
180+
return false
181+
}
182+
183+
if state.MinRemaining <= 0 || state.MinRemainingAt.IsZero() {
184+
return false
185+
}
186+
187+
return record.Now.Sub(state.MinRemainingAt) >= notConvergingWindow
188+
}
189+
164190
func (p *Progress) getState(record Record) State {
165191
if p == nil || p.store == nil || record.OperationUID == "" {
166192
return State{Progress: clampSyncRange(record.PreviousProgress), LastMetricAt: record.Now}
@@ -324,6 +350,24 @@ func maxRemaining(record Record) float64 {
324350
return 0
325351
}
326352

353+
func updateMinRemaining(record Record, state *State) {
354+
remaining := maxRemaining(record)
355+
if remaining <= 0 {
356+
return
357+
}
358+
if state.MinRemaining <= 0 || remaining < state.MinRemaining {
359+
state.MinRemaining = remaining
360+
state.MinRemainingAt = record.Now
361+
}
362+
}
363+
364+
func isAtMaxThrottle(record Record) bool {
365+
if !record.AutoConverge {
366+
return true
367+
}
368+
return record.HasThrottle && record.Throttle >= 0.99
369+
}
370+
327371
func updateMetricState(record Record, state *State) {
328372
if !metricChanged(record, state) {
329373
return

images/virtualization-artifact/pkg/controller/vmop/migration/internal/progress/store.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ type State struct {
4242
SmoothedRemaining float64
4343
Threshold float64
4444
LastIncreaseAt time.Time
45+
MinRemaining float64
46+
MinRemainingAt time.Time
4547
}
4648

4749
type Store struct {

0 commit comments

Comments
 (0)