Skip to content

Commit f8ee47e

Browse files
committed
refactor(core): rewrite migration progress engine with EMA smoothing and adaptive stall
Signed-off-by: Daniil Antoshin <daniil.antoshin@flant.com>
1 parent 68708ce commit f8ee47e

3 files changed

Lines changed: 398 additions & 206 deletions

File tree

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

Lines changed: 182 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,23 @@ const (
2828
SyncRangeMin int32 = 10
2929
SyncRangeMax int32 = 90
3030

31-
bulkCeiling = 48.0
32-
iterativeFloor = 46.0
33-
iterativeCeiling = 90.0
34-
bulkStallWindow = 45 * time.Second
35-
iterStallWindow = 25 * time.Second
36-
bulkUpdateInterval = time.Second
37-
iterUpdateInterval = 2 * time.Second
31+
bulkCeiling = 45.0
32+
iterativeFloor = 45.0
33+
iterativeCeiling = 90.0
34+
thresholdFactor = 0.05
35+
36+
bulkTimeRate = 0.55
37+
iterBaseTimeRate = 0.022
38+
iterThrottleRate = 0.0012
39+
bulkMetricWeight = 0.80
40+
bulkTimeWeight = 0.20
41+
iterMetricWeight = 0.76
42+
iterTimeWeight = 0.24
43+
smoothAlphaUp = 0.18
44+
smoothAlphaDown = 0.34
45+
bulkStallSeconds = 10.0
46+
iterStallSeconds = 8.0
47+
finalStallSeconds = 6.0
3848
)
3949

