Skip to content

Commit 218912b

Browse files
cristimclaude
andauthored
fix(aws): credit own replacement RIs in RDS family expiry sizing (#347)
Cost Explorer coverage is a lookback average, so RIs bought to replace expiring ones are not in it yet. With a partial (single-account) inventory the max(C-E, min(C, A-E)) formula took the C-E branch and re-bought them. Add ApplyFamilyNUSizingRDSWithExpiryOptions: visible active, non-expiring RIs that started inside CoverageLookbackDays (R) are credited, covered = max(0, max(min(C, C-E+R), min(C, A-E))). R = 0 is identical to the merged function, which is unchanged. Refs LeanerCloud/cloud-commitments-cli#2132, #2175 Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
1 parent 4178c85 commit 218912b

3 files changed

Lines changed: 346 additions & 16 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
3232

3333
### Added
3434

35+
- **`recommendations.ApplyFamilyNUSizingRDSWithExpiryOptions`:** the
36+
rebuy-window RDS family sizing, plus credit for replacements already bought.
37+
Coverage is a lookback average, so RIs bought inside `CoverageLookbackDays`
38+
are not in it yet; with R the NU of visible active, non-expiring RIs that
39+
started inside the lookback, post-expiry covered is
40+
`max(0, max(min(C, C - E + R), min(C, A - E)))`. It is never below the R = 0
41+
formula, so it can only buy less, and a partial inventory still never removes
42+
more than expires. `FamilyExpiryReport.ReplacementNU` records R.
43+
`CoverageLookbackDays <= 0` credits none. Additive; the other functions are
44+
unchanged (LeanerCloud/cloud-commitments-cli#2132, #2175).
3545
- **`common.SavingsPlanDetails.Currency`:** the ISO 4217 code the hourly
3646
commitment is denominated in. Additive and unused until the Azure Savings Plan
3747
client starts requiring it (#46).

‎providers/aws/recommendations/family_nu.go‎

Lines changed: 86 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -377,13 +377,32 @@ type FamilyExpiryReport struct {
377377
// ExpiringNU is the normalized units of active RDS RIs ending inside the
378378
// window that were deducted from family coverage.
379379
ExpiringNU float64
380+
// ReplacementNU is the normalized units of visible active, non-expiring RDS
381+
// RIs that started inside the coverage lookback and so are not yet in the
382+
// lagged coverage, summed over families that have expiring RIs. It is only
383+
// non-zero when FamilyExpiryOptions.CoverageLookbackDays > 0.
384+
ReplacementNU float64
380385
// Unusable counts active RDS RIs expiring inside the window that could not
381386
// be deducted: an unparsable instance class (NU 0) or a family key with no
382387
// coverage family (e.g. an engine spelled differently from the coverage
383388
// report). Callers must surface this; it never means "no expiry".
384389
Unusable int
385390
}
386391

392+
// FamilyExpiryOptions configures ApplyFamilyNUSizingRDSWithExpiryOptions.
393+
type FamilyExpiryOptions struct {
394+
// WindowDays is the rebuy window: active RIs ending within it are treated
395+
// as expiring. <= 0 disables the expiry adjustment.
396+
WindowDays int
397+
// CoverageLookbackDays is the lookback, in days, of the Cost Explorer
398+
// coverage average the caller passed in (the lookbackDays given to
399+
// GetRICoverageMap). Coverage is an average over that period, so an RI that
400+
// started inside it is not yet fully reflected. Visible active, non-expiring
401+
// RIs that started inside it are credited as already-bought replacements.
402+
// <= 0 credits none, which is exactly ApplyFamilyNUSizingRDSWithExpiry.
403+
CoverageLookbackDays int
404+
}
405+
387406
// ApplyFamilyNUSizingRDSWithExpiry is ApplyFamilyNUSizingRDS with rebuy-window
388407
// expiry folded into family coverage first, in normalized units. RDS RIs are
389408
// size-flexible and Cost Explorer reports coverage under the size actually
@@ -400,32 +419,86 @@ type FamilyExpiryReport struct {
400419
// actually expires.
401420
// windowDays <= 0 or no commitments leaves coverage unchanged. A family
402421
// with several size rows or linked-account rows is deducted once.
422+
//
423+
// This form ignores replacement RIs the account already bought for the
424+
// expiring ones; callers that know the coverage lookback should use
425+
// ApplyFamilyNUSizingRDSWithExpiryOptions.
403426
func ApplyFamilyNUSizingRDSWithExpiry(
404427
recs []common.Recommendation,
405428
coverage PoolCoverageMap,
406429
targetPct float64,
407430
commitments []common.Commitment,
408431
windowDays int,
432+
) (sizedRDS, nonRDS []common.Recommendation, drops FamilyDropCounts, expiry FamilyExpiryReport) {
433+
return ApplyFamilyNUSizingRDSWithExpiryOptions(recs, coverage, targetPct, commitments,
434+
FamilyExpiryOptions{WindowDays: windowDays})
435+
}
436+
437+
// ApplyFamilyNUSizingRDSWithExpiryOptions is ApplyFamilyNUSizingRDSWithExpiry
438+
// that also credits replacements already bought. With R = the NU of visible
439+
// active, non-expiring RIs in the family that started within
440+
// opts.CoverageLookbackDays, the family's post-expiry covered NU is
441+
//
442+
// max(0, max(min(C, C - E + R), min(C, A - E)))
443+
//
444+
// where C is CoveredNU, E the expiring NU and A the active NU. R only raises
445+
// the first term, so the result is never below the R = 0 formula (the purchase
446+
// never grows), never above C, and never more than E below C. Replacement RIs
447+
// bought inside the lookback are in the lagged C only in part, so crediting
448+
// them in full can only under-count the gap, never over-count it. With full
449+
// inventory the min(C, A - E) term already counts them.
450+
func ApplyFamilyNUSizingRDSWithExpiryOptions(
451+
recs []common.Recommendation,
452+
coverage PoolCoverageMap,
453+
targetPct float64,
454+
commitments []common.Commitment,
455+
opts FamilyExpiryOptions,
409456
) (sizedRDS, nonRDS []common.Recommendation, drops FamilyDropCounts, expiry FamilyExpiryReport) {
410457
if targetPct <= 0 || targetPct > 100 {
411458
return nil, recs, drops, expiry
412459
}
413460
familyCov := AggregateRDSFamilyCoverage(coverage)
414-
expiry = adjustFamilyCoverageForExpiry(familyCov, commitments, windowDays, time.Now())
461+
expiry = adjustFamilyCoverageForExpiry(familyCov, commitments, opts, time.Now())
415462
sizedRDS, nonRDS, drops = sizeRDSFamilies(recs, familyCov, targetPct)
416463
return sizedRDS, nonRDS, drops, expiry
417464
}
418465

419466
// adjustFamilyCoverageForExpiry mutates familyCov (a fresh aggregate owned by
420-
// the caller) as described on ApplyFamilyNUSizingRDSWithExpiry.
421-
func adjustFamilyCoverageForExpiry(familyCov map[string]FamilyCoverage, commitments []common.Commitment, windowDays int, now time.Time) FamilyExpiryReport {
467+
// the caller) as described on ApplyFamilyNUSizingRDSWithExpiryOptions.
468+
func adjustFamilyCoverageForExpiry(familyCov map[string]FamilyCoverage, commitments []common.Commitment, opts FamilyExpiryOptions, now time.Time) FamilyExpiryReport {
422469
var report FamilyExpiryReport
423-
if windowDays <= 0 || len(commitments) == 0 {
470+
if opts.WindowDays <= 0 || len(commitments) == 0 {
424471
return report
425472
}
426-
cutoff := now.Add(time.Duration(windowDays) * 24 * time.Hour)
427-
activeNU := make(map[string]float64)
428-
expiringNU := make(map[string]float64)
473+
totals := tallyFamilyCommitments(familyCov, commitments, opts, now, &report)
474+
for fk, exp := range totals.expiring {
475+
fc := familyCov[fk]
476+
rep := totals.replacement[fk]
477+
fc.CoveredNU = math.Max(0, math.Max(math.Min(fc.CoveredNU, fc.CoveredNU-exp+rep), math.Min(fc.CoveredNU, totals.active[fk]-exp)))
478+
familyCov[fk] = fc
479+
report.ExpiringNU += exp
480+
report.ReplacementNU += rep
481+
}
482+
return report
483+
}
484+
485+
// familyNUTotals holds per-family normalized-unit sums over the inventory.
486+
type familyNUTotals struct {
487+
active map[string]float64 // every active RI
488+
expiring map[string]float64 // active RIs ending inside the rebuy window
489+
replacement map[string]float64 // active, non-expiring RIs started inside the coverage lookback
490+
}
491+
492+
// tallyFamilyCommitments sums the inventory per family and counts expiring RIs
493+
// it cannot use into report.Unusable.
494+
func tallyFamilyCommitments(familyCov map[string]FamilyCoverage, commitments []common.Commitment, opts FamilyExpiryOptions, now time.Time, report *FamilyExpiryReport) familyNUTotals {
495+
cutoff := now.Add(time.Duration(opts.WindowDays) * 24 * time.Hour)
496+
lagStart := now.Add(-time.Duration(opts.CoverageLookbackDays) * 24 * time.Hour)
497+
t := familyNUTotals{
498+
active: make(map[string]float64),
499+
expiring: make(map[string]float64),
500+
replacement: make(map[string]float64),
501+
}
429502
for i := range commitments {
430503
fk, units, expiring, ok := rdsCommitmentFamilyUnits(&commitments[i], cutoff)
431504
if !ok {
@@ -434,23 +507,20 @@ func adjustFamilyCoverageForExpiry(familyCov map[string]FamilyCoverage, commitme
434507
}
435508
continue
436509
}
437-
activeNU[fk] += units
510+
t.active[fk] += units
438511
if !expiring {
512+
if opts.CoverageLookbackDays > 0 && commitments[i].StartDate.After(lagStart) {
513+
t.replacement[fk] += units
514+
}
439515
continue
440516
}
441517
if _, known := familyCov[fk]; !known {
442518
report.Unusable++
443519
continue
444520
}
445-
expiringNU[fk] += units
521+
t.expiring[fk] += units
446522
}
447-
for fk, exp := range expiringNU {
448-
fc := familyCov[fk]
449-
fc.CoveredNU = math.Max(0, math.Max(fc.CoveredNU-exp, math.Min(fc.CoveredNU, activeNU[fk]-exp)))
450-
familyCov[fk] = fc
451-
report.ExpiringNU += exp
452-
}
453-
return report
523+
return t
454524
}
455525

456526
// rdsCommitmentFamilyUnits maps an active RDS commitment to its family key and

0 commit comments

Comments
 (0)