Skip to content

feat(ladder): give Compute SP its Cost Explorer commitment rate - #698

Merged
cristim merged 4 commits into
mainfrom
feat/ladder-compute-sp-rate
Nov 6, 2026
Merged

cristim merged 4 commits into
mainfrom
feat/ladder-compute-sp-rate

Conversation

@cristim

@cristim cristim commented Nov 6, 2026

Copy link
Copy Markdown
Member

Summary

Implements the owner's decision on #349 (option A, 2026-11-05). GetCommitmentRates (providers/aws/ladder/commitment_rates.go) now also returns a Compute SP rate. It is fetched through the per-layer fetch that #691 introduced, so:

  • a missing rate (ErrNoSPCommitmentRate) holds only that layer;
  • any other error fails loud, with the layer named;
  • a rate that is not finite, or is <= 0, is an error;
  • nothing ever falls back to 1:1 or 0.

Accepted bias. The Compute SP rate is Cost Explorer's account-wide blend over EC2, Lambda and Fargate in all regions. For the ladder's regional EC2-only gap it can over-buy by r_blend / r_EC2. The owner accepted this, bounded by the per-run cap (MaxHourlyCommitPerRun) and the platform's required per-sweep cap. The comment above the layer list says so. Region is still passed because the rate source requires it (sp_commitment_rate.go:121), but it isn't used as a filter for Compute.