4050
type Strategy interface {
@@ -76,6 +86,7 @@ func (p *Progress) Forget(uid types.UID) {
7686

7787
func (p *Progress) SyncProgress(record Record) int32 {
7888
state := p.getState(record)
89+
7990
prev := clampSyncRange(record.PreviousProgress)
8091
if state.Progress > prev {
8192
prev = state.Progress
@@ -85,22 +96,57 @@ func (p *Progress) SyncProgress(record Record) int32 {
8596
if elapsed < 0 {
8697
elapsed = 0
8798
}
99+
elapsedSec := elapsed.Seconds()
88100

89101
iterative := isIterative(record)
90102
if iterative && !state.Iterative {
91103
state.Iterative = true
92104
state.IterativeSince = record.Now
105+
p.initIterative(record, &state, elapsedSec)
106+
}
107+
108+
if iterative {
109+
observeRemaining(record, &state)
110+
}
111+
112+
target := bulkTarget(record, elapsedSec)
113+
if iterative {
114+
target = iterativeTarget(record, &state, elapsedSec)
93115
}
94116

95-
target := bulkTarget(record, elapsed)
117+
maxStep := int32(10)
96118
if iterative {
97-
target = iterativeTarget(record, &state, prev)
119+
maxStep = 5
120+
}
121+
122+
cap := stageCap(iterative)
123+
progress := math.Max(float64(prev), math.Min(target, cap))
124+
next := clampPercent(progress)
125+
if next < prev {
126+
next = prev
127+
}
128+
if next > prev+maxStep {
129+
next = prev + maxStep
130+
}
131+
132+
if next == prev && float64(next) < cap {
133+
lastIncrease := state.LastIncreaseAt
134+
if lastIncrease.IsZero() {
135+
lastIncrease = record.StartedAt
136+
}
137+
stallWin := stallWindow(record, &state, iterative)
138+
if record.Now.Sub(lastIncrease).Seconds() >= stallWin {
139+
next++
140+
}
98141
}
99142

100-
metricAdvanced := metricChanged(record, &state)
101-
next := smoothProgress(prev, target, iterative, record.Throttle, record.Now.Sub(state.LastUpdatedAt), metricAdvanced, state.LastUpdatedAt.IsZero())
102-
next = applyStatefulStall(record, &state, next, iterative)
103-
next = clampSyncRange(maxInt32(prev, next))
143+
if float64(next) > cap {
144+
next = int32(cap)
145+
}
146+
147+
if next > prev {
148+
state.LastIncreaseAt = record.Now
149+
}
104150

105151
updateMetricState(record, &state)
106152
state.Progress = next
@@ -121,143 +167,161 @@ func (p *Progress) getState(record Record) State {
121167
}
122168
state, ok := p.store.Load(record.OperationUID)
123169
if !ok {
124-
state = State{Progress: clampSyncRange(record.PreviousProgress), LastMetricAt: record.Now}
170+
state = State{
171+
Progress: clampSyncRange(record.PreviousProgress),
172+
LastMetricAt: record.Now,
173+
}
125174
}
126175
return state
127176
}
128177

129-
func metricPercent(record Record) (float64, bool) {
130-
if record.DataTotalMiB <= 0 {
131-
return 0, false
178+
func (p *Progress) initIterative(record Record, state *State, _ float64) {
179+
total := record.DataTotalMiB
180+
if total <= 0 {
181+
total = 1
182+
}
183+
if total > state.InitialTotal {
184+
state.InitialTotal = total
185+
}
186+
if state.InitialTotal <= 0 {
187+
state.InitialTotal = total
132188
}
133189

134-
processed, hasProcessed := normalizedProcessedMiB(record)
135-
if !hasProcessed {
136-
return 0, false
190+
remaining := maxRemaining(record)
191+
if remaining <= 0 {
192+
remaining = state.InitialTotal
137193
}
138194

139-
return clampFloat((processed/record.DataTotalMiB)*100.0, 0, 100), true
195+
state.Threshold = math.Max(math.Ceil(state.InitialTotal*thresholdFactor), 1)
196+
state.InitialRemaining = math.Max(remaining, state.Threshold)
197+
state.SmoothedRemaining = state.InitialRemaining
140198
}
141199

142-
func normalizedProcessedMiB(record Record) (float64, bool) {
143-
if record.DataTotalMiB <= 0 {
144-
return 0, false
200+
func observeRemaining(record Record, state *State) {
201+
remaining := maxRemaining(record)
202+
if remaining <= 0 {
203+
return
204+
}
205+
206+
alpha := smoothAlphaUp
207+
if remaining < state.SmoothedRemaining {
208+
alpha = smoothAlphaDown
145209
}
146-
if record.DataProcessedMiB >= 0 {
147-
return clampFloat(record.DataProcessedMiB, 0, record.DataTotalMiB), true
210+
if record.Throttle >= 0.80 {
211+
alpha += 0.08
148212
}
149-
if record.DataRemainingMiB >= 0 {
150-
return clampFloat(record.DataTotalMiB-record.DataRemainingMiB, 0, record.DataTotalMiB), true
213+
if alpha > 0.90 {
214+
alpha = 0.90
151215
}
152-
return 0, false
153-
}
154216

155-
func bulkTarget(record Record, elapsed time.Duration) float64 {
156-
timeTarget := float64(SyncRangeMin) + math.Min(14, elapsed.Seconds()/8)
157-
metricPct, hasMetric := metricPercent(record)
158-
if !hasMetric {
159-
return clampFloat(timeTarget, float64(SyncRangeMin), bulkCeiling)
217+
if state.SmoothedRemaining <= 0 {
218+
state.SmoothedRemaining = remaining
219+
} else {
220+
state.SmoothedRemaining = alpha*remaining + (1-alpha)*state.SmoothedRemaining
160221
}
161-
metricTarget := float64(SyncRangeMin) + (metricPct/100.0)*(bulkCeiling-float64(SyncRangeMin))
162-
mixed := metricTarget*0.78 + timeTarget*0.22
163-
return clampFloat(mixed, float64(SyncRangeMin), bulkCeiling)
164222
}
165223

166-
func iterativeTarget(record Record, state *State, current int32) float64 {
167-
baseline := float64(current)
168-
if record.HasIteration {
169-
iterationBoost := math.Min(float64(record.Iteration), 6) * 1.5
170-
baseline = math.Max(baseline, float64(current)+iterationBoost)
224+
func bulkTarget(record Record, elapsedSec float64) float64 {
225+
total := record.DataTotalMiB
226+
if total <= 0 {
227+
total = 1
171228
}
172229

173-
target := baseline
174-
metricPct, hasMetric := metricPercent(record)
175-
if hasMetric {
176-
target = math.Max(target, iterativeMetricTarget(record, metricPct))
177-
}
230+
processed := math.Max(record.DataProcessedMiB, 0)
231+
metricRatio := clampFloat(processed/total, 0, 1)
232+
metricPct := float64(SyncRangeMin) + (bulkCeiling-float64(SyncRangeMin))*metricRatio
178233

179-
iterativeSince := state.IterativeSince
180-
if iterativeSince.IsZero() {
181-
iterativeSince = record.Now
234+
timePct := float64(SyncRangeMin) + elapsedSec*bulkTimeRate
235+
if timePct > bulkCeiling {
236+
timePct = bulkCeiling
182237
}
183-
iterElapsed := record.Now.Sub(iterativeSince)
184-
if iterElapsed < 0 {
185-
iterElapsed = 0
186-
}
187-
target += math.Min(10, iterElapsed.Seconds()/12)
188-
if record.HasThrottle {
189-
target += record.Throttle * 6
238+
239+
return bulkMetricWeight*metricPct + bulkTimeWeight*timePct
240+
}
241+
242+
func iterativeTarget(record Record, state *State, elapsedSec float64) float64 {
243+
metricRatio := iterativeMetricRatio(state)
244+
metricPct := iterativeFloor + (iterativeCeiling-5-iterativeFloor)*metricRatio
245+
246+
throttle := record.Throttle
247+
iterSince := state.IterativeSince
248+
if iterSince.IsZero() {
249+
iterSince = record.Now
190250
}
191-
if !hasMetric {
192-
target += math.Min(34, iterElapsed.Seconds()/20)
251+
iterElapsed := math.Max(0, elapsedSec-record.Now.Sub(iterSince).Seconds()+record.Now.Sub(iterSince).Seconds())
252+
iterElapsedSec := math.Max(0, record.Now.Sub(iterSince).Seconds())
253+
254+
timeRate := iterBaseTimeRate + throttle*iterThrottleRate
255+
timePct := iterativeFloor + iterElapsedSec*timeRate
256+
if timePct > iterativeCeiling {
257+
timePct = iterativeCeiling
193258
}
259+
_ = iterElapsed
194260

195-
return clampFloat(target, float64(current), iterativeCeiling)
261+
target := iterMetricWeight*metricPct + iterTimeWeight*timePct
262+
return math.Min(target, iterativeCeiling)
196263
}
197264

198-
func iterativeMetricTarget(record Record, metricPct float64) float64 {
199-
if record.DataTotalMiB > 0 && record.DataRemainingMiB >= 0 {
200-
remainingRatio := clampFloat(record.DataRemainingMiB/record.DataTotalMiB, 0.0001, 1)
201-
shaped := 1 - math.Log1p(remainingRatio*9)/math.Log(10)
202-
return clampFloat(iterativeFloor+shaped*(iterativeCeiling-iterativeFloor), iterativeFloor, iterativeCeiling)
265+
func iterativeMetricRatio(state *State) float64 {
266+
if state.InitialRemaining <= state.Threshold {
267+
return 1
203268
}
204-
return clampFloat(iterativeFloor+(metricPct/100.0)*(iterativeCeiling-iterativeFloor), iterativeFloor, iterativeCeiling)
205-
}
206269

207-
func isIterative(record Record) bool {
208-
return record.HasIteration && record.Iteration > 0
270+
current := math.Max(state.SmoothedRemaining, state.Threshold)
271+
initial := math.Max(state.InitialRemaining, state.Threshold)
272+
base := math.Log(initial / state.Threshold)
273+
if base <= 0 {
274+
return 1
275+
}
276+
277+
ratio := 1 - math.Log(current/state.Threshold)/base
278+
return clampFloat(ratio, 0, 1)
209279
}
210280

211-
func smoothProgress(current int32, target float64, iterative bool, throttle float64, sinceLast time.Duration, metricAdvanced, initial bool) int32 {
212-
delta := target - float64(current)
213-
if delta <= 0 {
214-
return current
281+
func stageCap(iterative bool) float64 {
282+
if !iterative {
283+
return bulkCeiling
215284
}
285+
return iterativeCeiling
286+
}
216287

217-
if !initial {
218-
minInterval := bulkUpdateInterval
219-
if iterative {
220-
minInterval = iterUpdateInterval
221-
}
222-
if sinceLast > 0 && sinceLast < minInterval {
223-
return current
224-
}
288+
func stallWindow(record Record, state *State, iterative bool) float64 {
289+
if !iterative {
290+
return bulkStallSeconds
225291
}
226292

227-
factor := 0.40
228-
maxStep := 6.0
229-
if iterative {
230-
factor = 0.28
231-
maxStep = 4
232-
if throttle > 0 {
233-
maxStep += math.Round(throttle * 2)
234-
}
293+
if state.Progress >= int32(iterativeCeiling)-2 {
294+
return 24.0
235295
}
236-
if metricAdvanced {
237-
maxStep += 1
296+
if state.Progress >= int32(iterativeCeiling)-5 {
297+
return 14.0
238298
}
239-
step := math.Max(1, math.Round(delta*factor))
240-
step = math.Min(step, maxStep)
241-
return current + int32(step)
242-
}
243-
244-
func applyStatefulStall(record Record, state *State, current int32, iterative bool) int32 {
245-
window := bulkStallWindow
246-
if iterative {
247-
window = iterStallWindow
299+
if state.SmoothedRemaining > 0 && state.SmoothedRemaining <= state.Threshold {
300+
return finalStallSeconds
248301
}
249-
lastMetricAt := state.LastMetricAt
250-
if lastMetricAt.IsZero() {
251-
lastMetricAt = record.Now
302+
303+
window := iterStallSeconds - 3*record.Throttle
304+
if window < finalStallSeconds {
305+
return finalStallSeconds
252306
}
253-
if record.Now.Sub(lastMetricAt) < window {
254-
return current
307+
return window
308+
}
309+
310+
func isIterative(record Record) bool {
311+
return record.HasIteration && record.Iteration > 0
312+
}
313+
314+
func maxRemaining(record Record) float64 {
315+
if record.DataRemainingMiB > 0 {
316+
return record.DataRemainingMiB
255317
}
256-
bump := int32(1)
257-
if iterative && record.HasThrottle && record.Throttle >= 0.5 {
258-
bump = 2
318+
if record.DataTotalMiB > 0 && record.DataProcessedMiB >= 0 {
319+
r := record.DataTotalMiB - record.DataProcessedMiB
320+
if r > 0 {
321+
return r
322+
}
259323
}
260-
return clampSyncRange(current + bump)
324+
return 0
261325
}
262326

263327
func updateMetricState(record Record, state *State) {
@@ -286,11 +350,15 @@ func almostEqual(a, b float64) bool {
286350
return math.Abs(a-b) < 0.01
287351
}
288352

289-
func maxInt32(a, b int32) int32 {
290-
if a > b {
291-
return a
353+
func clampPercent(v float64) int32 {
354+
i := int32(v)
355+
if i < SyncRangeMin {
356+
return SyncRangeMin
357+
}
358+
if i > SyncRangeMax {
359+
return SyncRangeMax
292360
}
293-
return b
361+
return i
294362
}
295363

296364
func clampFloat(v, minV, maxV float64) float64 {

0 commit comments

Comments
 (0)