| index 627b15a29..60b2c8713 100644 |
| --- a/internal/console/economicspass.go |
| +++ b/internal/console/economicspass.go |
| @@ -810,17 +810,19 @@ func (s *Server) fleetMeters(ctx context.Context, from, to time.Time) (map[strin |
| } |
| |
| // statementCloseDayGrace is the day of the following month after which the |
| -// previous month stops being recomputed. Matches billCEDayGrace: the same |
| -// argument (late-arriving data has landed by now) and no reason to differ. |
| +// previous month stops being recomputed. Same three-day argument as |
| +// billCEDayGrace (late-arriving data has landed by now); the previous month |
| +// closes on day 4, one day after CE treats a pull as settled, since this |
| +// check is a strict ">" against the day billCEDayGrace's ">=" settles on. |
| const statementCloseDayGrace = 3 |
| |
| // statementsPass runs statementsForMonth for two periods on billPass's |
| -// pattern: this month to date, and the previous month while it is still |
| -// inside its statementCloseDayGrace catch-up window. Host metrics only roll |
| -// an hour up after it ends, so a month's last hours land in the next |
| -// month's objects, after now has already moved past it — without this |
| -// second period those hours would never reach the closed statement. Once |
| -// past the grace, the previous month's period is marked closing, and |
| +// pattern: this month to date, and the previous month, every tick, |
| +// unconditionally. Host metrics only roll an hour up after it ends, so a |
| +// month's last hours land in the next month's objects, after now has |
| +// already moved past it — without this second period those hours would |
| +// never reach the closed statement. Only once past statementCloseDayGrace |
| +// is the previous month's period marked closing, and |
| // UpsertStatement (its CAS loop, not this caller — see its doc comment) |
| // refuses every write to a statement once it is closed. Each period's |
| // failure is logged and does not skip the other, matching billPass and |
| @@ -861,10 +863,11 @@ func (s *Server) statementsPass(ctx context.Context, now time.Time) error { |
| // device) into one usage.Statement per account, create-or-replace via |
| // usage.UpsertStatement — safe to run repeatedly as new days' objects |
| // appear. statementsPass calls this once per period (this month, and the |
| -// previous month while it is still inside its catch-up grace) so that a |
| -// month's final hours, which host metrics only roll up after the hour ends, |
| -// still reach that month's statement even once now has crossed into the |
| -// next one. It is a thin worker: sweep the period (sweepUsage), fold in |
| +// previous month, every tick — the catch-up grace only decides `closing`, |
| +// not whether this runs) so that a month's final hours, which host metrics |
| +// only roll up after the hour ends, still reach that month's statement even |
| +// once now has crossed into the next one. It is a thin worker: sweep the |
| +// period (sweepUsage), fold in |
| // storage/census meters (foldStorageAndCensus), fold in fleet meters |
| // (fleetMeters — a union merge, since an account with ONLY fleet usage and |
| // no platform routes at all must still get every meter key it's owed), then |
| @@ -959,7 +962,14 @@ func (s *Server) statementsForMonth(ctx context.Context, month string, start, en |
| Closed: closing, |
| } |
| if err := usage.UpsertStatement(ctx, s.cfg.Releases, st); err != nil { |
| - return fmt.Errorf("console: statements pass: upsert %s: %w", acct, err) |
| + // Log and continue, not return: UpsertStatement now returns an |
| + // error for a statement object it cannot parse (rather than |
| + // overwriting it), and map iteration order is random, so |
| + // returning here would let one corrupt account's statement |
| + // wedge every other account's out of this and every future |
| + // tick until someone deletes the bad object by hand. |
| + log.Printf("console: statements pass: upsert %s: %v", acct, err) |
| + continue |
| } |
| } |
| return nil |