fix(stats): flush throttled daily mileage projections
This commit is contained in:
@@ -797,6 +797,97 @@ func TestWriterAppendThrottlesDailyProjectionPerVehicleProtocolDate(t *testing.T
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriterFlushesFinalProjectionWhenVehicleStopsInsideThrottleWindow(t *testing.T) {
|
||||
exec := &recordingExec{}
|
||||
loc := time.FixedZone("Asia/Shanghai", 8*3600)
|
||||
writer := NewWriter(exec, loc)
|
||||
base := managedTestSource(envelope.FrameEnvelope{
|
||||
Protocol: envelope.ProtocolJT808,
|
||||
VIN: "LMRKH9AC7R1004120",
|
||||
Phone: "64341233712",
|
||||
SourceEndpoint: "115.159.85.149:7339",
|
||||
Fields: map[string]any{
|
||||
"jt808.location.total_mileage_km": 4732.6,
|
||||
},
|
||||
})
|
||||
|
||||
first := base
|
||||
first.EventTimeMS = time.Date(2026, 7, 19, 15, 4, 45, 0, loc).UnixMilli()
|
||||
if err := writer.Append(context.Background(), first); err != nil {
|
||||
t.Fatalf("first Append() error = %v", err)
|
||||
}
|
||||
final := base
|
||||
final.EventTimeMS = time.Date(2026, 7, 19, 15, 4, 52, 0, loc).UnixMilli()
|
||||
final.Fields = map[string]any{"jt808.location.total_mileage_km": 4740.7}
|
||||
if err := writer.Append(context.Background(), final); err != nil {
|
||||
t.Fatalf("final Append() error = %v", err)
|
||||
}
|
||||
|
||||
if got := writer.CacheStats().PendingProjectionEntries; got != 1 {
|
||||
t.Fatalf("pending projections = %d, want final throttled sample queued", got)
|
||||
}
|
||||
beforeDue, err := writer.FlushPendingProjections(context.Background(), time.Now().Add(-time.Hour))
|
||||
if err != nil {
|
||||
t.Fatalf("early FlushPendingProjections() error = %v", err)
|
||||
}
|
||||
if beforeDue.Attempted != 0 || beforeDue.Written != 0 {
|
||||
t.Fatalf("early flush result = %+v, want no eligible projection", beforeDue)
|
||||
}
|
||||
|
||||
flushed, err := writer.FlushPendingProjections(context.Background(), time.Time{})
|
||||
if err != nil {
|
||||
t.Fatalf("FlushPendingProjections() error = %v", err)
|
||||
}
|
||||
if flushed.Attempted != 1 || flushed.Written != 1 {
|
||||
t.Fatalf("flush result = %+v, want final projection written", flushed)
|
||||
}
|
||||
if got := countExecQueries(exec.calls, "INSERT INTO vehicle_daily_mileage\n"); got != 2 {
|
||||
t.Fatalf("daily projections = %d, want initial plus eventual final projection", got)
|
||||
}
|
||||
if got := writer.CacheStats().PendingProjectionEntries; got != 0 {
|
||||
t.Fatalf("pending projections after flush = %d, want 0", got)
|
||||
}
|
||||
key := projectionCacheKey(MetricSample{
|
||||
VIN: "LMRKH9AC7R1004120",
|
||||
Protocol: envelope.ProtocolJT808,
|
||||
StatDate: "2026-07-19",
|
||||
})
|
||||
wantProjectedAt := time.UnixMilli(final.EventTimeMS).In(loc)
|
||||
if got := writer.lastProjection[key].projectedAt; !got.Equal(wantProjectedAt) {
|
||||
t.Fatalf("last projected event = %s, want %s", got, wantProjectedAt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriterPendingProjectionFailureDoesNotBlockOtherVehicles(t *testing.T) {
|
||||
exec := &selectiveFailingExec{failVIN: "FAIL-VIN"}
|
||||
loc := time.FixedZone("Asia/Shanghai", 8*3600)
|
||||
writer := NewWriter(exec, loc)
|
||||
writer.projectionInterval = time.Hour
|
||||
|
||||
for _, vin := range []string{"FAIL-VIN", "OK-VIN"} {
|
||||
sample := MetricSample{
|
||||
VIN: vin,
|
||||
Protocol: envelope.ProtocolJT808,
|
||||
StatDate: "2026-07-19",
|
||||
SourceKey: "jt808:test",
|
||||
EventTime: time.Date(2026, 7, 19, 15, 4, 52, 0, loc),
|
||||
TotalMileageKM: 100,
|
||||
}
|
||||
writer.markProjectionPending(sample, time.Now().Add(-time.Hour))
|
||||
}
|
||||
|
||||
result, err := writer.FlushPendingProjections(context.Background(), time.Time{})
|
||||
if err == nil {
|
||||
t.Fatal("FlushPendingProjections() error = nil, want failed vehicle reported")
|
||||
}
|
||||
if result.Attempted != 2 || result.Written != 1 || result.Failed != 1 {
|
||||
t.Fatalf("flush result = %+v, want one failure and one successful projection", result)
|
||||
}
|
||||
if got := writer.CacheStats().PendingProjectionEntries; got != 1 {
|
||||
t.Fatalf("pending projections after partial failure = %d, want only failed vehicle retained", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriterAppendProjectsImmediatelyForNewSource(t *testing.T) {
|
||||
exec := &recordingExec{}
|
||||
loc := time.FixedZone("Asia/Shanghai", 8*3600)
|
||||
@@ -2321,6 +2412,17 @@ type recordingExec struct {
|
||||
errs []error
|
||||
}
|
||||
|
||||
type selectiveFailingExec struct {
|
||||
failVIN string
|
||||
}
|
||||
|
||||
func (e *selectiveFailingExec) ExecContext(_ context.Context, _ string, args ...any) (sql.Result, error) {
|
||||
if len(args) > 0 && fmt.Sprint(args[0]) == e.failVIN {
|
||||
return nil, errors.New("test projection failure")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type concurrentExec struct {
|
||||
mu sync.Mutex
|
||||
calls int
|
||||
|
||||
Reference in New Issue
Block a user