Skip to content

fix(aws): EC2 per-pool expiry ignores size flexibility and keys non-RDS services without the service #450

Closed

Description

@cristim

Summary

Two known limitations of the EC2 per-pool expiry adjustment in providers/aws/recommendations/expiry.go. They predate go#449, which documents them in its godoc.

  1. Size flexibility. Pools key on the exact instance type. A replacement of a different size in the same family is not credited, and an expiring RI of one size is not netted against coverage of another size. Size-flexible regional Linux/default-tenancy RIs are therefore over-deducted, which makes the tool re-buy. The RDS family path (family_nu.go) handles this with normalized units.
  2. Key collision. Non-RDS services share the commitmentPoolKey shape (expiry.go:~153). If two services used an identical ResourceType string in the same region, their expiring counts would combine.

Expected

  1. Credit and net EC2 size-flexible RIs in normalized units within the family, as the RDS family path does.
  2. Include the service in the pool key, or prove it already is unique per service.

Test

  1. A large RI expiring, replaced by two medium RIs, produces no re-buy.
  2. Two services with the same ResourceType string in one region keep their expiring counts separate.

Refs #346, #347.

Activity

  1. self-assigned this
    on Oct 16, 2026
  2. cristim commented on Oct 16, 2026

    @cristim
    MemberAuthor

    Claiming: working on this.

  3. cristim commented on Oct 16, 2026

    @cristim
    MemberAuthor

    Plan (go-450-ec2-expiry-sizeflex-key.md) findings on main:

    Part 1, the key collision, is not reachable today. The non-RDS services that share the 2-part region:type key have disjoint ResourceType shapes:

    • EC2 m5.large
    • ElastiCache cache.m5.large
    • OpenSearch m5.large.search
    • Redshift dc2.* / ra3.*
    • MemoryDB db.r6g.large

    RDS uses its own 4-part rdsPoolKey. Adding a service dimension would change poolKey, lookupPoolKey and commitmentPoolKey together, and with them the exported PoolCoverageMap shape that the ladder and the CLI read. So I'm doing a guard test over the real per-service shapes plus a comment on commitmentPoolKey, with no key change.

    Part 2, EC2 size flexibility, needs maintainer decisions before implementation, because it changes recommended purchase quantities. Proposal:

    Decisions needed:

    1. Attribution. Should every size in the family lose 100*d/T coverage points (uniform), or should d be weighted toward the expiring sizes?
    2. Lookback 0. Should cross-size netting apply even when CoverageLookbackDays == 0, i.e. on expiring-vs-expiring netting alone?
    3. Unknown dimensions. Should an unknown or empty platform, tenancy or scope count as non-flex? The proposed default is yes, which keeps today's exact-pool behaviour.

    Part 2 is about 400 lines and may split into two PRs. It changes no API, so the CLI caller picks it up on upgrade. I'm leaving part 2 open pending these answers.

  4. cristim commented on Nov 12, 2026

    @cristim
    MemberAuthor

    Fixed on main in 984f206, 5a3302b and f1ffe43 (head f1ffe43).

    Root cause

    • Size flexibility: commitmentPoolKey keyed EC2 by exact instance type, so an expiring large RI was not netted against a replacement of another size in the same family, and the rec re-bought.
    • Key collision: non-RDS pools had no service name, so equal ResourceType strings in different services could net together.

    Change (providers/aws/recommendations/expiry.go)

    • EC2 types in a size-flexible family net per region and family, each instance weighted by its AWS normalization factor. At lookback 0 nothing is credited, so the net equals the commitment's own units.
    • Every other non-RDS service keys on its service name; ElastiCache keeps its engine (fix(aws): ElastiCache expiry pools ignore the engine, so Redis and Memcached reservations net against each other #475). Producer aliases (compute, search, data-warehouse, cache) map to one canonical service.
    • Savings Plans commitments are skipped.
    • Unknown service, or an EC2 size with no normalization factor in a flexible family, returns an error and leaves recs untouched. The three exported Adjust functions now return error; this is an API change, recorded in CHANGELOG Notices.

    Tests

    • TestPoolExpiryNetsSizeFlexWithinFamily: large replaced by two mediums, no re-buy.
    • TestPoolExpiryWeightsExpiringSizeByNormalizationFactor: lookback 0 weighting.
    • TestPoolExpiryKeysNonRDSByService: no cross-service netting.
    • TestPoolExpiryUnknownDimensionFailsLoud: error path, recs untouched.
    • TestPoolExpiryAcceptsProducerServiceAliases, TestPoolExpiryAcceptsComputeRecommendationAlias.

    The first three fail on the previous code. Mutation probes on the weight, the service key and both error paths each fail a test.

    Known limits, not in this change

    • Size netting ignores Platform, Tenancy and Scope on the commitment.
    • Metal sizes (no family-independent factor) return an error, so one expiring metal RI disables the expiry adjustment for the call.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions