Skip to content

refactor(aws): page Cost Explorer through one capped helper and cap the unbounded loops - #462

Merged
cristim merged 1 commit into
mainfrom
refactor/35-ce-pagination-helper
Oct 16, 2026
Merged

cristim merged 1 commit into
mainfrom
refactor/35-ce-pagination-helper

Conversation

@cristim

@cristim cristim commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

Summary

This is PR 1 of 2 for #35. The AWS provider has 9 Cost Explorer pagination loops in providers/aws/recommendations, all hand-rolled. Three of them had no page cap:

  • coverage.go fetchCoveragePaged;
  • utilization.go;
  • usage_history.go, which also had no context check.

A Cost Explorer response that kept returning a next token made them loop forever.

New file: ce_paginate.go

paginateCE(ctx, maxPages, what, page) runs one capped loop. It checks ctx.Err() first and returns it unwrapped, because an existing test requires Equal(context.Canceled). Then it checks the cap, then fetches. A nil or empty token ends the loop. The cap error is <what>: pagination cap reached after N pages, and partial results are never returned as complete. The callback keeps each site's own accumulation and error wrapping.

Per-site caps

These are runaway guards, not size limits. No Cost Explorer input sets MaxResults, so Cost Explorer chooses the page size.

  • coverage and utilization: new constants maxCoveragePages and maxUtilizationPages, both 100. The guard is generous because one over-cap pool aborts the whole GetRICoverageMap survey (coverage.go:190-200).
  • usage history: maxRecommendationPages (20). It uses a 7-day lookback, one SKU and no GroupBy, so it is a single page in practice. It now checks ctx every page. fetchDailyCoverage keeps its "failed to get daily coverage for ..." prefix.

Behaviour change: these three loops now fail loud past 100, 100 and 20 pages respectively. Before, they were unbounded.

Out of scope here

Refs #35

Verification (macOS, Go 1.26.9)

Commands

  • (cd providers/aws && GOWORK=off go test -race -short ./...) passes, and existing tests are unchanged.
  • make build and make vet pass.
  • golangci-lint v2.10.1 (the CI pin) reports 0 issues.
  • gofmt is clean.

Tests

  • 7 helper tests: token loop, empty token ends, exactly maxPages calls before the cap error, boundary success, canceled before the first call and mid-loop (raw context.Canceled), and callback error propagation.
  • One endless-token regression per site: TestFetchCoveragePaged_CapsEndlessTokens, TestGetRIUtilization_CapsEndlessTokens and TestGetDailyUsagePcts_CapsEndlessTokens. Each asserts the literal cap and the call count. The mock fails with "runaway pagination" after 500 calls, so a regression fails fast instead of hanging CI.
  • TestGetDailyUsagePcts_PreCanceledContext.

Fail-before: on the original loops, all four site tests time out at 15s.

Mutation probes by the independent reviewer, in scratch copies. Each made a test fail within 2s:

  • dropping the ctx check;
  • > instead of >= at the cap;
  • treating an empty token as "continue";
  • raising any cap to 400.

Review: two plan passes and two diff passes by the same non-author reviewer. Pass 1 led to the per-site caps of 100, the bounded mock with literal-cap assertions, and the error wording. No actionable findings remain on fbdc9a0.

Fakes only; no AWS call.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • AWS cost and reservation reports now stop pagination at a defined page limit, preventing requests from continuing indefinitely.
    • If the page limit is reached before results are complete, the request returns an error instead of partial results.
    • Canceled requests are detected during pagination, and service errors continue to be reported.

@cristim cristim added impact/internal Team-internal only type/chore Maintenance / non-user-visible priority/p3 Polish / idea / may never ship severity/low Minor harm urgency/this-quarter Within the quarter effort/s Hours triaged Item has been triaged labels Oct 16, 2026
@coderabbitai

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Warning

Review limit reached

  • Run on-demand review

This review includes 5 billable files and costs up to $1.25.

  • Ask an admin to make reviews automatic

Open in CodeRabbit

Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing.

