Skip to content

feat(ladder): integrate stranded phase-2 AWS capability onto main - #1361

Merged
cristim merged 8 commits into
mainfrom
feat/ladder-aws-integrate
Jul 12, 2026
Merged

feat(ladder): integrate stranded phase-2 AWS capability onto main#1361
cristim merged 8 commits into
mainfrom
feat/ladder-aws-integrate

Conversation

@cristim

@cristim cristim commented Jul 12, 2026

Copy link
Copy Markdown
Member

What

Re-integrates the Phase-2 AWS commitment-laddering capability onto main. This code was written and CodeRabbit-reviewed in PRs #1341, #1347 and #1349, but those PRs were stacked (each based on the previous feature branch, not on main) and merged bottom-to-top within the stack. The top-of-stack PR into main was never opened, so the content shows as "merged" on GitHub yet never landed on main. providers/aws/ladder/ and pkg/ladder/ladder.go are absent from main today, and LadderCapability has no implementation.

This PR cherry-picks the seven original commits (already reviewed) onto a fresh branch off current origin/main, restoring:

Why now

Phase-3 PR-2 (the ladder_run scheduled task, issue #1336) is designed to sit on this AWS capability. Without it on main, the handler half will not compile. Integrating the stranded stack first restores the intended foundation.

Also fixed (pre-existing main debt)

The last commit repairs a stale test mock: mockCostExplorerClient never implemented GetSavingsPlansCoverage/GetSavingsPlansUtilization after those methods were added to recommendations.CostExplorerAPI (637c570). The gap left the root providers/aws test package failing to build (service_client_test.go:41), which only surfaces under go test/go vet, not go build. The read side exercises those CE queries, so the mock is fixed here rather than left un-testable.

Verification (all exit 0)

  • pkg: go build ./..., go vet ./... clean; go test ./ladder/... 246 pass.
  • providers/aws: go build ./..., go vet ./... clean; go test ./... all ok (root aws pkg 105, ladder 113).
  • root module: go build ./... clean; go mod tidy no-op.

Scope

No conflicts on cherry-pick: allocate.go/capability.go/plan.go are identical between the stranded branch and main; the shared store.go/types.go diffs are the tranche additions; the twelve other files are net-new (main lacks them). No execution, email, approval, or scheduling paths are touched; everything stays behind the existing default-off flags.

Closes #1335. Part of #1333.

Summary by CodeRabbit

  • New Features

    • Added AWS cost-commitment ladder support for Convertible Reserved Instances and EC2/Compute Savings Plans.
    • Added commitment inventory, usage baselines, coverage, utilization, and layer-state reporting.
    • Added purchase workflows with validation and idempotency protection.
    • Added automated buffer reshaping with summaries for completed, pending, skipped, and failed actions.
    • Added deterministic ramp scheduling for immediate and future purchases with exact allocation totals.
  • Bug Fixes

    • Improved validation and error handling for invalid metrics, recommendations, schedules, identifiers, and AWS scopes.
    • Prevented duplicate scheduled actions and incomplete commitment records.

cristim added 8 commits July 12, 2026 23:57
…dule

Split each Allocation into a buy-now purchase for the immediate ramp step
and future Tranche rows per delayed step, so commitment terms expire
staggered. Step amounts use exact big.Rat arithmetic with a last-step
remainder and an overshoot clamp, so totals always reconstruct the gap
exactly. Tranche rows are fully self-describing (Layer, Term,
PaymentOption, amount) and clock plus ID generation are injected. Part of #1334.
…ring

Reject empty or repeated IDs from the injected NewID across all tranches
of a run, since SaveTranches upserts by ID and a collision would silently
drop a scheduled purchase. Render step fractions with %.4g so small
percentages no longer show as 0% on positive purchases, require a
non-empty rationale on every input allocation, and align the tranche
amount validation docs with what big.Rat SetString actually enforces.
Switch TrancheInput and Tranche Term/PaymentOption fields from raw
strings to the ladder.Term and ladder.PaymentOption enums introduced on
feat/ladder-types, validate them via the enum Validate methods instead
of non-empty checks, and move all test fixtures to the typed constants.
Unknown enum values are now rejected at the tranche-building boundary in
addition to empty ones.
AWSLadder implements the pkg/ladder.LadderCapability read methods:
commitment listing merging convertible RIs and savings plans with
reservation-total costing (InstanceCount, UsagePrice, amortized upfront);
layer states honoring the explicit-zeros contract; nearest-rank percentile
usage baseline with strict series validation. Write side stubbed for PR 6.

Part of #1335.
PurchaseLayer dispatches to the savingsplans/ec2 purchase clients with
mandatory idempotency tokens and per-target boundary validation.
ReshapeBuffer is a thin RunAutoExchange delegation with fail-loud dry-run
rejection pending #1348 and a documented store-wide pending-cancellation
warning. Also: SDK-derived plan-type constants and coverage interface split.

Part of #1335.
Require non-empty Platform/Tenancy/Scope on EC2 RI purchase recs (the ec2
client silently defaults empty Tenancy to "default" and Scope to "Regional",
which could buy a default-tenancy RI from a dedicated-tenancy rec). Correct
the ReshapeBuffer godoc: daily-cap stops surface as Failed plus an error,
not Skipped (upstream reclassification tracked in #1348). Add tests for the
new rejections, +Inf HourlyCommitment, and the inverse plan-type mismatch.
Add direct coverage of the write-side-not-wired guard (errors.Is on both
write methods of a New()-built ladder). Fix the stale read-dependency count
in the AWSLadder doc comment (four -> five after the coverage interface
split) and the stale "coverage source" reference in the short-series error.
Denominate the partial-failure message by actual attempts (completed +
failed) instead of Analyzed, which includes never-attempted items.
mockCostExplorerClient did not implement GetSavingsPlansCoverage or
GetSavingsPlansUtilization, which were added to recommendations.CostExplorerAPI
when the CE savings-plans queries landed (637c570). The gap left the root
providers/aws test package failing to build (service_client_test.go:41), which
only surfaces on `go test`/`go vet`, not `go build`. Bringing in the AWSLadder
read side, which exercises those CE queries, makes the stale mock worth fixing
now rather than leaving the package un-testable.

Add the two missing methods as empty-output stubs, matching the existing
mock methods, so mockCostExplorerClient satisfies the full interface again.
@cristim cristim added triaged Item has been triaged priority/p1 Next up; this sprint severity/medium Moderate harm urgency/this-quarter Within the quarter impact/many Affects most users effort/l Weeks type/feat New capability labels Jul 12, 2026
@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

No new commits to review since the last review.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cb91b362-1a23-4ccf-be55-eb3a257a1efa

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds deterministic provider-neutral tranche construction with exact ramp allocation and self-describing tranche validation, plus an AWS ladder implementation covering commitments, layer states, usage baselines, purchases, and RI reshaping.

Changes

Core tranche construction

Layer / File(s) Summary
Tranche contracts and validation
pkg/ladder/ladder.go, pkg/ladder/store.go, pkg/ladder/store_test.go
Adds tranche input/result types and execution fields for amount, layer, term, and payment option, with rational amount validation.
Ramp splitting and result construction
pkg/ladder/ladder.go
Builds immediate actions and delayed tranches from ramp allocations using exact clamped amounts, generated IDs, metadata, and duplicate-ID checks.
Tranche behavior coverage
pkg/ladder/ladder_test.go
Tests routing, exact totals, deterministic output, metadata, rationale, source propagation, validation errors, precision handling, and ID uniqueness.

AWS ladder capability

Layer / File(s) Summary
AWS ladder contracts and wiring
providers/aws/ladder/interfaces.go, providers/aws/ladder/ladder.go
Defines AWS ladder dependencies, Savings Plans models, configuration defaults, supported layers, constructor validation, and write-side wiring.
AWS commitments, states, and baseline
providers/aws/ladder/baseline.go, providers/aws/ladder/commitments.go, providers/aws/ladder/layer_states.go
Adds commitment listing, RI and Savings Plans layer-state aggregation, coverage/utilization handling, and nearest-rank usage baselines.
AWS purchase and reshape operations
providers/aws/ladder/purchase.go, providers/aws/ladder/reshape.go
Adds validated RI/Savings Plans purchase dispatch and automatic RI exchange configuration and result summarization.
AWS capability behavior coverage
providers/aws/ladder/*_test.go, providers/aws/service_client_test.go
Adds hermetic tests and Cost Explorer mocks for constructor, read-side, write-side, state, baseline, purchase, and reshape behavior.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant AWSLadder
  participant AWSReadSources
  participant CostExplorer
  Caller->>AWSLadder: request commitments, states, or baseline
  AWSLadder->>AWSReadSources: fetch RIs, Savings Plans, or usage series
  AWSLadder->>CostExplorer: fetch coverage and utilization
  CostExplorer-->>AWSLadder: metric summaries
  AWSLadder-->>Caller: ladder data
Loading
sequenceDiagram
  participant Caller
  participant BuildTranches
  participant PlannedAction
  participant Tranche
  Caller->>BuildTranches: provide allocations and ramp schedule
  BuildTranches->>PlannedAction: create immediate purchase actions
  BuildTranches->>Tranche: create delayed scheduled rows
  Tranche-->>Caller: return validated tranche result
Loading

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.12% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately describes the AWS phase-2 ladder capability being reintegrated.
Linked Issues check ✅ Passed The changes implement the AWS LadderCapability read/write side and supporting CE Savings Plans behavior described in #1335.
Out of Scope Changes check ✅ Passed All listed changes support tranche building or AWS ladder capability and test coverage; no unrelated code paths stand out.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ladder-aws-integrate

Comment @coderabbitai help to get the list of available commands.

@cristim

cristim commented Jul 12, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim
cristim merged commit cc497f3 into main Jul 12, 2026
14 of 16 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
providers/aws/ladder/layer_states.go (1)

38-72: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider parallelizing the independent read calls.

GetLayerStates issues up to seven sequential blocking calls (RI list, SP list, RI coverage, RI utilization, SP coverage, and SP utilization x2 for the two SP layers), none of which depend on each other's results. For a scheduled task that may run per-account/per-region, this adds up to several seconds of avoidable latency per invocation.

Consider fanning these out with errgroup (each goroutine populating its own result/err, joined before building states), while preserving the existing degrade-to-nil-on-error semantics per source. Note: verify AWS Cost Explorer per-account rate limits accommodate concurrent calls, since CE has historically had low default TPS quotas.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@providers/aws/ladder/layer_states.go` around lines 38 - 72, The independent
AWS reads in GetLayerStates currently run sequentially; fan them out with an
errgroup and join all work before constructing states. Run RI listing, SP
listing, coverage, utilization, and the single shared SP coverage fetch
concurrently, while preserving each source’s existing error handling and
degrade-to-nil behavior. Keep SP layer state construction dependent on the
shared result and verify the concurrency is compatible with Cost Explorer
account rate limits.
providers/aws/ladder/reshape.go (1)

79-108: 🩺 Stability & Availability | 🔵 Trivial

Cross-task coordination hazard is documented but not enforced.

The WARNING above (lines 31-37) correctly calls out that the underlying RunAutoExchange cancels ALL pending exchange records store-wide, including ones from the unrelated standalone ri_exchange_reshape scheduled task. Since Phase 3 scheduling isn't part of this PR, there's currently no code-level guard against a Phase 3 ladder_run invocation racing with that standalone task against the same store.

Worth confirming before ReshapeBuffer is wired into production scheduling: what mechanism (distributed lock, mutex flag, disabling the standalone scheduler, etc.) will enforce mutual exclusion between the two callers of the same exchange store.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@providers/aws/ladder/reshape.go` around lines 79 - 108, Add an explicit
mutual-exclusion guard around AWSLadder.ReshapeBuffer and the standalone
ri_exchange_reshape caller so only one can operate on the shared exchange store
at a time. Use the repository’s established distributed-lock, mutex, or
scheduler-disable mechanism, and ensure the guard is acquired before
RunAutoExchange and released on every exit path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@providers/aws/ladder/layer_states.go`:
- Around line 38-72: The independent AWS reads in GetLayerStates currently run
sequentially; fan them out with an errgroup and join all work before
constructing states. Run RI listing, SP listing, coverage, utilization, and the
single shared SP coverage fetch concurrently, while preserving each source’s
existing error handling and degrade-to-nil behavior. Keep SP layer state
construction dependent on the shared result and verify the concurrency is
compatible with Cost Explorer account rate limits.

In `@providers/aws/ladder/reshape.go`:
- Around line 79-108: Add an explicit mutual-exclusion guard around
AWSLadder.ReshapeBuffer and the standalone ri_exchange_reshape caller so only
one can operate on the shared exchange store at a time. Use the repository’s
established distributed-lock, mutex, or scheduler-disable mechanism, and ensure
the guard is acquired before RunAutoExchange and released on every exit path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ffbd449d-4137-474c-aed0-df6902e18f27

📥 Commits

Reviewing files that changed from the base of the PR and between c3e93c3 and ed9f25b.

📒 Files selected for processing (15)
  • pkg/ladder/ladder.go
  • pkg/ladder/ladder_test.go
  • pkg/ladder/store.go
  • pkg/ladder/store_test.go
  • providers/aws/ladder/baseline.go
  • providers/aws/ladder/commitments.go
  • providers/aws/ladder/interfaces.go
  • providers/aws/ladder/ladder.go
  • providers/aws/ladder/ladder_test.go
  • providers/aws/ladder/layer_states.go
  • providers/aws/ladder/purchase.go
  • providers/aws/ladder/purchase_test.go
  • providers/aws/ladder/reshape.go
  • providers/aws/ladder/reshape_test.go
  • providers/aws/service_client_test.go

@cristim
cristim deleted the feat/ladder-aws-integrate branch July 12, 2026 21:23
cristim added a commit that referenced this pull request Jul 21, 2026
The prior commit's code comments cited "#1461" as the tracking issue
for the RI utilization filter bug. #1461 is an unrelated PR (NaN/Inf
rejection in SP money parsing). The bug this fix addresses was
introduced by PR #1361, per the review that surfaced it; correct all
comment references accordingly.
cristim added a commit that referenced this pull request Jul 22, 2026
… decisions

GetRIUtilization built GetReservationUtilization with no Filter at all,
so it blended utilization across every reserved-resource type in the
account (RDS, ElastiCache, OpenSearch, Redshift, standard EC2 RIs) and
every region into one SUBSCRIPTION_ID-grouped number. The ladder
ConvertibleRI (buffer) layer reads this as if it were EC2-convertible
utilization for one account/region, then uses it to decide whether to
trigger a real RI reshape/exchange -- an underutilized RI in an
unrelated service or region could trigger a reshape, or mask a
genuinely poorly-utilized convertible RI.

Add a SERVICE+REGION Filter to the CE call, mirroring the same pattern
already used by GetRICoverageMap in coverage.go. Also intersect the
(now EC2+region-scoped) response against the account's own convertible
RI IDs before aggregating in GetLayerStates, since a standard
(non-convertible) EC2 RI in the same account/region would otherwise
still pass the SERVICE+REGION filter and blend into the layer's
UtilizationPct.

Threads a region parameter through GetRIUtilization's callers
(providers/aws/ladder, providers/aws/service_client.go,
internal/server, internal/api). Follow-up to #1361.
cristim added a commit that referenced this pull request Jul 22, 2026
The prior commit's code comments cited "#1461" as the tracking issue
for the RI utilization filter bug. #1461 is an unrelated PR (NaN/Inf
rejection in SP money parsing). The bug this fix addresses was
introduced by PR #1361, per the review that surfaced it; correct all
comment references accordingly.
cristim added a commit that referenced this pull request Jul 22, 2026
… decisions (follow-up to #1361) (#1479)

* fix(aws/ladder): scope RI utilization query to EC2+region for reshape decisions

GetRIUtilization built GetReservationUtilization with no Filter at all,
so it blended utilization across every reserved-resource type in the
account (RDS, ElastiCache, OpenSearch, Redshift, standard EC2 RIs) and
every region into one SUBSCRIPTION_ID-grouped number. The ladder
ConvertibleRI (buffer) layer reads this as if it were EC2-convertible
utilization for one account/region, then uses it to decide whether to
trigger a real RI reshape/exchange -- an underutilized RI in an
unrelated service or region could trigger a reshape, or mask a
genuinely poorly-utilized convertible RI.

Add a SERVICE+REGION Filter to the CE call, mirroring the same pattern
already used by GetRICoverageMap in coverage.go. Also intersect the
(now EC2+region-scoped) response against the account's own convertible
RI IDs before aggregating in GetLayerStates, since a standard
(non-convertible) EC2 RI in the same account/region would otherwise
still pass the SERVICE+REGION filter and blend into the layer's
UtilizationPct.

Threads a region parameter through GetRIUtilization's callers
(providers/aws/ladder, providers/aws/service_client.go,
internal/server, internal/api). Follow-up to #1361.

* fix(aws/ladder): correct comment reference from #1461 to #1361

The prior commit's code comments cited "#1461" as the tracking issue
for the RI utilization filter bug. #1461 is an unrelated PR (NaN/Inf
rejection in SP money parsing). The bug this fix addresses was
introduced by PR #1361, per the review that surfaced it; correct all
comment references accordingly.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/l Weeks impact/many Affects most users priority/p1 Next up; this sprint severity/medium Moderate harm triaged Item has been triaged type/feat New capability urgency/this-quarter Within the quarter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(ladder): phase 2 - AWS LadderCapability (CE SP coverage, read/write sides)

1 participant