Planner: use summed slot duration for rate coverage check (#30469)

This commit is contained in:
MaIII Themd 2026-07-18 17:00:44 +07:00 • committed by GitHub
parent 16a0ead640
commit 31cf9748aa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 69 additions and 1 deletions

View file

@ -160,7 +160,7 @@ func (t *Planner) Plan(requiredDuration, precondition time.Duration, targetTime
rates = clampRates(rates, now, targetTime)
// check if rate coverage is sufficient for planning
if len(rates) == 0 || rates[len(rates)-1].End.Sub(rates[0].Start) < requiredDuration {
if len(rates) == 0 || Duration(rates) < requiredDuration {
t.log.DEBUG.Printf("planner: rate coverage in [%v,%v] insufficient for required duration %v- falling back to simple plan",
now.Local(), targetTime.Local(), requiredDuration.Round(time.Second))
return simplePlan

View file

@ -271,6 +271,74 @@ func TestChargeAfterTargetTime(t *testing.T) {
assert.Equal(t, simplePlan, plan, "expected simple plan")
}
func TestGappedRatesCoverage(t *testing.T) {
clock := clock.NewMock()
ctrl := gomock.NewController(t)
// two 1h slots separated by a 4h gap: 2h of real coverage spanning 6h
rr := api.Rates{
{Start: clock.Now(), End: clock.Now().Add(time.Hour), Value: 10},
{Start: clock.Now().Add(5 * time.Hour), End: clock.Now().Add(6 * time.Hour), Value: 10},
}
trf := api.NewMockTariff(ctrl)
trf.EXPECT().Rates().AnyTimes().Return(rr, nil)
p := &Planner{
log: util.NewLogger("foo"),
clock: clock,
tariff: trf,
}
requiredDuration := 3 * time.Hour
// summed coverage (2h) is below requiredDuration (3h), so the planner must
// fall back to simplePlan and still cover the full duration rather than
// returning an under-covered plan based on the wall-clock span (6h)
for _, continuous := range []bool{false, true} {
plan := p.Plan(requiredDuration, 0, clock.Now().Add(6*time.Hour), continuous)
assert.GreaterOrEqual(t, Duration(plan), requiredDuration,
"plan must cover requiredDuration even when the rate series has gaps (continuous=%v)", continuous)
}
}
func TestExactCoverageNoFallback(t *testing.T) {
clock := clock.NewMock()
ctrl := gomock.NewController(t)
// 3h of real coverage inside a 4h window (1h gap), exactly equal to
// requiredDuration. Duration(rates) == requiredDuration must NOT trigger the
// simplePlan fallback (the guard uses a strict <), so the planner builds a
// plan from the real rates instead. Locks the < behavior against a slip to <=.
rr := api.Rates{
{Start: clock.Now(), End: clock.Now().Add(time.Hour), Value: 10},
{Start: clock.Now().Add(time.Hour), End: clock.Now().Add(2 * time.Hour), Value: 20},
{Start: clock.Now().Add(3 * time.Hour), End: clock.Now().Add(4 * time.Hour), Value: 30},
}
trf := api.NewMockTariff(ctrl)
trf.EXPECT().Rates().AnyTimes().Return(rr, nil)
p := &Planner{
log: util.NewLogger("foo"),
clock: clock,
tariff: trf,
}
requiredDuration := 3 * time.Hour // == Duration(rr)
targetTime := clock.Now().Add(4 * time.Hour)
simplePlan := api.Rates{
{Start: targetTime.Add(-requiredDuration), End: targetTime},
}
for _, continuous := range []bool{false, true} {
plan := p.Plan(requiredDuration, 0, targetTime, continuous)
assert.NotEqual(t, simplePlan, plan,
"exact coverage (Duration == requiredDuration) must not fall back to simplePlan (continuous=%v)", continuous)
}
}
func TestPrecondition(t *testing.T) {
clock := clock.NewMock()
ctrl := gomock.NewController(t)