Or wait 50 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available. Your 68 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: LeanerCloud/cloud-commitments-go/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: 7cec7d6e-777a-41e8-b1be-a20c8e01a3f2
📥 Commits

Reviewing files that changed from the base of the PR and between 51855eb and 8906050.

📒 Files selected for processing: 5
  • providers/aws/recommendations/ce_paginate.go
  • providers/aws/recommendations/ce_paginate_test.go
  • providers/aws/recommendations/coverage.go
  • providers/aws/recommendations/usage_history.go
  • providers/aws/recommendations/utilization.go

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: LeanerCloud/cloud-commitments-go/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: 480e8cf4-6861-40f9-a398-cf5e796dd752
📥 Commits

Reviewing files that changed from the base of the PR and between c2b1511 and 51855eb.

📒 Files selected for processing: 5
  • providers/aws/recommendations/ce_paginate.go
  • providers/aws/recommendations/ce_paginate_test.go
  • providers/aws/recommendations/coverage.go
  • providers/aws/recommendations/usage_history.go
  • providers/aws/recommendations/utilization.go

Included review availability: This review used your included allowance. 0 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

📝 Walkthrough

Cost Explorer pagination now uses a shared helper with page limits and context checks. Coverage, utilization, and daily-usage retrieval use the helper. Tests cover token handling, page limits, cancellation, and fetch errors.

Changes: Cost Explorer pagination

Layer / File(s) Summary
Shared paginator and pagination rules
providers/aws/recommendations/ce_paginate.go, providers/aws/recommendations/ce_paginate_test.go
Adds paginateCE with context checks, token-based termination, fetch-error propagation, and a page cap. Tests cover these behaviors and the cap boundary.
Coverage and utilization integrations
providers/aws/recommendations/coverage.go, providers/aws/recommendations/utilization.go, providers/aws/recommendations/usage_history.go, providers/aws/recommendations/ce_paginate_test.go
Coverage, utilization, and daily-usage retrieval use paginateCE. Tests check configured page limits, no partial coverage results after a cap error, and canceled daily-usage requests.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant Coverage as fetchCoveragePaged
  participant Paginator as paginateCE
  participant CostExplorer as Cost Explorer API
  Coverage->>Paginator: provide page-fetch callback and page cap
  Paginator->>CostExplorer: fetch page with current token
  CostExplorer-->>Paginator: return page and next token
  Paginator-->>Coverage: return pagination result or error
Loading
Merge Risk: ⚪ Minimal · up to 51855

The bounded pagination change appears mergeable after normal checks; no actionable issue remains in the supplied review evidence.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 12 functions across 5 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed 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 clearly summarizes the main change: adding a shared capped pagination helper for Cost Explorer and limiting previously unbounded loops.
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: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@cristim

cristim commented Oct 16, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review rate limited.

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 commented Oct 16, 2026

Copy link
Copy Markdown
Member Author

Independent review verdict: CLEAN at d074459

Reviewer: fresh Claude agent (Sonnet, high effort), not the author, cold brief. The PR body was treated as a claim. Coordinator: claude-cc-reviewer.

Blocking findings: none.

Each call site keeps its behaviour:

Site Token threading, order Cap ctx
coverage.go:317-335 Same input with NextPageToken set each page; results recorded only after full success 100, errors Now the raw ctx error (was wrapped); errors.Is still works
utilization.go:68-83 Same 100, errors Same as coverage
usage_history.go:90-104 Same, with the existing wrap kept maxRecommendationPages = 20, errors Newly checked per page

Helper and cap:

What was attacked and how (providers/aws, GOWORK=off, no AWS calls):

  • go vet ./... is clean. go test -race -count=1 ./... passes 2213 in 17 packages.
  • The existing recommendations tests pass at main and at this head.
  • Mutation probes, both killed:
    • Not threading the token: TestPaginateCE_FollowsTokens fails. Only the helper test guards threading; the per-site tests do not.
    • Cap returns nil instead of an error: the helper cap test and all three per-site CapsEndlessTokens tests fail.

