Repository navigation
feat(ladder): allocation algorithm with three-layer split and guardrails - #1340
Conversation
📝 WalkthroughWalkthroughAdds a new ChangesLadder Allocation Engine and Config
Estimated code review effort: 4 (Complex) | ~60 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
pkg/ladder/allocate_test.go (1)
456-475: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider extracting a
validConfigAzurehelper.The Azure test builds a full
LadderConfigliteral from scratch instead of reusing a shared helper likevalidConfigAWS. Minor duplication; not blocking.🤖 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 `@pkg/ladder/allocate_test.go` around lines 456 - 475, The Azure allocation test is duplicating a full LadderConfig setup instead of reusing a shared valid config helper. Extract or reuse a `validConfigAzure` helper alongside `validConfigAWS`, and update `TestAllocate_AzureMergedBaseBuffer` to build from that helper so the Azure-specific defaults stay centralized and the test only overrides the fields it needs.
🤖 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.
Inline comments:
In `@pkg/ladder/allocate.go`:
- Around line 240-243: The allocation logic in allocate.go currently floors
negative net commitment to zero after subtracting expiring from existing, which
hides inconsistent provider snapshots. Update the calculation in the
net/existing/expiring path inside the allocation function so that if
ExpiringUSDPerHour exceeds ExistingUSDPerHour it fails loudly instead of
silently clamping, using the existing allocate flow and big.Rat comparison logic
to return an error or otherwise stop processing.
- Around line 176-192: validateLayers only checks flex/base counts, but splitGap
assumes stricter topology invariants. Update validateLayers in allocate.go to
reject duplicate LayerType values, enforce that only the base+buffer role merge
is allowed, and limit buffer roles to at most one; use the existing LayerSpec,
LayerType, RoleBase, RoleFlex, and any buffer role symbols to keep the
validation aligned with findLayerForRole’s first-match behavior.
- Around line 501-505: The Allocation rationale in allocate.go still describes
the pre-cap role gap after scaling, so update the rationale built in the
allocation scaling path to include the final capped amount. Use the existing
symbols Allocation, a.GapUSDPerHour, and the scaling logic in the loop to append
the post-cap GapUSDPerHour (or equivalent final layer amount) alongside the
current capped-by-max_hourly_commit_per_run text so approval/audit output
reflects the actual capped value.
- Around line 198-210: Validate UtilizationPct in validateLayerStates before it
is used by reshape logic. The current LayerState checks in validateLayerStates
only verify ExistingUSDPerHour and ExpiringUSDPerHour, so add a finite-range
check for st.UtilizationPct when it is non-nil, ensuring it is not NaN/Inf and
stays within [0,100]. Use the existing LayerState and validateLayerStates
symbols so the new validation catches bad utilization values before decisions
made later in the allocate flow.
---
Nitpick comments:
In `@pkg/ladder/allocate_test.go`:
- Around line 456-475: The Azure allocation test is duplicating a full
LadderConfig setup instead of reusing a shared valid config helper. Extract or
reuse a `validConfigAzure` helper alongside `validConfigAWS`, and update
`TestAllocate_AzureMergedBaseBuffer` to build from that helper so the
Azure-specific defaults stay centralized and the test only overrides the fields
it needs.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: f633a0a8-fc7b-4cc7-b57a-a206a69ab7f6
📒 Files selected for processing (4)
pkg/ladder/allocate.gopkg/ladder/allocate_test.gopkg/ladder/types.gopkg/ladder/types_test.go
|
@coderabbitai review |
✅ Action performedReview finished.
|
…rdrails Compute the hourly commitment gap from the baseline low-water mark with a utilization-protection clamp (never commit above the observed usage floor), then split it across base/flex/buffer roles, including the Azure topology where one layer carries both base and buffer. Guardrails: proportional scaling to max_hourly_commit_per_run, fail-loud errors on incomplete provider data and on bufferless topologies with a nonzero buffer fraction. All money math uses exact big.Rat arithmetic. Part of #1334.
Address review findings on the allocation algorithm: - Reject duplicate layer types, multiple buffer-role layers, and any multi-role merge other than base+buffer (Azure reservation shape). - Require non-nil UtilizationPct to be finite and within [0, 100]; a NaN previously skipped reshape detection silently. - Expiring exceeding existing commitment is now a fail-loud inconsistent snapshot error instead of a silent floor at zero. - Cap-scaling rationales now state the final post-cap dollar amount.
…yer validation - Detect buffer reshapes before the gap branch so over-committed accounts (target met or baseline unavailable) still get reshape recommendations; merge maintenance actions into the early-exit hold results. - Enum-validate LayerSpec types and roles; reject unknown LayerStates keys that would silently understate existing coverage. - Drop sub-minimum split amounts with an informational hold per layer instead of emitting un-executable sub-cent purchases. - Render coverage/scale percentages with two decimals so 99.5% never displays as 100%; keep the stable-unknown routing note on flex only.
…rebase The rebase onto the hardened base switched the buffer utilization threshold bound check to the NaN-hostile withinExclIncl helper (a plain "<= 0 || > 100" comparison admits NaN); add NaN and +Inf rejection cases to the config validation table. The typed Term/PaymentOption enums need no code changes here: allocation hold/reshape actions never set those fields and the typed zero values satisfy the empty-field invariant.
2d7f6cb to
03fffc4
Compare
|
@coderabbitai review |
✅ Action performedReview finished.
|
Four compatibility fixes required after rebasing onto main which included PRs #1343 (errcheck), #1346 (ladder-sp-coverage), and #1340 (ladder-allocate): - cmd/cleanup-lambda: remove unused `errors` and `pgx` imports left over from conflict resolution (main's rollback pattern doesn't use them) - internal/mocks/stores.go: update ListStoredRecommendations signature to pointer (*RecommendationFilter) matching the interface change in #1343 - internal/scheduler/scheduler.go: pass ¶ms (pointer) to GetRecommendations after PR #1346 changed the function signature - providers/aws/service_client_test.go: add GetSavingsPlansCoverage and GetSavingsPlansUtilization stub methods to mockCostExplorerClient after PR #1346 extended the CostExplorerAPI interface
Four compatibility fixes required after rebasing onto main which included PRs #1343 (errcheck), #1346 (ladder-sp-coverage), and #1340 (ladder-allocate): - cmd/cleanup-lambda: remove unused `errors` and `pgx` imports left over from conflict resolution (main's rollback pattern doesn't use them) - internal/mocks/stores.go: update ListStoredRecommendations signature to pointer (*RecommendationFilter) matching the interface change in #1343 - internal/scheduler/scheduler.go: pass ¶ms (pointer) to GetRecommendations after PR #1346 changed the function signature - providers/aws/service_client_test.go: add GetSavingsPlansCoverage and GetSavingsPlansUtilization stub methods to mockCostExplorerClient after PR #1346 extended the CostExplorerAPI interface
Four compatibility fixes required after rebasing onto main which included PRs #1343 (errcheck), #1346 (ladder-sp-coverage), and #1340 (ladder-allocate): - cmd/cleanup-lambda: remove unused `errors` and `pgx` imports left over from conflict resolution (main's rollback pattern doesn't use them) - internal/mocks/stores.go: update ListStoredRecommendations signature to pointer (*RecommendationFilter) matching the interface change in #1343 - internal/scheduler/scheduler.go: pass ¶ms (pointer) to GetRecommendations after PR #1346 changed the function signature - providers/aws/service_client_test.go: add GetSavingsPlansCoverage and GetSavingsPlansUtilization stub methods to mockCostExplorerClient after PR #1346 extended the CostExplorerAPI interface
Four compatibility fixes required after rebasing onto main which included PRs #1343 (errcheck), #1346 (ladder-sp-coverage), and #1340 (ladder-allocate): - cmd/cleanup-lambda: remove unused `errors` and `pgx` imports left over from conflict resolution (main's rollback pattern doesn't use them) - internal/mocks/stores.go: update ListStoredRecommendations signature to pointer (*RecommendationFilter) matching the interface change in #1343 - internal/scheduler/scheduler.go: pass ¶ms (pointer) to GetRecommendations after PR #1346 changed the function signature - providers/aws/service_client_test.go: add GetSavingsPlansCoverage and GetSavingsPlansUtilization stub methods to mockCostExplorerClient after PR #1346 extended the CostExplorerAPI interface
Summary
PR 2 of the commitment-laddering feature: the pure allocation algorithm in
pkg/ladder/allocate.go. Given pre-fetched provider data,Allocatedecides how much new hourly commitment to place on each layer.Pipeline:
LadderConfig.Validate), layer topology (exactly one flex role, at most one base role), and layer states (every layer needs non-nil Existing and Expiring $/hr; providers must pass explicit zeros - incomplete data is a hard error, never treated as 0).gap = min(target - existing, low_water - existing)- the utilization-protection clamp never commits above the observed usage floor. Gaps at or below the named 1-cent threshold produce a Hold whose rationale embeds the concrete numbers.max_hourly_commit_per_run(scaled sum equals the cap exactly); exceedingmax_actions_per_runis an error, never a silent drop; a bufferless topology with a nonzero buffer fraction is an error (no purchase directive may target a nonexistent layer).BufferUtilizationThresholdPctconfig field (validated (0, 100], default 90) emitActionReshape; unknown utilization emits an informational Hold; empty buffer layers are skipped entirely (no noise on fresh accounts).All money math is exact
*big.Rat; float64 inputs are converted once at the boundary with NaN/Inf/negative rejection. Notime.Now()inside - the clock is injected viaAllocationInput.Now.Stacked on #1338 (
feat/ladder-types); will auto-retarget tomainwhen that merges. Part of #1334 (phase 1 of LeanerCloud/cloud-commitments-go#27).The package remains inert: no callers until PR 8 wires the engine.
Test plan
allocate_test.go(154 total in the package post-rebase), including fails-pre-fix regressions for the empty-buffer-layer noise and the bufferless-topology empty-layer allocation bug.big.Ratexpectations (no float tolerance) for the AWS 3-layer split, Azure merged split, cap scaling (sum == cap exactly), and utilization clamp.go build ./...,go vet ./ladder/,gofmt -l ladder/(empty),go test ./ladder/ -count=1,golangci-lint run ./ladder/....Summary by CodeRabbit
New Features
BufferUtilizationThresholdPctwith a default value to control when reshapes are issued.Bug Fixes
Tests