Repository navigation
Conversation
…n state tolerateIncompleteSweep kept the rows of a partial sweep but recorded the incompleteness only in a WARN log, so the freshness API (and therefore the dashboard) rendered 1-of-10 subscription coverage exactly like full coverage. The helper now returns a sanitized *sweepGap (nil = complete) instead of a bool. It is threaded through the per-account collectors, fanOutPerAccount, the three provider collectors and collectAllProviders into a new recommendations_state.last_collection_partial JSONB column (migration 000128), served as last_collection_partial on GET /api/recommendations/freshness. The payload carries per-provider counts (incomplete accounts, attempted/failed scopes, failed details) and a closed set of reason categories; raw provider errors and subscription/project IDs stay in the log, because the state row is global while accounts are access-scoped. UpsertRecommendations and ReplaceRecommendations reset the column, so the next complete collection clears the marker. Hard failures are unchanged: they still fail the provider and populate last_collection_error. Tests now drive the partial/complete/hard-error cases for AWS, Azure and GCP (ambient) plus AWS/GCP registered accounts, closing audit finding A16-007's per-provider coverage gap. Closes LeanerCloud#87 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Comment |
Independent review verdict: CLEAN at 1d2c9e2, with a merge-order constraintReviewer: 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.
Consumers of the new partial flag. None of them is on a money path:
Stale rows: eviction is unchanged. An incomplete account is kept out of Migration 000128:
OpenAPI: the nullable Note (non-blocking): What was attacked and how:
Gate:
|
…on upsert UpsertRecommendations now also resets recommendations_state.last_collection_partial (migration 000128, LeanerCloud#87), so the upsert failed on the v111 schema this test pinned before re-collecting. Bump the pin to 128, following the test's existing precedent for 000111. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
CI fix for Root cause: the test pins the schema to 000111 before its final Fix: test-only. The pin moves to 128, following the test's existing 000111 precedent, and the comment now says why. Production semantics are unchanged. I grepped every other Evidence (AWS_CONFIG_FILE and AWS_SHARED_CREDENTIALS_FILE set to /dev/null; no cloud calls):
🤖 Generated with Claude Code |
|
Head moved to 474842d. The reviewed 1d2c9e2 is unchanged ( |
Root cause
tolerateIncompleteSweep(internal/scheduler/scheduler.go) kept the rows of a partial multi-subscription sweep and withheld stale-row eviction, but it recorded the incompleteness only withlogging.Warnfand returned a barecomplete bool. Nothing reachedrecommendations_state, soGET /api/recommendations/freshness(which the dashboard reads) showed a sweep that covered 1 of 10 subscriptions exactly like full coverage. The code comment at the oldscheduler.go:1007-1012described this as follow-up work.Fix
tolerateIncompleteSweepnow returns(*sweepGap, error). A nil gap means the sweep was complete. A non-nil gap is a sanitized summary built fromPartialSubscriptionFailureError(Azure/GCP) orIncompleteRecommendationsError(AWS). Eviction semantics are unchanged:gap != nilkeeps the account out of the eviction roster, exactly as!completedid.fanOutPerAccount(newaccountOutcome.Gap, merged across incomplete accounts), the three provider collectors andcollectAllProviders. That produces a*config.RecommendationsCollectionPartialwith one entry per provider that had an incomplete sweep.persistCollectionrecords it with the newSetRecommendationsCollectionPartial, afterUpsertRecommendations, the same waylast_collection_erroris recorded.UpsertRecommendationsandReplaceRecommendationsnow also setlast_collection_partial = NULL, and the scheduler only writes it again when the collection was partial.last_collection_error, and is never reported as partial. The new tests check this.New file
internal/scheduler/partial_sweep.goholds the gap type and the cause classifier.API shape
GET /api/recommendations/freshness(and the freshness returned by the refresh handler) gainslast_collection_partial. The key is always present:nullwhen the last persisted collection was complete.attempted_scopesis 0 for AWS because AWS does not report it.failed_detailsonly applies to AWS.reasonskeys come from a closed set:permission_denied,throttled,timeout,canceled,not_found,provider_unavailable,other. They are derived from azcore/googleapi/gRPC/smithy error codes and statuses.recommendations_stateis a single global row that everyview:recommendationsuser reads, while accounts are access-scoped, so naming failed subscriptions there would leak their existence to users who are not authorized for them. The failed IDs and raw errors stay in the existing WARN log line. This is a deliberate departure from the issue's "failed subscription IDs" bullet. If the maintainer wants IDs surfaced, it needs a per-viewer, scope-filtered endpoint, which is out of scope here.openapi.yaml: the freshness path was not documented at all. This PR adds/api/recommendations/freshnessplus theRecommendationsFreshness,RecommendationsCollectionPartialandPartialProviderSweepschemas (with 401/403).Migration notes
ALTER TABLE recommendations_state ADD COLUMN IF NOT EXISTS last_collection_partial JSONB(nullable, so existing deployments read as complete). The down migration drops it.000128_recommendations_state_partial_test.gousespreviousMigrationVersion, so it does not depend on 127 existing.Frontend follow-up (not in this PR; frontend/ untouched)
frontend/src/recommendations.ts, render a "Partial collection" banner or badge whenfreshness.last_collection_partialis non-null. Example copy: "Azure: 9 of 10 subscriptions could not be queried (permission denied ×7, throttled ×2). Savings shown may be understated." Useattempted_scopeswhen it is > 0; otherwise showfailed_scopesalone (AWS).last_collection_errorbanner. Partial data is still valid, and the post-refresh path atrecommendations.ts:445currently treatslast_collection_erroras "refresh failed". That is the main reason this is a separate field rather than text inlast_collection_error.last_collection_partial: null).Tests
CollectRecommendations(ctx, ""):SetRecommendationsCollectionPartialgets the exact expected summary;last_collection_errorand never sets the marker.appendPartialProvider, and a sanitization test that the JSON contains no subscription IDs, tenant IDs, ARNs or raw error text.nilclears. Also a unit test that undecodable JSON reads as nil instead of failing the freshness read.nullwhen the collection was complete.Verification
All Go tests were run with
AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null. No real cloud calls were made.go build ./...,go vet ./...,go vet -tags integration ./...: clean../internal/scheduler/,./internal/config/,./internal/api/,./internal/mocks/,./internal/server/,./internal/analytics/and./internal/database/postgres/migrations/../internal/database/postgres/migrations/package (152s), the full./internal/scheduler/package, and./internal/config/ -run 'Recommendation|Freshness|Collection'.--allow-parallel-runners) on the touched packages: 0 issues.gocyclo -over 10on changed non-test files: none.--register-aws, isolated HOME, temp copy with.gitallowed) on all changed files: clean.scripts/test-git-secrets-allowlist.sh: 42 PASS, 0 FAIL.forAccount;tolerateIncompleteSweepreturning nil for a partial (this is the pre-fix behavior);appendPartialProviderbeing dropped;persistCollectionskipping the write (also the pre-fix behavior);attemptedScopesbeing zeroed;UpsertRecommendationsnot clearing the column (killed by the integration test).Out of scope
PartialResultError(per-service partials).tolerateIncompleteSweepdid not handle it before this PR and still does not.known_issues/has no entry for fix(scheduler): surface partial multi-subscription sweeps in collection state so the dashboard shows them as incomplete #87, so there is nothing to move.Closes #87
🤖 Generated with Claude Code