Skip to content

fix(plans): a ramp step counts as complete when one of N accounts buys, and both silent-under-buy paths are log-only #1861

Description

@cristim

Split out of #1669 (fix in flight on fix/1669-ramp-step-idempotent), which deliberately kept its scope to "one ramp step must not be counted twice". Two problems remain, and they are the same problem seen from two sides: a plan can quietly buy less commitment than the customer intended, and nothing durable says so.

1. A ramp step counts as complete when ONE account's execution runs clean

CompletePlanStep advances the ramp when any execution for that step finishes successfully. A multi-account plan fans one ramp step out into one execution per cloud account, each of which succeeds or fails independently, so an operator who repairs and retries a single failed account moves the plan to "step N done" while the other accounts have bought nothing for step N.

This predates #1669 and is unchanged by it: the pre-#1669 blind CurrentStep++ had the same granularity, plus the double-count that #1669 removes. It is visible directly in that fix's own regression test (internal/purchase/ramp_step_progress_integration_test.go): a 3-account plan whose step-3 fan-out committed only account A reaches CurrentStep = 3 as soon as account B's retry succeeds, while account C is still failed.

The original issue asked for this decision to be settled explicitly:

a ramp step is only complete when every account's row for that step has succeeded, not when any one of them has. Worth settling that explicitly in this issue before implementing.

It was not settled, and #1669 does not settle it. Settling it is this issue.

Options, roughly in order of preference:

  1. Derive progress instead of accumulating it (fix(plans): retrying two failed accounts of one ramp step advances CurrentStep twice, so a later step silently never purchases #1669's option 2): compute CurrentStep from the executions table as the highest step whose per-account rows are all terminal-and-successful, rather than storing an incrementally-mutated counter. This also removes the migration/backfill class of problem entirely, because there is no stored convention left to get wrong.
  2. Gate the advance on sibling completeness: keep the counter, but only advance when no per-account row for that step is outstanding. Cheaper, but needs care that a permanently-failed account cannot freeze the ramp forever, which is the failure mode option 1 avoids by construction.

Whichever is chosen, "the plan's target accounts at the time the step ran" has to be pinned down, since a plan's account set can change between steps.

2. Two reachable money-path decisions are recorded only in a log line

Both of these silently under-buy and neither leaves durable evidence. purchase.executeAndFinalize calls updatePlanProgress for side effect only and swallows any error into logging.Errorf, so once the Lambda log ages out there is nothing to find.

  • A refused advance. CompletePlanStep refuses to advance when the completing step is more than one beyond CurrentStep, because the steps in between never completed and jumping would overstate what the plan bought. Reachable whenever a step fails terminally and a later step then succeeds. The plan silently stops advancing, next_execution_date stops moving, and plan_health can only infer "behind schedule" from wall-clock drift, which it reports identically for a plan that is merely late.
  • A mis-stamped step. A purchase_executions row whose step_number predates fix(plans): retrying two failed accounts of one ramp step advances CurrentStep twice, so a later step silently never purchases #1669's convention correction, or that was written during the deploy overlap, completes a step the plan has already counted. The advance is a correct no-op, and equally invisible.

Wanted:

  • Stamp the outcome on the execution row (an audit note alongside error) so a refused or no-op advance surfaces in History rather than only in CloudWatch.
  • Give plan_health a distinct stalled-ramp factor, keyed on the recorded refusal instead of letting behind_schedule proxy for it.

Why this is P1 rather than P2

The failure direction is the same silent under-buy that #1669 was filed for: commitment the customer intended to buy that never gets bought, with nothing erroring and the plan reporting itself further along than it is. Multi-account plans are the normal shape for the customers who ramp, and per-account retry became the canonical recovery flow in #1655, so item 1 is reachable on the routine path rather than an edge case.

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions