fix(stats): recover durable daily mileage projections
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user