fix(stats): recover durable daily mileage projections

This commit is contained in:
lingniu
2026-07-19 21:00:56 +08:00
parent 2b18a400b1
commit 5539c5e0c2
6 changed files with 174 additions and 0 deletions

View File

@@ -95,6 +95,13 @@ type ProjectionFlushResult struct {
Failed int
}
type ProjectionReconcileResult struct {
Targets int
Attempted int
Written int
Failed int
}
type CacheStats struct {
LastTotalMileageEntries int
LastSourceSeenEntries int
@@ -553,6 +560,62 @@ func (w *Writer) FlushPendingProjections(ctx context.Context, readyBefore time.T
return result, flushErr
}
// ReconcileDateProjections rebuilds final daily rows whose durable source
// candidates are newer than the projection (or whose projection is missing).
// The pending queue is intentionally in-memory for the hot path; this durable
// sweep closes the crash window between a committed source candidate and its
// throttled final projection.
func (w *Writer) ReconcileDateProjections(ctx context.Context, statDate string) (ProjectionReconcileResult, error) {
var result ProjectionReconcileResult
if w.query == nil {
return result, errors.New("daily mileage projection reconciliation requires query support")
}
statDate = strings.TrimSpace(statDate)
if statDate == "" {
return result, errors.New("daily mileage projection reconciliation requires stat date")
}
rows, err := w.query.QueryContext(ctx, staleDailyMileageProjectionTargetsSQL, statDate)
if err != nil {
return result, err
}
type target struct {
vin string
protocol envelope.Protocol
}
var targets []target
for rows.Next() {
var item target
if err := rows.Scan(&item.vin, &item.protocol); err != nil {
_ = rows.Close()
return result, err
}
if strings.TrimSpace(item.vin) != "" && strings.TrimSpace(string(item.protocol)) != "" {
targets = append(targets, item)
}
}
if err := rows.Close(); err != nil {
return result, err
}
if err := rows.Err(); err != nil {
return result, err
}
result.Targets = len(targets)
var reconcileErr error
for _, item := range targets {
if err := ctx.Err(); err != nil {
return result, errors.Join(reconcileErr, err)
}
result.Attempted++
if err := ProjectDailyMileage(ctx, w.exec, item.vin, statDate, item.protocol); err != nil {
result.Failed++
reconcileErr = errors.Join(reconcileErr, fmt.Errorf("reconcile daily mileage %s/%s/%s: %w", item.vin, statDate, item.protocol, err))
continue
}
result.Written++
}
return result, reconcileErr
}
func (w *Writer) projectPendingMileage(ctx context.Context, sample MetricSample) error {
project := func(exec Execer) error {
if strings.Contains(sample.SourceKey, platformSourceKeyPrefix) {
@@ -576,6 +639,24 @@ func (w *Writer) projectPendingMileage(ctx context.Context, sample MetricSample)
return project(w.exec)
}
const staleDailyMileageProjectionTargetsSQL = `
SELECT s.vin, s.protocol
FROM vehicle_daily_mileage_source s
LEFT JOIN vehicle_daily_mileage m
ON m.vin = s.vin
AND m.stat_date = s.stat_date
AND m.protocol = s.protocol
WHERE s.stat_date = ?
AND s.quality_status = '` + QualityOK + `'
AND (
m.vin IS NULL
OR s.updated_at > m.updated_at
OR s.latest_event_time > m.updated_at
)
GROUP BY s.vin, s.protocol
ORDER BY s.protocol ASC, s.vin ASC
`
func (w *Writer) seenSameMileage(sample MetricSample) bool {
key := mileageCacheKey(sample)
w.mu.Lock()