Summary
tolerateIncompleteSweep (internal/scheduler/scheduler.go:906-925) converts an Azure *azure.PartialSubscriptionFailureError into scheduler-level success. Today that is a no-op at all five call sites (every Azure path pins a subscription, and GCP/AWS never return that error type), so this is latent — it becomes live the moment the org-wide fan-out from #1520 is actually wired up.
The hazard
Once the fan-out is reachable, an account whose sweep missed N subscriptions is recorded as succeeded. Two consequences follow from treating a partial sweep as a complete one:
- Row eviction. The collection path replaces the account's stored recommendations with what the sweep returned. A sweep that never queried subscriptions X and Y returns no rows for them, so previously-collected rows for X and Y are deleted — the savings for those subscriptions silently vanish from the UI and from any downstream aggregate.
last_collection_error is cleared, so nothing on the account records that the sweep was incomplete.
This is exactly the COR-03 hazard that mergeServiceResults' own comment warns about (providers/azure/recommendations.go:276-280): a partial result persisted as if it were complete reads as a shrinking savings opportunity rather than a failed collection.
tolerateIncompleteSweep's NOTE mentions the last_collection_error consequence but not the eviction one, which is the more damaging of the two.
Suggested direction
Before (or as part of) the change that makes the fan-out reachable, decide explicitly how a partial sweep interacts with persistence. Options, roughly in increasing order of effort:
- Record the partial-failure detail on the account (a dedicated column, or keep
last_collection_error populated) instead of clearing it, so the incompleteness survives into the data model.
- Make the persistence step merge rather than replace when the sweep is known-partial, so un-queried subscriptions keep their previous rows.
- Scope the replace to the subscriptions that were actually queried (
PartialSubscriptionFailureError already carries Failed / FailedSubscriptionIDs(), so the queried set is derivable).
Whichever is chosen, extend tolerateIncompleteSweep's NOTE to state the eviction consequence, not only the last_collection_error one.
Provenance
Found during the adversarial review of #1520. Deliberately not fixed in that PR: the code path is unreachable today, and the fix is a persistence-semantics decision rather than a defect in the fan-out client.
Summary
tolerateIncompleteSweep(internal/scheduler/scheduler.go:906-925) converts an Azure*azure.PartialSubscriptionFailureErrorinto scheduler-level success. Today that is a no-op at all five call sites (every Azure path pins a subscription, and GCP/AWS never return that error type), so this is latent — it becomes live the moment the org-wide fan-out from #1520 is actually wired up.The hazard
Once the fan-out is reachable, an account whose sweep missed N subscriptions is recorded as succeeded. Two consequences follow from treating a partial sweep as a complete one:
last_collection_erroris cleared, so nothing on the account records that the sweep was incomplete.This is exactly the COR-03 hazard that
mergeServiceResults' own comment warns about (providers/azure/recommendations.go:276-280): a partial result persisted as if it were complete reads as a shrinking savings opportunity rather than a failed collection.tolerateIncompleteSweep's NOTE mentions thelast_collection_errorconsequence but not the eviction one, which is the more damaging of the two.Suggested direction
Before (or as part of) the change that makes the fan-out reachable, decide explicitly how a partial sweep interacts with persistence. Options, roughly in increasing order of effort:
last_collection_errorpopulated) instead of clearing it, so the incompleteness survives into the data model.PartialSubscriptionFailureErroralready carriesFailed/FailedSubscriptionIDs(), so the queried set is derivable).Whichever is chosen, extend
tolerateIncompleteSweep's NOTE to state the eviction consequence, not only thelast_collection_errorone.Provenance
Found during the adversarial review of #1520. Deliberately not fixed in that PR: the code path is unreachable today, and the fix is a persistence-semantics decision rather than a defect in the fan-out client.