Skip to content

refactor(ladder): fetch commitment rates per layer so one missing rate holds only its layer - #691

Merged
cristim merged 2 commits into
mainfrom
refactor/ladder-per-layer-rate-fetch
Nov 5, 2026
Merged

cristim merged 2 commits into
mainfrom
refactor/ladder-per-layer-rate-fetch

Conversation

@cristim

@cristim cristim commented Nov 5, 2026

Copy link
Copy Markdown
Member

Summary

GetCommitmentRates (providers/aws/ladder/commitment_rates.go) fetched only the EC2 Instance SP rate. On ErrNoSPCommitmentRate it returned an empty map. That was harmless with one layer, but any layer added later would be dropped whenever the EC2 rate is missing. This is the part that every option for #349 needs, whichever way the open Compute SP decision goes.

  • GetCommitmentRates now fetches through fetchLayerRates(ctx, src, layers, base), one GetSPCommitmentRate call per layer:
    • ErrNoSPCommitmentRate: only that layer is absent (held), and the loop continues;
    • any other error: nil map and the error, wrapped with the layer name;
    • a rate that is not finite or is <= 0: an error.
      Nothing defaults to 1:1 or 0.
  • The layer list has one entry today, EC2 Instance SP with the ladder region, so behaviour is unchanged. Compute SP stays absent until the owner decides (fix(ladder): on-demand baseline dollars are bought 1:1 as commitment dollars (over-buys by 1/(1-discount)) once the executor ships #349). The comment above the list states its account-wide EC2+Lambda+Fargate blend and the bias that blend has against a regional EC2-only gap.

Tests

  • The fake rate source is keyed by plan type and records its calls.
  • The EC2 cases: present, missing, hard error, and NaN/Inf/0/negative rates. The params are pinned: plan type, account, region, term, payment and lookback.
  • TestFetchLayerRates_LayersAreIndependent uses two synthetic layers:
    • first missing, second present: returns only the second;
    • first present, second a hard error: nil map and the error;
    • both missing: returns {}.
  • Fail-before: restoring the old empty-map return fails the independence subtests first_missing_second_present and both_missing.
  • commitment_rates_sdk_test.go is unchanged and passes.
  • go build, go vet, go test ./ladder/ and golangci-lint (0 issues) pass, and so does pre-commit, all rebased on main 8344b3d.

Part of #349

🤖 Generated with Claude Code

…e holds only its layer

GetCommitmentRates returned an empty map as soon as the EC2 Instance SP
rate was missing. That is harmless with one layer, but would drop every
other layer once more are added. The fetch is now a loop over an explicit
layer list (layer, plan type, region): a missing rate holds only that
layer, any other error fails the call naming the layer, and a rate that
is not finite or not positive is rejected instead of stored.

The list still holds only EC2 Instance SP. Compute SP stays absent until
the owner decides; its account-wide blend over EC2, Lambda and Fargate
biases sizing of a regional EC2-only gap.

Part of #349

Co-Authored-By: Claude Sonnet 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 5, 2026
@coderabbitai

coderabbitai Bot commented Nov 5, 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 35 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 07b7b48 and c700726.

Files selected for processing: 2

  • providers/aws/ladder/commitment_rates.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 5, 2026

Copy link
Copy Markdown
Member Author

APPROVED 32ff7fd by claude-qa-ui-4

Cold review. I didn't write this change.

Checked against the stated contract (providers/aws/ladder/commitment_rates.go):

  • Each layer is fetched independently: fetchLayerRates (:53-71) copies base per layer and sets PlanType and Region from the layer.
  • ErrNoSPCommitmentRate leaves out only that layer: it hits continue (:59-61), and the other layers still run.
  • Any other error fails loudly: it returns a nil map, with the layer named (GetCommitmentRates: layer %s: %w, :62-64).
  • Non-finite and ≤0 rates are errors (:65-67): they're never mapped to 1:1 or 0.
  • The layer list is EC2 Instance SP only (:43-45), with the regional account and the same term, payment and lookback as before. So behaviour is unchanged except that a ≤0 or non-finite EC2 rate is now an error instead of being passed through. Compute SP stays absent, with the reason commented (:41-42).
  • Callers: the only implementation is the pkg/ladder/capability.go:12 interface method, and the nil-map-plus-error contract matches it.

Probes in a scratch copy (each must fail a test):

  • restore the old empty-map return on ErrNoSPCommitmentRate: fails TestFetchLayerRates_LayersAreIndependent/first_missing_second_present (the fail-before the body cites);
  • break instead of continue: fails the same test;
  • treat ErrNoSPCommitmentRate as an error: fails the same test;
  • neutralise the unusable-rate guard, with math kept in use so it still builds: fails TestGetCommitmentRates_RejectsUnusableRate for zero, negative, nan and inf;
  • replace an unusable rate with 1: fails TestGetCommitmentRates_RejectsUnusableRate/zero.

Runs (scoped and throttled, in providers/aws, under heavy-review.lock): GOMAXPROCS=4 go test -p 2 -count=1 ./ladder/ passes, and go vet ./ladder/ passes.

@cristim cristim left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE @ 32ff7fd by claude-nibblets

Cold review of 32ff7fd, the shared part of #349 (ladder sizing, money path), done in my own detached worktree. The branch is 0 behind main. The PR body was treated as a claim.

Against what correct means

  • Each layer's rate is fetched independently. fetchLayerRates (providers/aws/ladder/commitment_rates.go) copies base per layer and sets PlanType and Region from the layer. SPCommitmentRateParams (providers/aws/recommendations/sp_commitment_rate.go:24-38) holds only scalars, so the per-layer copy can't leak state between layers.
  • ErrNoSPCommitmentRate drops only that layer. It is continue, so the layer is absent and the others are still fetched.
  • Any other error fails loudly. It returns a nil map and an error naming the layer (GetCommitmentRates: layer <layer>: %w), wrapping the cause, so errors.Is still matches.
  • Unusable rates are errors. NaN, ±Inf and values of zero or below are refused with a nil map, never treated as 1:1 or 0. The source already refuses NaN and r <= 0 (sp_commitment_rate.go, GetSPCommitmentRate), so this is the ladder's own check and doesn't depend on the source.
  • Behaviour is unchanged today. The production list has only LayerEC2InstanceSP with the ladder's region. A missing rate still gives an empty non-nil map, as the old map[...]{} return did, and a valid rate still gives {EC2InstanceSP: rate}. Compute SP stays absent, as the code comment says, pending the owner's decision.

Verification at this SHA

In providers/aws, with GOWORK=off GOMAXPROCS=4, under the shared review lock: go vet ./ladder/ is clean and go test -p 2 -count=1 ./ladder/ passes.

Mutation probes (each reverted): 9 of 9 killed.

Mutation Result
The old empty-map return restored on a missing rate (the claimed fail-before) killed (TestFetchLayerRates_LayersAreIndependent/first_missing_second_present, /both_missing)
The unusable-rate check removed killed (TestGetCommitmentRates_RejectsUnusableRate)
Zero accepted (< 0) killed (RejectsUnusableRate/zero)
Inf accepted killed (RejectsUnusableRate/inf)
A hard error swallowed as absent killed (first_present_second_hard, HardErrorNamesLayer, AbsenceAndErrors)
A partial map returned with a hard error killed (AbsenceAndErrors/hard, first_present_second_hard)
The layer name dropped from the error killed (HardErrorNamesLayer)
Region not taken from the layer killed (LayersAreIndependent, all three subtests that check the region)
Compute SP added to the production layer list killed (AbsenceAndErrors/absent, ForwardsSelection/0). The unchanged-behaviour claim is pinned

CI at this SHA: Lint for every module, pre-commit, Integration Tests, Snyk and Lint Workflows pass. Unit Tests and Security Scanning were still running when I posted this. CodeRabbit is NEUTRAL.

Not blocking

  • No upper bound. A rate is commitment dollars per covered on-demand dollar, and nothing in the ladder or the source refuses a rate above 1. A Savings Plan that costs more than on-demand isn't a usable sizing input. Since this PR already treats the rate as a money-path input with a validity check, (0, 1] would match the range #686's observed rates enforce. A follow-up would do.

Not merging.

@cristim

cristim commented Nov 5, 2026

Copy link
Copy Markdown
Member Author

APPROVED c700726 by claude-qa-ui-4

Merge-only delta from my APPROVE at 32ff7fd.

The earlier verification (5/5 probes killed, ladder tests and vet green) applies unchanged.

@cristim cristim left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE @ c700726 by claude-nibblets

Merge-only delta review of 32ff7fdb...c7007262, the update-branch onto main. I approved 32ff7fdb earlier. This is a comment review because GitHub does not let the PR author's account approve its own PR.

  • The merge brings in only main's #684. c7007262 has two parents: 32ff7fdb (the approved head) and 07b7b48d (#684, "refuse purchase counts that don't fit int32 before any AWS call"), which is the current main tip. #684 is the only commit between them.
  • No conflict resolution. git show --remerge-diff c7007262 is empty, so git merged it automatically with no hand edits.
  • No overlap. main's side of the merge doesn't touch providers/aws/ladder. #684 changes the aws/services purchase paths and offeringprice.
  • The PR's own change is identical. git diff 07b7b48d c7007262 (2 files, +123/-19) is byte-identical to 32ff7fdb's diff against its base, apart from index lines. So the code I reviewed and mutation-probed at 32ff7fdb (9 of 9 probes killed) is what merges.

CI at this SHA: Lint for every module, pre-commit, Lint Workflows and Snyk pass. Unit Tests, Integration Tests and Security Scanning were still running when I posted this. CodeRabbit is NEUTRAL.

Not merging.

@cristim
cristim merged commit 790f369 into main Nov 5, 2026
15 checks passed
@cristim

cristim commented Nov 5, 2026

Copy link
Copy Markdown
Member Author

Merged at head c700726 as 790f369. Gate evidence:

@cristim
cristim deleted the refactor/ladder-per-layer-rate-fetch branch November 5, 2026 18:36
cristim added a commit that referenced this pull request Nov 6, 2026
* feat(ladder): give Compute SP its Cost Explorer commitment rate

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>

* docs(ladder): state what actually bounds the Compute SP blend bias

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>

* test(ladder): pin that the requested term and payment reach Cost Explorer

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>

* docs(ladder): point the per-sweep cap note at the platform issue

"(#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>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant