Summary
The RI Exchange / reshape pipeline enumerates cross-family alternatives via a hand-curated peerFamilyGroups allowlist plus a per-recommendation DescribeReservedInstancesOfferings API call. CUDly already caches AWS Cost Explorer purchase recommendations in Postgres — the reshape page should pair underutilised convertible RIs against that cache instead.
Captured in: known_issues/24_exchange_offering_cache.md
Current behaviour
In pkg/exchange/reshape.go:
ListConvertibleReservedInstances surfaces underutilised convertible RIs.
peerFamilyGroups (hardcoded map) picks candidate target families.
FindConvertibleOfferings (providers/aws/services/ec2/client.go) calls DescribeReservedInstancesOfferings per recommendation to enrich pricing.
passesDollarUnitsCheck filters the surviving candidates.
Consequences today:
- New EC2 families are invisible until someone updates
peerFamilyGroups by hand.
- Every reshape page load fans out N×M offering-enumeration API calls.
- Specialty/legacy alternatives exist only because of hand-curated group lists.
Expected behaviour
The recommendations table already stores Cost Explorer's GetReservationPurchaseRecommendation output (providers/aws/recommendations/client.go). Pair underutilised convertibles against those cached recommendations — AWS's own advice is strictly more relevant than enumerating every offering in the family, and the data is already in Postgres.
Steps to reproduce
- Launch a reshape page load while tailing backend logs.
- Observe one
DescribeReservedInstancesOfferings call per convertible RI, gated by the static peerFamilyGroups allowlist.
- Confirm the
recommendations table already has cached Cost Explorer suggestions (SELECT provider,service,region,resource_type FROM recommendations WHERE provider='aws' AND service='ec2').
Proposed fix
- Delete
peerFamilyGroups, candidateFamilies, and alternativesForTarget from pkg/exchange/reshape.go.
- Replace the
OfferingLookup signature with a recommendation-driven PurchaseRecLookup(ctx, region, currencyCode) ([]OfferingOption, error) that reads store.ListStoredRecommendations(...) and maps each row to an OfferingOption (effective monthly = UpfrontCost/termMonths + MonthlyCost).
- Rename
AnalyzeReshapingWithOfferings → AnalyzeReshapingWithRecs; the existing passesDollarUnitsCheck gate is unchanged.
- Update
internal/api/handler_ri_exchange.go::convertToExchangeTypes and internal/server/handler_ri_exchange.go::convertForAutoExchange to thread the new adapter.
- Delete
providers/aws/services/ec2/client.go::FindConvertibleOfferings if no other callers (pre-flight grep).
No new schema, no new store API, no scheduler hook needed — the recommendations table is already populated on every scheduler tick.
Edge cases
- Cache empty / cold start: no alternatives — same UX as a region with no AWS-recommended buys.
- No Cost Explorer recs in region: correct zero-alternatives display; AWS hasn't recommended anything.
- Term or currency mismatch:
passesDollarUnitsCheck rejects the pair; filter by currency at SQL layer too.
- Cross-account leak:
RecommendationFilter must include source account ID — reshape is per-account.
auto.go path: uses base AnalyzeReshaping (no lookup) → AlternativeTargets=nil. Verify with grep -rn "AlternativeTargets" internal/ pkg/.
Test plan
Unit tests in pkg/exchange/:
TestAnalyzeReshapingWithRecs_RecommendationDrivenAlternatives — fake lookup spanning m5/c5/r5, asserts cross-family alternatives surface.
TestAnalyzeReshapingWithRecs_EmptyLookupReturnsNoAlternatives — cold-start UX.
TestAnalyzeReshapingWithRecs_AppliesDollarUnitsFilter — filter gate works.
- Migrate existing
TestAnalyzeReshapingWithOfferings_* to the new signature.
- Delete
TestCandidateFamilies_*.
Handler tests:
TestPurchaseRecLookupFromStore_RegionFilter / AccountFilter / NoRecsReturnsEmpty.
References
- Commit
cd440d9ea — feat(exchange): cross-family RI alternatives for specialty + legacy (the hand-curated fix this replaces).
pkg/exchange/reshape.go, providers/aws/services/ec2/client.go::FindConvertibleOfferings.
internal/config/store_postgres_recommendations.go::ListStoredRecommendations.
- Known-issue doc:
known_issues/24_exchange_offering_cache.md.
Effort
Small — single refactor commit, no schema changes, no new failure surfaces. ~0.5–1 day including test updates.
Verification
go build ./...
go test -short -count=1 -race ./pkg/exchange/... ./internal/api/... ./internal/server/...
Post-deploy: reshape page surfaces alternatives for instance types launched after the last hand-curated allowlist update; zero DescribeReservedInstancesOfferings calls fire during a reshape load.
Summary
The RI Exchange / reshape pipeline enumerates cross-family alternatives via a hand-curated
peerFamilyGroupsallowlist plus a per-recommendationDescribeReservedInstancesOfferingsAPI call. CUDly already caches AWS Cost Explorer purchase recommendations in Postgres — the reshape page should pair underutilised convertible RIs against that cache instead.Captured in:
known_issues/24_exchange_offering_cache.mdCurrent behaviour
In
pkg/exchange/reshape.go:ListConvertibleReservedInstancessurfaces underutilised convertible RIs.peerFamilyGroups(hardcoded map) picks candidate target families.FindConvertibleOfferings(providers/aws/services/ec2/client.go) callsDescribeReservedInstancesOfferingsper recommendation to enrich pricing.passesDollarUnitsCheckfilters the surviving candidates.Consequences today:
peerFamilyGroupsby hand.Expected behaviour
The
recommendationstable already stores Cost Explorer'sGetReservationPurchaseRecommendationoutput (providers/aws/recommendations/client.go). Pair underutilised convertibles against those cached recommendations — AWS's own advice is strictly more relevant than enumerating every offering in the family, and the data is already in Postgres.Steps to reproduce
DescribeReservedInstancesOfferingscall per convertible RI, gated by the staticpeerFamilyGroupsallowlist.recommendationstable already has cached Cost Explorer suggestions (SELECT provider,service,region,resource_type FROM recommendations WHERE provider='aws' AND service='ec2').Proposed fix
peerFamilyGroups,candidateFamilies, andalternativesForTargetfrompkg/exchange/reshape.go.OfferingLookupsignature with a recommendation-drivenPurchaseRecLookup(ctx, region, currencyCode) ([]OfferingOption, error)that readsstore.ListStoredRecommendations(...)and maps each row to anOfferingOption(effective monthly =UpfrontCost/termMonths + MonthlyCost).AnalyzeReshapingWithOfferings→AnalyzeReshapingWithRecs; the existingpassesDollarUnitsCheckgate is unchanged.internal/api/handler_ri_exchange.go::convertToExchangeTypesandinternal/server/handler_ri_exchange.go::convertForAutoExchangeto thread the new adapter.providers/aws/services/ec2/client.go::FindConvertibleOfferingsif no other callers (pre-flight grep).No new schema, no new store API, no scheduler hook needed — the
recommendationstable is already populated on every scheduler tick.Edge cases
passesDollarUnitsCheckrejects the pair; filter by currency at SQL layer too.RecommendationFiltermust include source account ID — reshape is per-account.auto.gopath: uses baseAnalyzeReshaping(no lookup) →AlternativeTargets=nil. Verify withgrep -rn "AlternativeTargets" internal/ pkg/.Test plan
Unit tests in
pkg/exchange/:TestAnalyzeReshapingWithRecs_RecommendationDrivenAlternatives— fake lookup spanning m5/c5/r5, asserts cross-family alternatives surface.TestAnalyzeReshapingWithRecs_EmptyLookupReturnsNoAlternatives— cold-start UX.TestAnalyzeReshapingWithRecs_AppliesDollarUnitsFilter— filter gate works.TestAnalyzeReshapingWithOfferings_*to the new signature.TestCandidateFamilies_*.Handler tests:
TestPurchaseRecLookupFromStore_RegionFilter/AccountFilter/NoRecsReturnsEmpty.References
cd440d9ea—feat(exchange): cross-family RI alternatives for specialty + legacy(the hand-curated fix this replaces).pkg/exchange/reshape.go,providers/aws/services/ec2/client.go::FindConvertibleOfferings.internal/config/store_postgres_recommendations.go::ListStoredRecommendations.known_issues/24_exchange_offering_cache.md.Effort
Small — single refactor commit, no schema changes, no new failure surfaces. ~0.5–1 day including test updates.
Verification
go build ./... go test -short -count=1 -race ./pkg/exchange/... ./internal/api/... ./internal/server/...Post-deploy: reshape page surfaces alternatives for instance types launched after the last hand-curated allowlist update; zero
DescribeReservedInstancesOfferingscalls fire during a reshape load.