Gate: p3 with its own labels, no closing issue. Merge once CI is green, in its turn in the go merge order.

@cristim

cristim commented Oct 16, 2026

Copy link
Copy Markdown
Member Author

CI is green on head d0744592: pre-commit 38951519537 and Build & Test 38951519858.

@cristim
cristim force-pushed the refactor/35-ce-pagination-helper branch from d074459 to 51855eb Compare October 16, 2026 20:44
@cristim

cristim commented Oct 16, 2026

Copy link
Copy Markdown
Member Author

Pure rebase onto main after go#459 merged. git range-diff of d074459 against new head 51855eb shows identical commits (=), so the CLEAN verdict carries. (claude-cc-reviewer)

…he unbounded loops

Add paginateCE, a shared NextPageToken loop that checks ctx.Err() (returned
raw) on each iteration, enforces a page cap, and ends on a nil or empty
token. Hitting the cap is an error, never partial results reported as
complete.

Migrate the three loops that had no cap: fetchCoveragePaged (100 pages),
GetRIUtilization (100 pages) and fetchDailyCoverage (maxRecommendationPages,
20, and now with a per-page ctx check). The 100-page caps are runaway
guards, not size limits: no Cost Explorer input sets MaxResults, so CE picks
the page size. The already-capped sites move over separately.

Refs #35

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cristim
cristim force-pushed the refactor/35-ce-pagination-helper branch from 51855eb to 8906050 Compare October 16, 2026 20:54
@cristim

cristim commented Oct 16, 2026

Copy link
Copy Markdown
Member Author

Pure rebase onto main after go#465 merged. git range-diff of d074459 against new head 8906050 shows the commit identical (=), so the CLEAN verdict carries. (claude-cc-reviewer)

@cristim
cristim merged commit 4453ddd into main Oct 16, 2026
15 checks passed
@cristim
cristim deleted the refactor/35-ce-pagination-helper branch October 16, 2026 21:07
IAmJSD added a commit to IAmJSD/cloud-commitments-go that referenced this pull request Oct 16, 2026
fetchGroupedDayCoverage was the only Cost Explorer token loop in the
package left uncapped after LeanerCloud#462/LeanerCloud#463. Route it through paginateCE with
maxCoveragePages so endless fresh tokens or a token cycle stop at the cap
with an error; keep the repeated-token check as a fast failure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cristim added a commit that referenced this pull request Oct 17, 2026
* fix(aws): batch daily coverage sparkline fetch per region

AttachDailyUsageHistory issued one GetReservationCoverage request per
distinct (service, region, instance type), so CE requests scaled with SKU
count. Group recommendations by (service, region). Buckets with more
distinct types than lookback days now issue one region-wide request per
day grouped by INSTANCE_TYPE (CE rejects DAILY granularity combined with
GroupBy, so a single grouped request cannot return a daily series) and
fan each type's series out to its recommendations. Smaller buckets keep
the existing per-type DAILY request, so a bucket never costs more than
min(types, lookback days) requests plus continuation pages.

Per-type series, missing-day zeros and nil-for-no-data semantics match
the per-type path; a failed grouped fetch leaves the bucket's usage
history nil instead of reporting zero coverage.

Closes #85

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

* fix(aws): stop grouped coverage paging on a repeated page token

If Cost Explorer returned the same NextPageToken twice, the grouped
per-day loop re-requested the same page until ctx was canceled. Return
an error instead (CodeRabbit finding on #464).

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

* fix(aws): cap grouped daily coverage paging through paginateCE

fetchGroupedDayCoverage was the only Cost Explorer token loop in the
package left uncapped after #462/#463. Route it through paginateCE with
maxCoveragePages so endless fresh tokens or a token cycle stop at the cap
with an error; keep the repeated-token check as a fast failure.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Cristian Magherusan-Stanciu <cristim@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/s Hours impact/internal Team-internal only priority/p3 Polish / idea / may never ship severity/low Minor harm triaged Item has been triaged type/chore Maintenance / non-user-visible urgency/this-quarter Within the quarter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant