Skip to content

feat(aws): AWSLadder capability read side - #1347

Merged
cristim merged 1 commit into
feat/ladder-tranchesfrom
feat/ladder-aws-read
Jul 3, 2026
Merged

feat(aws): AWSLadder capability read side#1347
cristim merged 1 commit into
feat/ladder-tranchesfrom
feat/ladder-aws-read

Conversation

@cristim

@cristim cristim commented Jul 3, 2026

Copy link
Copy Markdown
Member

Summary

  • New package providers/aws/ladder: AWSLadder implements the READ side of pkg/ladder.LadderCapability for AWS.
  • ListCommitments merges active convertible RIs and Savings Plans (EC2Instance + Compute; other plan types filtered) into common.Commitment values with reservation-total hourly costing: (RecurringHourlyAmount + UsagePrice + FixedPrice/(Duration/3600)) * InstanceCount, matching the canonical monthlyCostFromConvertibleRI per-instance semantics.
  • GetLayerStates returns per-layer snapshots for the three AWS ladder layers (EC2 Instance SP = base, Compute SP = flex, Convertible RI = buffer), honoring the explicit-zeros contract: ExistingUSDPerHour/ExpiringUSDPerHour are explicit zero pointers on empty layers; CoveragePct/UtilizationPct are nil when genuinely unmeasured. Degraded data sources log WARNINGs instead of failing the snapshot.
  • GetUsageBaseline computes a nearest-rank percentile low-water mark from a daily on-demand series, with strict boundary validation (>= 7 days, every element finite and non-negative, errors name the offending index).
  • Write side (PurchaseLayer, ReshapeBuffer) returns an explicit "not yet wired" error (deliberately NOT ErrCommitmentPurchaseNotSupported); implementation follows in PR 6.
  • All data sources are injected via narrow interfaces, so the test suite is fully hermetic.

Stacked on #1341 (ladder engine stack); retargets as parents merge. Part of #1335 (phase 2 of #1333).

Documented seams

  • The SP coverage/utilization source interfaces (spCoverageSource, spUtilizationSource) need a thin adapter to feat(aws): CE savings plans coverage and utilization queries #1346's recommendations.SPCoverageSummary/SPUtilizationSummary types at wiring time (Go interface satisfaction requires identical return types; nil pct must be preserved as no-data). Until wired they are nil and SP layer coverage/utilization stay nil (unmeasured).
  • StableUSDPerHour is deliberately nil: the pkg/ladder contract defines Stable as the post-buffer-fraction estimate, and no stable-usage estimator exists for AWS yet. nil triggers the engine's documented "stable usage unknown; routing all core gap to flex" degradation.

Test plan

  • 51 hermetic tests (fakes for all injected interfaces; no AWS calls), covering constructor validation, layer/role cardinality, commitment merging and per-instance RI costing (incl. count>1 regression), explicit-zeros contract, expiry horizon boundary, coverage/utilization aggregation and degradation, percentile math, and baseline series validation (NaN/Inf/negative).
  • All gates exit 0 from providers/aws/: go build ./..., go vet ./ladder/..., gofmt -l ladder/ (empty), go test ./ladder/... -count=1, golangci-lint run --config ../../.golangci.yml ./ladder/... (clean on the new package).

Write side follows in PR 6.

Summary by CodeRabbit

  • New Features

    • Added AWS ladder support for viewing commitment states, including Reserved Instances and Savings Plans.
    • Added layer state snapshots with existing and expiring cost estimates, plus coverage and utilization values.
  • Bug Fixes

    • Improved baseline calculations with stricter input checks and clearer error handling for invalid or incomplete data.
    • Added safer handling for missing coverage/utilization data so snapshots can still be returned when some sources are unavailable.

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.
@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/m Days type/feat New capability labels Jul 3, 2026
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces a new providers/aws/ladder package implementing the AWSLadder provider: interfaces for RI/SP data sources, a constructor with validation, provider capability advertisement, write-side stubs, commitment listing (RI/SP merging), layer state snapshots (coverage/utilization/cost), and percentile-based usage baseline computation, accompanied by extensive tests.

Changes

AWS Ladder provider implementation

Layer / File(s) Summary
Interfaces and data contracts
providers/aws/ladder/interfaces.go
Defines riLister, spLister, coverageSource, utilizationSource, spCoverageSource, spUtilizationSource interfaces and ActiveSP, SPCoverageSummary, SPUtilizationSummary structs.
AWSLadder construction and capability stubs
providers/aws/ladder/ladder.go
Adds Config, AWSLadder, New constructor with validation, Provider(), SupportedLayers(), and stubbed PurchaseLayer/ReshapeBuffer returning a not-wired error.
ListCommitments: RI/SP mapping
providers/aws/ladder/commitments.go
Implements ListCommitments with scope validation, RI and SP listing helpers, plan-type filtering, and mapping to common.Commitment.
GetLayerStates: layer snapshot computation
providers/aws/ladder/layer_states.go
Computes per-layer existing/expiring costs, EC2 coverage percentage, RI/SP utilization, and SP plan-type mapping, degrading to nil on source errors.
GetUsageBaseline: percentile-based baseline
providers/aws/ladder/baseline.go
Validates scope/args, fetches on-demand series, validates series elements, and computes nearest-rank percentile for LowWaterUSDPerHour.
Test suite
providers/aws/ladder/ladder_test.go
Hermetic fakes and tests covering construction, commitments, layer states, percentile math, and baseline validation/computation.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant AWSLadder
  participant riLister
  participant spLister
  participant coverageSource
  participant utilizationSource

  Caller->>AWSLadder: ListCommitments(ctx, scope)
  AWSLadder->>AWSLadder: validateScope(scope)
  AWSLadder->>riLister: listRICommitments
  riLister-->>AWSLadder: ConvertibleRIs
  AWSLadder->>spLister: listSPCommitments
  spLister-->>AWSLadder: ActiveSPs
  AWSLadder-->>Caller: merged []Commitment

  Caller->>AWSLadder: GetLayerStates(ctx, scope)
  AWSLadder->>coverageSource: GetEC2Coverage
  coverageSource-->>AWSLadder: PoolCoverageMap
  AWSLadder->>utilizationSource: GetRIUtilization
  utilizationSource-->>AWSLadder: utilization data
  AWSLadder-->>Caller: map[LayerType]LayerState

  Caller->>AWSLadder: GetUsageBaseline(ctx, scope, lookbackDays, percentile)
  AWSLadder->>coverageSource: GetOnDemandSeries
  coverageSource-->>AWSLadder: daily USD series
  AWSLadder->>AWSLadder: nearestRankPercentile(series)
  AWSLadder-->>Caller: UsageBaseline
Loading

Related Issues: None specified.

Related PRs: None specified.

Suggested labels: aws, ladder, feature

Suggested reviewers: None specified.

Poem

A rabbit hops through AWS lanes,
Counting RIs and Savings Plans' gains,
Percentiles sorted, low-water found,
Layers snapshot, no error unbound,
Hop, hop, the ladder now stands ground. 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding the AWSLadder read-side capability implementation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ladder-aws-read

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

@cristim

cristim commented Jul 3, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 3, 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.

@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 (3)
providers/aws/ladder/interfaces.go (1)

66-69: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider splitting coverageSource by concern.

GetRICoverageMap (RI coverage) and GetOnDemandSeries (baseline usage data) are unrelated capabilities bundled into a single interface. Since GetLayerStates only needs the former and GetUsageBaseline only needs the latter, separate interfaces would keep dependencies narrower and make future hermetic fakes/mocks smaller per consumer.

🤖 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/interfaces.go` around lines 66 - 69, The coverageSource
interface currently mixes two unrelated concerns, RI coverage and on-demand
baseline usage, which makes consumers depend on more than they need. Split it
into two smaller interfaces, one exposing GetRICoverageMap for GetLayerStates
and another exposing GetOnDemandSeries for GetUsageBaseline, then update the
corresponding consumers to depend on the narrower interface they actually use.
Keep the existing method names intact so the concrete AWS implementation can
satisfy the new interfaces without behavior changes.
providers/aws/ladder/layer_states.go (1)

38-72: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

Sequential external calls could be parallelized.

GetLayerStates sequentially calls RI listing, SP listing, GetRICoverageMap, GetRIUtilization, and (inside fetchSPCoveragePct) another CE call — each a network round trip. For a read-path snapshot function, running the independent calls concurrently (e.g., via errgroup) would reduce latency, especially since coverage/utilization failures are already tolerated independently.

🤖 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, GetLayerStates in
AWSLadder is doing several independent network reads one after another, which
adds avoidable latency. Refactor the RI/SP listing, GetRICoverageMap,
GetRIUtilization, and fetchSPCoveragePct calls to run concurrently, ideally with
errgroup, while preserving the existing per-call error handling and degraded nil
coverage/utilization behavior. Keep the validateScope check first, then gather
the results and build the layer states from the collected values.
providers/aws/ladder/commitments.go (1)

114-117: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Repeated "EC2Instance"/"Compute" plan-type string literals.

The literal plan-type strings "EC2Instance" and "Compute" are duplicated across isLadderSPType here and toSPUtilPlanType/spLayerState in layer_states.go. Extracting shared constants would reduce the risk of typos causing silent divergence between the two files.

♻️ Proposed refactor
+const (
+	spPlanTypeEC2Instance = "EC2Instance"
+	spPlanTypeCompute     = "Compute"
+)
+
 func isLadderSPType(planType string) bool {
-	return planType == "EC2Instance" || planType == "Compute"
+	return planType == spPlanTypeEC2Instance || planType == spPlanTypeCompute
 }

Also applies to: 126-128, 196-204

🤖 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/commitments.go` around lines 114 - 117, The plan-type
strings are duplicated across isLadderSPType and the related logic in
toSPUtilPlanType and spLayerState, so extract shared constants for the
EC2Instance and Compute values and reuse them everywhere. Update isLadderSPType
to compare against those shared symbols, and have the corresponding branches in
layer_states.go reference the same constants to keep the mapping consistent and
avoid silent drift.
🤖 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/commitments.go`:
- Around line 114-117: The plan-type strings are duplicated across
isLadderSPType and the related logic in toSPUtilPlanType and spLayerState, so
extract shared constants for the EC2Instance and Compute values and reuse them
everywhere. Update isLadderSPType to compare against those shared symbols, and
have the corresponding branches in layer_states.go reference the same constants
to keep the mapping consistent and avoid silent drift.

In `@providers/aws/ladder/interfaces.go`:
- Around line 66-69: The coverageSource interface currently mixes two unrelated
concerns, RI coverage and on-demand baseline usage, which makes consumers depend
on more than they need. Split it into two smaller interfaces, one exposing
GetRICoverageMap for GetLayerStates and another exposing GetOnDemandSeries for
GetUsageBaseline, then update the corresponding consumers to depend on the
narrower interface they actually use. Keep the existing method names intact so
the concrete AWS implementation can satisfy the new interfaces without behavior
changes.

In `@providers/aws/ladder/layer_states.go`:
- Around line 38-72: GetLayerStates in AWSLadder is doing several independent
network reads one after another, which adds avoidable latency. Refactor the
RI/SP listing, GetRICoverageMap, GetRIUtilization, and fetchSPCoveragePct calls
to run concurrently, ideally with errgroup, while preserving the existing
per-call error handling and degraded nil coverage/utilization behavior. Keep the
validateScope check first, then gather the results and build the layer states
from the collected values.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 75887bc9-7049-4d36-8fde-7c772a99e97b

📥 Commits

Reviewing files that changed from the base of the PR and between 328743e and 018da7a.

📒 Files selected for processing (6)
  • 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

@cristim

cristim commented Jul 3, 2026

Copy link
Copy Markdown
Member Author

CodeRabbit nitpick triage: (1) plan-type string literals - agreed, being fixed in the stacked follow-up PR (feat/ladder-aws-write) which touches the same dispatch paths; typed constants referencing the savingsplans package vocabulary. (2) coverageSource interface split - agreed, also landing in the stacked PR alongside its new write-side interfaces to avoid churning this diff. (3) parallelizing GetLayerStates - deferred: this snapshot runs once per day per account on the scheduled ladder task, so the added errgroup complexity buys nothing user-visible; will revisit if the call moves to an interactive path.

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/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.

1 participant