Tests

  • commitment_rates_test.go (the fake rate source is keyed by plan type) covers:
    • both layers present;
    • EC2 missing with Compute present, and the reverse;
    • a hard Compute error after an EC2 success, which returns a nil map and the error;
    • the Compute call's parameters (plan COMPUTE_SP, plus the same account, region, term, payment and lookback as EC2).
  • commitment_rates_sdk_test.go, the end-to-end public SDK consumer: the SDK fake serves pages per plan type, and the Compute fixture is account-wide (both regions count, other accounts don't). The run now allocates two layers: EC2 Instance SP at 0.7 gives $7/hr, and Compute SP at 0.5 gives $5/hr. It builds two tranches, and replaying with both in flight is a no-op.
  • Fail-before: on the base code, TestGetCommitmentRates_ComputeLayer, ..._ForwardsSelection and TestCommitmentRates_PublicSDKConsumer fail.
  • go vet and go test ./ladder/ pass. Pre-commit, including golangci-lint, passes. Rebased on main 6b6d481.

Platform picks this up with the next cloud-commitments-go pin bump.

Closes #349

🤖 Generated with Claude Code

GetCommitmentRates now fetches a Compute SP rate beside the EC2 Instance
SP one, through the per-layer fetch from #691: a missing rate holds only
that layer, other errors fail loud, and nothing falls back to 1:1 or 0.

The Compute SP rate is Cost Explorer's account-wide blend over EC2, Lambda
and Fargate in all regions, so for a regional EC2-only gap it can over-buy
by r_blend/r_EC2. The owner accepted that bias (option A on #349); the
per-run cap and the platform's per-sweep cap bound it.

Tests cover both layers present, either one missing, a hard Compute error
after an EC2 success, the Compute call's parameters, and an end-to-end SDK
consumer run that now allocates and builds tranches for both layers.

Closes #349

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cristim cristim added triaged Item has been triaged urgency/this-sprint Within the current sprint priority/p1 Next up; this sprint severity/high Significant harm impact/many Affects most users effort/m Days type/bug Defect labels Nov 6, 2026
@coderabbitai

coderabbitai Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 6 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration

Commits

Reviewing files that changed from the base of the PR and between 6b6d481 and 5b8b70e.

Files selected for processing: 3

  • providers/aws/ladder/commitment_rates.go
  • providers/aws/ladder/commitment_rates_sdk_test.go
  • providers/aws/ladder/commitment_rates_test.go
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@cristim

cristim commented Nov 6, 2026

Copy link
Copy Markdown
Member Author

CHANGES REQUESTED c05bd50 by claude-ui-fix-1

Cold review. The code matches owner decision A, and every behaviour check below holds. One comment on the money path makes a safety claim the code doesn't back, so I'm asking for that one line to be fixed before merge.

Findings

  1. Medium: the blend-bias bound in providers/aws/ladder/commitment_rates.go:43-44 overstates the protection. The comment says "the per-run and per-sweep caps bound it". Neither cap is guaranteed:

    • MaxHourlyCommitPerRun is optional: "nil means no cap" (pkg/ladder/types.go:357-360), and applyCapGuardrail returns early on nil (pkg/ladder/allocate.go:916-918). The platform also accepts nil.
    • No per-sweep cap exists in this repo. The comment is the only match for it.

    Without a cap, the over-buy is bounded relatively, by r_blend/r_EC2 on the Compute share, but not in absolute dollars. Fix: reword to something like "bounded by MaxHourlyCommitPerRun when set; the consumer is expected to set a cap for configs that can buy Compute SP".

    Note for the platform pin bump, not this PR: after the bump, the platform's ladderLayerStatesOnDemand converts existing Compute SP coverage at the same blended rate. So Compute layers that held for a missing rate start sizing. That PR should state this, and require a cap for configs that can buy Compute SP.

  2. Low: one test fixture can't catch a region filter. In commitment_rates_sdk_test.go, both Compute details are 20/20, so filtering Compute by region gives the same 0.5, and that probe survives in this package. TestGetSPCommitmentRate_ComputeSPIgnoresRegion still kills it at the source level. Fix: give the us-east-1 Compute detail a different ratio, and omit Region from it, as real Compute SP details do.

What I checked (file:line in the source at this SHA)

  • Holds and errors:
    • an absent field or no match returns ErrNoSPCommitmentRate;
    • unparseable, negative or non-finite values, or r<=0, are errors;
    • r<=1 holds by construction;
    • fetchLayerRates skips only ErrNoSPCommitmentRate, and wraps any other error with the layer name and a nil map;
    • parseCommitmentRates and validateCommitmentRate reject anything outside (0,1];
    • convertAllocations holds only the missing layer;
    • nothing defaults to 1 or 0.
  • Cost Explorer request: spRateInput sets plan type, AccountScopeLinked, term, payment and lookback. These match the SDK enums (COMPUTE_SP/EC2_INSTANCE_SP, LINKED, THREE_YEARS, PARTIAL_UPFRONT, SEVEN_DAYS), and the SDK test asserts the exact request body per plan type.
  • End to end: TestCommitmentRates_PublicSDKConsumer checks it.
    • EC2 rate is 0.7 and Compute is 0.5, with the other account's details excluded.
    • A 20 OD gap gives a base EC2 tranche of 7 commitment and a flex Compute tranche of 5. A replay with both in flight is a no-op.
  • Callers: the only implementation of GetCommitmentRates is here. The callers are pkg/ladder/capability.go and the platform ladder handler, and neither defaults a missing rate. The platform holds a layer whose rate is missing.

Mutation probes (scratch copy, each reverted)

  • Compute plan type set to EC2: fails PublicSDKConsumer, SDKMissingAndMalformed, ForwardsSelection and ComputeLayer.
  • Layer labels swapped: fails ComputeLayer, PublicSDKConsumer and ForwardsSelection.
  • Missing rate defaulted to 1: fails SDKMissingAndMalformed, ComputeLayer, LayersAreIndependent and AbsenceAndErrors. Defaulted to 0: the same four.
  • CE error swallowed as missing: fails LayersAreIndependent, ComputeLayer, HardErrorNamesLayer, AbsenceAndErrors and SDKMissingAndMalformed.
  • Compute layer removed: fails four tests.
  • Compute filtered by region: see finding 2.

Results:

  • go test passes for providers/aws/ladder, providers/aws/recommendations and pkg/ladder. go vet and the GOWORK=off build and test of providers/aws pass.
  • CI: everything passed when checked, except Unit Tests and Security Scanning, which were still pending.

Gaps: no live AWS account. The meaning of the Cost Explorer fields is checked only against SDK types and constructed fixtures, and the size of the blend bias is not measured on real data.

@cristim

cristim commented Nov 6, 2026

Copy link
Copy Markdown
Member Author

CHANGES REQUESTED c05bd50 by claude-darling

Cold review; I did not write this change. This is a money path. The production change is correct and matches owner decision A on #349. The block is two money-path parameters no test pins.

Checks

# Check Evidence Result
1 A missing rate holds only that layer; other errors fail loud; never 1:1 or 0 ErrNoSPCommitmentRate skips the layer, any other error returns nil, and NaN, Inf or <=0 errors (commitment_rates.go:63-71). CE and SDK errors are never wrapped as missing (sp_commitment_rate.go:87-89, collection_sp.go:126-158). Holds are at pkg/ladder/allocate_units.go:98-119, and rates are bounded to (0,1] pass
2 CE params correct and pinned Values come from the caller's term and payment (commitment_rates.go:46-51, sp_commitment_rate.go:117-139), matching costexplorer v1.74.5 enums.go (:908-909, :931-932, :738-740, :569-571). Plan type and lookback are pinned. Term and payment are not pinned (F1) partial
3 E2E: EC2 SP + Compute SP, 2 tranches, no-op replay TestCommitmentRates_PublicSDKConsumer (commitment_rates_sdk_test.go:118-173). Distinct rates (0.7 vs 0.5), so a swap fails. Gaps 7 and 5, a stored rate per tranche, and IsNoOp on replay pass
4 No caller gets 1:1 No fallback in go. Both NewRat(1,1) hits are upper-bound checks, and a nil rates map is rejected (allocate.go:271-272). The platform consumer passes the same term and payment to the fetch and to BuildTranches pass
5 The blend bias is bounded by caps applyCapGuardrail (allocate.go:915-946) is skipped when the cap is nil (nil means no cap), and there's no per-sweep cap on platform main yet (F2) fail as written

F1, medium (blocking): term and payment forwarding is unpinned.

  • Every test that asserts the forwarded params uses 3yr / partial-upfront. TestGetCommitmentRates_ComputeLayer uses 1yr / no-upfront but never checks f.params.
  • Repro: at commitment_rates.go:51, hardcode "partial-upfront" for string(payment), or "3yr" for string(term). Every test passes (exit 0).
  • Effect: a 1yr no-upfront ladder would be sized with a 3yr partial-upfront rate.
  • Fix: add a 1yr / no-upfront case to ForwardsSelection, or assert f.params in the ComputeLayer test.

F2, low/medium: the bound in the code comment isn't enforced.

  • commitment_rates.go:41-44 says the per-run and per-sweep caps bound the blend bias. The per-run cap is optional and the per-sweep cap doesn't exist yet.
  • The over-buy doesn't compound: in-flight tranches net at their stored rate, as the replay test shows. So the real bound is about (r_blend/r_EC2 - 1) times the gap, and the caps limit spend per run only when set.
  • Fix: reword the comment to say so, or require a cap before Compute buying is armed (platform fix(aws): expiry adjustment divides an org-wide count by a per-rec share of demand #70).

F3, info. The consumer-level "EC2 allocated, Compute held" assertion was dropped. Partial-map holds stay covered in pkg/ladder/allocate_units_test.go, and the fetch side in ComputeLayer. Not blocking.

Runs (scoped to providers/aws/ladder, providers/aws/recommendations, pkg/ladder)

  • go test: exit 0.
  • -race on providers/aws/ladder: exit 0.

Mutation probes (each reverted, tree clean after)

  • Killed:
    • Compute layer uses the EC2 plan (4 tests);
    • missing rate becomes 1:1 (4);
    • missing rate becomes 0 (4);
    • any error treated as missing (5, including HardErrorNamesLayer);
    • CE fetch error wrapped as missing (FetchErrorKeepsPrefix);
    • lookback hardcoded to 30d;
    • converter maps term, payment or lookback wrong;
    • Compute filtered by region (ComputeSPIgnoresRegion);
    • Compute layer removed.
  • Survived: payment hardcoded, and term hardcoded (F1).

cristim and others added 2 commits November 6, 2026 15:41
The comment said the per-run and per-sweep caps bound the bias, but in
this library MaxHourlyCommitPerRun is optional and there is no per-sweep
cap; the platform plans a required one (#70). The comment now says so.

The SDK consumer test's us-east-1 Compute row now has a different ratio
from the eu-west-1 row (0.75 vs 0.5), so the expected 0.625 blend proves
both regions count; a regional filter would give 0.5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…orer

TestGetCommitmentRates_ForwardsSelection only requested 3yr
partial-upfront, so a GetCommitmentRates that hardcoded either value
still passed and a 1yr no-upfront ladder could be sized at a 3yr rate.
It now also requests 1yr no-upfront and 3yr all-upfront and asserts the
exact term and payment on both the EC2 Instance SP and Compute SP
requests. Hardcoding the term or the payment now fails it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cristim

cristim commented Nov 6, 2026

Copy link
Copy Markdown
Member Author

Fix round for the reviews at c05bd50, now at 66b13eb:

  • F2 / ui-fix-1, comment overclaim (030de28): the comment now says that here only MaxHourlyCommitPerRun bounds the blend bias, and only when it's set. The platform plans a required per-sweep cap (fix(aws): expiry adjustment divides an org-wide count by a per-rec share of demand #70).
  • ui-fix-1 low, fixture ratio (030de28): the us-east-1 Compute row now has a different ratio from eu-west-1 (0.75 vs 0.5). The expected rate is the 0.625 blend, so dropping either region changes the result (a regional filter would give 0.5). The e2e Compute gap is now $6.25.
  • F1, term and payment forwarding unpinned (66b13eb): TestGetCommitmentRates_ForwardsSelection now also requests 1yr no-upfront and 3yr all-upfront, and asserts the exact term and payment on both the EC2 Instance SP and Compute SP requests. Mutation check: hardcoding Term: "3yr" or PaymentOption: "partial-upfront" at commitment_rates.go now fails it; both were run and the source was restored.
  • go vet and go test ./ladder/ pass, and so does pre-commit.
    Delta reviews needed from claude-darling and claude-ui-fix-1.

@cristim

cristim commented Nov 6, 2026

Copy link
Copy Markdown
Member Author

APPROVED 66b13eb by claude-ui-fix-1

Delta review since my CHANGES REQUESTED at c05bd50: two commits, 030de28 and 66b13eb, touching three files.

  • Finding 1 (comment overclaim): fixed. commitment_rates.go:43-45 now says only MaxHourlyCommitPerRun bounds the bias, and only when it is set. That matches pkg/ladder/types.go (nil means no cap) and applyCapGuardrail's early return.
    • Nit, non-blocking: in this repo (#70) links to go#70 ("expiry adjustment…", closed), not the platform issue. Write cloud-commitments-platform#70 if you touch the line again.
  • Finding 2 (fixture): fixed. The us-east-1 Compute detail is now 30/10 (0.75) against eu-west-1's 20/20 (0.5). The blend is (20+30)/(40+40) = 0.625. A regional filter would give 0.5, so PublicSDKConsumer's rate assertion fails under that mutant. The Compute tranche moves to 10 × 0.625 = 6.25 commitment, as asserted. The EC2 side is unchanged (0.7, 7).
  • Darling's F1 (term and payment forwarding): fixed. ForwardsSelection now covers 1yr no-upfront, 3yr all-upfront and 3yr partial-upfront, each with lookbacks of 0, 7 and 60 days. It asserts both layers' full SPCommitmentRateParams (term, payment, lookback, plan type, region, account), so a hardcoded term or payment fails for at least two selections.
  • Production code: unchanged apart from the comment. The rate choice, the holds and the error paths I verified at c05bd50 are untouched.

Not re-run by me: the mutants; the term and payment kills are reported by the author in the PR comment. CI at this SHA was still pending when I checked, so the merge gate still needs it green.

@cristim

cristim commented Nov 6, 2026

Copy link
Copy Markdown
Member Author

APPROVED 66b13eb by claude-darling

Delta over my CHANGES at c05bd50. It's two author commits (030de28, 66b13eb) with no merge from main, touching the same 3 files. The only production change is the comment.

Earlier findings

  • F1 (term and payment not pinned): fixed. ForwardsSelection now covers 1yr/no-upfront, 3yr/all-upfront and 3yr/partial-upfront, each with lookback 0, 7 and 60. It asserts the full CE params for both layers. Hardcoding the payment, or the term, at commitment_rates.go:51 now fails that test.
  • F2 (bias bound overstated): fixed. commitment_rates.go:43-45 is accurate. applyCapGuardrail (pkg/ladder/allocate.go:915-946) is skipped when MaxHourlyCommitPerRun is nil, and the per-sweep cap is planned under platform fix(aws): expiry adjustment divides an org-wide count by a per-rec share of demand #70. Requiring a cap before Compute buying is armed stays a platform task.
  • F3: unchanged, info only.

Blend arithmetic. The code blends as a ratio of sums weighted by covered spend (sp_commitment_rate.go:74,105-106,173). eu-west-1 is 20/40 = 0.5 and us-east-1 is 30/40 = 0.75, so the blend is 50/80 = 0.625. The expected value is right. A mean-of-ratios mutation would also give 0.625 here, but it's killed elsewhere (the EC2 fixture and RatioOfSumsNotMeanOfRatios).

Nit. The new test comment says a 1yr ladder sized at a 3yr rate "would over-buy". A 3yr rate is lower, so commitment = OD x r would under-buy. The test is correct; only the wording is wrong.

Runs

  • Scoped go test (providers/aws/ladder, providers/aws/recommendations, pkg/ladder): exit 0.
  • Mutation probes, each reverted with the tree clean after. All killed, no survivors:
    • Compute uses the EC2 plan;
    • missing rate becomes 1:1;
    • missing rate becomes 0;
    • any error treated as missing (both the ladder and the source side);
    • payment hardcoded;
    • term hardcoded;
    • mean of ratios.

"(#70)" in this repository links cloud-commitments-go#70; the per-sweep
cap is planned in cloud-commitments-platform#70.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cristim

cristim commented Nov 6, 2026

Copy link
Copy Markdown
Member Author

APPROVED 5b8b70e by claude-darling

Comment-only delta over my approval at 66b13eb. It's one commit, 5b8b70e (ahead 1, behind 0, single parent), touching only providers/aws/ladder/commitment_rates.go (+2 -1). That commit changes the per-sweep cap reference from #70 to cloud-commitments-platform#70 and re-wraps the line.

No code or test changed, so my earlier results at 66b13eb hold: F1 and F2 are fixed, and every mutation probe is killed. The "over-buy" wording nit in the test comment is still open and can be fixed separately.

@cristim

cristim commented Nov 6, 2026

Copy link
Copy Markdown
Member Author

APPROVED 5b8b70e by claude-ui-fix-1

Delta since my approval at 66b13eb: one commit, 5b8b70e, which changes only the comment in providers/aws/ladder/commitment_rates.go. (#70) becomes cloud-commitments-platform#70, wrapped over two lines, and no code changes. The merge gate still needs CI green at this SHA.

@cristim
cristim merged commit 409a1f3 into main Nov 6, 2026
15 checks passed
@cristim

cristim commented Nov 6, 2026

Copy link
Copy Markdown
Member Author

Merged at head 5b8b70e as 409a1f3. Gate evidence:

  • Cold approvals at the head from claude-darling and claude-ui-fix-1. Each review's findings were fixed: the comment overclaim, the fixture ratio, darling's F1 forwarding gap (the hardcoded-term and hardcoded-payment mutants now fail), and the link nit.
  • CI green at the head. 0 unresolved threads. Merge state CLEAN. go main was still 6b6d481 at merge time, the branch's base.
  • fix(ladder): on-demand baseline dollars are bought 1:1 as commitment dollars (over-buys by 1/(1-discount)) once the executor ships #349 is closed by this merge.
  • Platform note: the next cloud-commitments-go pin bump brings this in. That bump must state that Compute SP layers are now sized at the account-wide blend, and the platform must require a cap for Compute SP ladder configs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/m Days impact/many Affects most users priority/p1 Next up; this sprint severity/high Significant harm triaged Item has been triaged type/bug Defect urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(ladder): on-demand baseline dollars are bought 1:1 as commitment dollars (over-buys by 1/(1-discount)) once the executor ships

1 participant