feat(stats): backfill mileage source candidates
This commit is contained in:
@@ -39,31 +39,40 @@ type config struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type rawFrameRow struct {
|
type rawFrameRow struct {
|
||||||
Protocol envelope.Protocol
|
Protocol envelope.Protocol
|
||||||
VIN string
|
VIN string
|
||||||
EventID string
|
Phone string
|
||||||
MessageID string
|
DeviceID string
|
||||||
EventTimeMS int64
|
SourceEndpoint string
|
||||||
ReceivedAtMS int64
|
EventID string
|
||||||
ParsedJSON string
|
MessageID string
|
||||||
|
EventTimeMS int64
|
||||||
|
ReceivedAtMS int64
|
||||||
|
ParsedJSON string
|
||||||
}
|
}
|
||||||
|
|
||||||
type metricAgg struct {
|
type metricAgg struct {
|
||||||
VIN string
|
VIN string
|
||||||
Date string
|
Date string
|
||||||
Protocol envelope.Protocol
|
Protocol envelope.Protocol
|
||||||
FirstKM float64
|
FirstKM float64
|
||||||
LatestKM float64
|
LatestKM float64
|
||||||
Count int64
|
Count int64
|
||||||
SourceKey string
|
SourceKey string
|
||||||
Phone string
|
Phone string
|
||||||
SourceEndpoint string
|
DeviceID string
|
||||||
|
SourceEndpoint string
|
||||||
|
FirstEventTime time.Time
|
||||||
|
LatestEventTime time.Time
|
||||||
|
QualityStatus string
|
||||||
|
QualityReason string
|
||||||
}
|
}
|
||||||
|
|
||||||
type dailySourceLast struct {
|
type dailySourceLast struct {
|
||||||
VIN string
|
VIN string
|
||||||
SourceKey string
|
SourceKey string
|
||||||
Phone string
|
Phone string
|
||||||
|
DeviceID string
|
||||||
SourceEndpoint string
|
SourceEndpoint string
|
||||||
TS time.Time
|
TS time.Time
|
||||||
TotalKM float64
|
TotalKM float64
|
||||||
@@ -164,15 +173,18 @@ func main() {
|
|||||||
}
|
}
|
||||||
parsed++
|
parsed++
|
||||||
env := envelope.FrameEnvelope{
|
env := envelope.FrameEnvelope{
|
||||||
EventID: row.EventID,
|
EventID: row.EventID,
|
||||||
Protocol: row.Protocol,
|
Protocol: row.Protocol,
|
||||||
MessageID: row.MessageID,
|
MessageID: row.MessageID,
|
||||||
VIN: row.VIN,
|
VIN: row.VIN,
|
||||||
EventTimeMS: row.EventTimeMS,
|
Phone: row.Phone,
|
||||||
ReceivedAtMS: row.ReceivedAtMS,
|
DeviceID: row.DeviceID,
|
||||||
Fields: fields,
|
SourceEndpoint: row.SourceEndpoint,
|
||||||
ParsedFields: fields,
|
EventTimeMS: row.EventTimeMS,
|
||||||
ParseStatus: envelope.ParseOK,
|
ReceivedAtMS: row.ReceivedAtMS,
|
||||||
|
Fields: fields,
|
||||||
|
ParsedFields: fields,
|
||||||
|
ParseStatus: envelope.ParseOK,
|
||||||
}
|
}
|
||||||
samples, err := stats.SamplesFromEnvelope(env, cfg.Location)
|
samples, err := stats.SamplesFromEnvelope(env, cfg.Location)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -205,16 +217,24 @@ func main() {
|
|||||||
|
|
||||||
func addSamples(aggregates map[string]*metricAgg, samples []stats.MetricSample) {
|
func addSamples(aggregates map[string]*metricAgg, samples []stats.MetricSample) {
|
||||||
for _, sample := range samples {
|
for _, sample := range samples {
|
||||||
key := sample.VIN + "|" + sample.StatDate + "|" + string(sample.Protocol)
|
key := sample.VIN + "|" + sample.StatDate + "|" + string(sample.Protocol) + "|" + sample.SourceKey
|
||||||
agg, ok := aggregates[key]
|
agg, ok := aggregates[key]
|
||||||
if !ok {
|
if !ok {
|
||||||
aggregates[key] = &metricAgg{
|
aggregates[key] = &metricAgg{
|
||||||
VIN: sample.VIN,
|
VIN: sample.VIN,
|
||||||
Date: sample.StatDate,
|
Date: sample.StatDate,
|
||||||
Protocol: sample.Protocol,
|
Protocol: sample.Protocol,
|
||||||
FirstKM: sample.TotalMileageKM,
|
FirstKM: sample.TotalMileageKM,
|
||||||
LatestKM: sample.TotalMileageKM,
|
LatestKM: sample.TotalMileageKM,
|
||||||
Count: 1,
|
Count: 1,
|
||||||
|
SourceKey: sample.SourceKey,
|
||||||
|
Phone: sample.Phone,
|
||||||
|
DeviceID: sample.DeviceID,
|
||||||
|
SourceEndpoint: sample.SourceEndpoint,
|
||||||
|
FirstEventTime: sample.EventTime,
|
||||||
|
LatestEventTime: sample.EventTime,
|
||||||
|
QualityStatus: stats.QualityOK,
|
||||||
|
QualityReason: "scan_backfill",
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -224,6 +244,19 @@ func addSamples(aggregates map[string]*metricAgg, samples []stats.MetricSample)
|
|||||||
if sample.TotalMileageKM > agg.LatestKM {
|
if sample.TotalMileageKM > agg.LatestKM {
|
||||||
agg.LatestKM = sample.TotalMileageKM
|
agg.LatestKM = sample.TotalMileageKM
|
||||||
}
|
}
|
||||||
|
if agg.FirstEventTime.IsZero() || sample.EventTime.Before(agg.FirstEventTime) {
|
||||||
|
agg.FirstEventTime = sample.EventTime
|
||||||
|
}
|
||||||
|
if sample.EventTime.After(agg.LatestEventTime) {
|
||||||
|
agg.LatestEventTime = sample.EventTime
|
||||||
|
agg.SourceEndpoint = sample.SourceEndpoint
|
||||||
|
if strings.TrimSpace(sample.Phone) != "" {
|
||||||
|
agg.Phone = sample.Phone
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(sample.DeviceID) != "" {
|
||||||
|
agg.DeviceID = sample.DeviceID
|
||||||
|
}
|
||||||
|
}
|
||||||
agg.Count++
|
agg.Count++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -232,66 +265,56 @@ func writeAggregates(ctx context.Context, db *sql.DB, aggregates map[string]*met
|
|||||||
if len(aggregates) == 0 {
|
if len(aggregates) == 0 {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
if batchSize <= 0 {
|
|
||||||
batchSize = 500
|
|
||||||
}
|
|
||||||
rows := make([]*metricAgg, 0, len(aggregates))
|
|
||||||
for _, agg := range aggregates {
|
|
||||||
rows = append(rows, agg)
|
|
||||||
}
|
|
||||||
var written int64
|
var written int64
|
||||||
for start := 0; start < len(rows); start += batchSize {
|
for _, agg := range aggregates {
|
||||||
end := start + batchSize
|
identity := stats.SourceIdentity{
|
||||||
if end > len(rows) {
|
Protocol: agg.Protocol,
|
||||||
end = len(rows)
|
SourceIP: stats.NormalizeSourceIP(agg.SourceEndpoint),
|
||||||
|
SourceEndpoint: agg.SourceEndpoint,
|
||||||
}
|
}
|
||||||
if err := writeAggregateBatch(ctx, db, rows[start:end]); err != nil {
|
if err := stats.UpsertDataSource(ctx, db, identity, agg.LatestEventTime); err != nil {
|
||||||
return written, err
|
return written, err
|
||||||
}
|
}
|
||||||
written += int64(end - start)
|
dailyKM := agg.LatestKM - agg.FirstKM
|
||||||
|
candidate := stats.SourceMileageSample{
|
||||||
|
VIN: agg.VIN,
|
||||||
|
StatDate: agg.Date,
|
||||||
|
Protocol: agg.Protocol,
|
||||||
|
SourceKey: agg.SourceKey,
|
||||||
|
SourceIP: identity.SourceIP,
|
||||||
|
SourceEndpoint: agg.SourceEndpoint,
|
||||||
|
Phone: agg.Phone,
|
||||||
|
DeviceID: agg.DeviceID,
|
||||||
|
FirstTotalKM: agg.FirstKM,
|
||||||
|
LatestTotalKM: agg.LatestKM,
|
||||||
|
DailyKM: dailyKM,
|
||||||
|
SampleCount: agg.Count,
|
||||||
|
FirstEventTime: agg.FirstEventTime,
|
||||||
|
LatestEventTime: agg.LatestEventTime,
|
||||||
|
QualityStatus: agg.QualityStatus,
|
||||||
|
QualityReason: agg.QualityReason,
|
||||||
|
}
|
||||||
|
if candidate.QualityStatus == "" {
|
||||||
|
candidate.QualityStatus = stats.QualityOK
|
||||||
|
}
|
||||||
|
if candidate.QualityReason == "" {
|
||||||
|
candidate.QualityReason = "same_source_previous_day"
|
||||||
|
}
|
||||||
|
if candidate.QualityStatus == stats.QualityOK && (candidate.DailyKM < 0 || candidate.DailyKM > maxTrustedDailyMileageKM) {
|
||||||
|
candidate.QualityStatus = stats.QualityInvalidDelta
|
||||||
|
candidate.QualityReason = "outside_daily_range"
|
||||||
|
}
|
||||||
|
if err := stats.UpsertSourceMileage(ctx, db, candidate); err != nil {
|
||||||
|
return written, err
|
||||||
|
}
|
||||||
|
if err := stats.ProjectDailyMileage(ctx, db, agg.VIN, agg.Date, agg.Protocol); err != nil {
|
||||||
|
return written, err
|
||||||
|
}
|
||||||
|
written++
|
||||||
}
|
}
|
||||||
return written, nil
|
return written, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeAggregateBatch(ctx context.Context, db *sql.DB, rows []*metricAgg) error {
|
|
||||||
placeholders := make([]string, 0, len(rows))
|
|
||||||
args := make([]any, 0, len(rows)*7)
|
|
||||||
for _, row := range rows {
|
|
||||||
daily := row.LatestKM - row.FirstKM
|
|
||||||
if daily < 0 {
|
|
||||||
daily = 0
|
|
||||||
}
|
|
||||||
placeholders = append(placeholders, "(?,?,?,?,?,?,?,?,?,?)")
|
|
||||||
args = append(args,
|
|
||||||
row.VIN,
|
|
||||||
row.Date,
|
|
||||||
string(row.Protocol),
|
|
||||||
daily,
|
|
||||||
row.FirstKM,
|
|
||||||
row.LatestKM,
|
|
||||||
row.SourceKey,
|
|
||||||
row.Phone,
|
|
||||||
row.SourceEndpoint,
|
|
||||||
row.Count,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
sqlText := `INSERT INTO vehicle_daily_mileage
|
|
||||||
(vin, stat_date, protocol, daily_mileage_km, first_total_mileage_km, latest_total_mileage_km,
|
|
||||||
trusted_source_key, trusted_phone, trusted_source_endpoint, sample_count)
|
|
||||||
VALUES ` + strings.Join(placeholders, ",") + `
|
|
||||||
ON DUPLICATE KEY UPDATE
|
|
||||||
daily_mileage_km = VALUES(daily_mileage_km),
|
|
||||||
first_total_mileage_km = VALUES(first_total_mileage_km),
|
|
||||||
latest_total_mileage_km = VALUES(latest_total_mileage_km),
|
|
||||||
trusted_source_key = VALUES(trusted_source_key),
|
|
||||||
trusted_phone = VALUES(trusted_phone),
|
|
||||||
trusted_source_endpoint = VALUES(trusted_source_endpoint),
|
|
||||||
sample_count = VALUES(sample_count),
|
|
||||||
updated_at = CURRENT_TIMESTAMP`
|
|
||||||
_, err := db.ExecContext(ctx, sqlText, args...)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildLastDiffAggregates(ctx context.Context, db *sql.DB, cfg config) (map[string]*metricAgg, error) {
|
func buildLastDiffAggregates(ctx context.Context, db *sql.DB, cfg config) (map[string]*metricAgg, error) {
|
||||||
dates, err := dateRangeWithPrevious(cfg.DateFrom, cfg.DateTo)
|
dates, err := dateRangeWithPrevious(cfg.DateFrom, cfg.DateTo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -320,21 +343,35 @@ func buildLastDiffAggregates(ctx context.Context, db *sql.DB, cfg config) (map[s
|
|||||||
current := lastByProtocolDate[protocol][date]
|
current := lastByProtocolDate[protocol][date]
|
||||||
previous := lastByProtocolDate[protocol][prevDate]
|
previous := lastByProtocolDate[protocol][prevDate]
|
||||||
for vin, currentSources := range current {
|
for vin, currentSources := range current {
|
||||||
chosen, ok := chooseTrustedSource(currentSources, previous[vin])
|
previousBySource := map[string]dailySourceLast{}
|
||||||
if !ok {
|
for _, previousRow := range previous[vin] {
|
||||||
continue
|
previousBySource[previousRow.SourceKey] = previousRow
|
||||||
}
|
}
|
||||||
key := vin + "|" + date + "|" + string(protocol)
|
for _, currentRow := range currentSources {
|
||||||
aggregates[key] = &metricAgg{
|
key := vin + "|" + date + "|" + string(protocol) + "|" + currentRow.SourceKey
|
||||||
VIN: vin,
|
agg := &metricAgg{
|
||||||
Date: date,
|
VIN: vin,
|
||||||
Protocol: protocol,
|
Date: date,
|
||||||
FirstKM: chosen.previous.TotalKM,
|
Protocol: protocol,
|
||||||
LatestKM: chosen.current.TotalKM,
|
FirstKM: currentRow.TotalKM,
|
||||||
Count: 1,
|
LatestKM: currentRow.TotalKM,
|
||||||
SourceKey: chosen.current.SourceKey,
|
Count: 1,
|
||||||
Phone: chosen.current.Phone,
|
SourceKey: currentRow.SourceKey,
|
||||||
SourceEndpoint: chosen.current.SourceEndpoint,
|
Phone: currentRow.Phone,
|
||||||
|
DeviceID: currentRow.DeviceID,
|
||||||
|
SourceEndpoint: currentRow.SourceEndpoint,
|
||||||
|
FirstEventTime: currentRow.TS,
|
||||||
|
LatestEventTime: currentRow.TS,
|
||||||
|
QualityStatus: stats.QualityNoPreviousBaseline,
|
||||||
|
QualityReason: "missing_previous_source",
|
||||||
|
}
|
||||||
|
if previousRow, ok := previousBySource[currentRow.SourceKey]; ok {
|
||||||
|
agg.FirstKM = previousRow.TotalKM
|
||||||
|
agg.FirstEventTime = previousRow.TS
|
||||||
|
agg.QualityStatus = stats.QualityOK
|
||||||
|
agg.QualityReason = "same_source_previous_day"
|
||||||
|
}
|
||||||
|
aggregates[key] = agg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -383,10 +420,10 @@ func queryDailyLastSourceRows(ctx context.Context, db *sql.DB, cfg config, proto
|
|||||||
fmt.Sprintf("protocol = '%s'", quote(string(protocol))),
|
fmt.Sprintf("protocol = '%s'", quote(string(protocol))),
|
||||||
realtimeMileageFramePredicate(),
|
realtimeMileageFramePredicate(),
|
||||||
}
|
}
|
||||||
sqlText := fmt.Sprintf(`SELECT vin, phone, source_endpoint, LAST(ts), LAST(parsed_json)
|
sqlText := fmt.Sprintf(`SELECT vin, phone, device_id, source_endpoint, LAST(ts), LAST(parsed_json)
|
||||||
FROM %s.raw_frames
|
FROM %s.raw_frames
|
||||||
WHERE %s
|
WHERE %s
|
||||||
GROUP BY vin, phone, source_endpoint`, ident(cfg.TDengineDatabase), strings.Join(where, " AND "))
|
GROUP BY vin, phone, device_id, source_endpoint`, ident(cfg.TDengineDatabase), strings.Join(where, " AND "))
|
||||||
rows, err := db.QueryContext(ctx, sqlText)
|
rows, err := db.QueryContext(ctx, sqlText)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -396,10 +433,11 @@ GROUP BY vin, phone, source_endpoint`, ident(cfg.TDengineDatabase), strings.Join
|
|||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var vin string
|
var vin string
|
||||||
var phone string
|
var phone string
|
||||||
|
var deviceID string
|
||||||
var sourceEndpoint string
|
var sourceEndpoint string
|
||||||
var ts time.Time
|
var ts time.Time
|
||||||
var parsedJSON string
|
var parsedJSON string
|
||||||
if err := rows.Scan(&vin, &phone, &sourceEndpoint, &ts, &parsedJSON); err != nil {
|
if err := rows.Scan(&vin, &phone, &deviceID, &sourceEndpoint, &ts, &parsedJSON); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
fields := fieldsForStats(protocol, vin, parsedJSON)
|
fields := fieldsForStats(protocol, vin, parsedJSON)
|
||||||
@@ -416,7 +454,7 @@ GROUP BY vin, phone, source_endpoint`, ident(cfg.TDengineDatabase), strings.Join
|
|||||||
if err != nil || len(samples) == 0 {
|
if err != nil || len(samples) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
sourceKey := normalizedSourceKey(phone, sourceEndpoint)
|
sourceKey := normalizedSourceKey(string(protocol), phone, deviceID, sourceEndpoint)
|
||||||
if sourceKey == "" {
|
if sourceKey == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -424,6 +462,7 @@ GROUP BY vin, phone, source_endpoint`, ident(cfg.TDengineDatabase), strings.Join
|
|||||||
VIN: strings.TrimSpace(vin),
|
VIN: strings.TrimSpace(vin),
|
||||||
SourceKey: sourceKey,
|
SourceKey: sourceKey,
|
||||||
Phone: strings.TrimSpace(phone),
|
Phone: strings.TrimSpace(phone),
|
||||||
|
DeviceID: strings.TrimSpace(deviceID),
|
||||||
SourceEndpoint: strings.TrimSpace(sourceEndpoint),
|
SourceEndpoint: strings.TrimSpace(sourceEndpoint),
|
||||||
TS: ts,
|
TS: ts,
|
||||||
TotalKM: samples[0].TotalMileageKM,
|
TotalKM: samples[0].TotalMileageKM,
|
||||||
@@ -443,26 +482,9 @@ GROUP BY vin, phone, source_endpoint`, ident(cfg.TDengineDatabase), strings.Join
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizedSourceKey(phone string, endpoint string) string {
|
func normalizedSourceKey(protocol string, phone string, deviceID string, endpoint string) string {
|
||||||
var parts []string
|
sourceIP := stats.NormalizeSourceIP(endpoint)
|
||||||
if phone = strings.TrimSpace(phone); phone != "" {
|
return stats.SourceKey(envelope.Protocol(protocol), phone, deviceID, sourceIP)
|
||||||
parts = append(parts, phone)
|
|
||||||
}
|
|
||||||
if host := endpointHost(endpoint); host != "" {
|
|
||||||
parts = append(parts, host)
|
|
||||||
}
|
|
||||||
return strings.Join(parts, "@")
|
|
||||||
}
|
|
||||||
|
|
||||||
func endpointHost(endpoint string) string {
|
|
||||||
endpoint = strings.TrimSpace(endpoint)
|
|
||||||
if endpoint == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if host, _, ok := strings.Cut(endpoint, ":"); ok {
|
|
||||||
return strings.TrimSpace(host)
|
|
||||||
}
|
|
||||||
return endpoint
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func fieldsForStats(protocol envelope.Protocol, vin string, text string) map[string]any {
|
func fieldsForStats(protocol envelope.Protocol, vin string, text string) map[string]any {
|
||||||
@@ -563,7 +585,7 @@ func queryRawFrames(ctx context.Context, db *sql.DB, cfg config) (*sql.Rows, err
|
|||||||
where = append(where, "protocol IN ("+strings.Join(quoted, ",")+")")
|
where = append(where, "protocol IN ("+strings.Join(quoted, ",")+")")
|
||||||
}
|
}
|
||||||
where = append(where, realtimeMileageFramePredicate())
|
where = append(where, realtimeMileageFramePredicate())
|
||||||
sqlText := fmt.Sprintf(`SELECT protocol, vin, event_id, message_id, event_time, received_at, parsed_json
|
sqlText := fmt.Sprintf(`SELECT protocol, vin, phone, device_id, source_endpoint, event_id, message_id, event_time, received_at, parsed_json
|
||||||
FROM %s.raw_frames
|
FROM %s.raw_frames
|
||||||
WHERE %s
|
WHERE %s
|
||||||
ORDER BY ts ASC`, ident(cfg.TDengineDatabase), strings.Join(where, " AND "))
|
ORDER BY ts ASC`, ident(cfg.TDengineDatabase), strings.Join(where, " AND "))
|
||||||
@@ -583,20 +605,23 @@ func realtimeMileageFramePredicate() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func scanRawFrame(rows *sql.Rows) (rawFrameRow, error) {
|
func scanRawFrame(rows *sql.Rows) (rawFrameRow, error) {
|
||||||
var protocol, vin, eventID, parsed string
|
var protocol, vin, phone, deviceID, sourceEndpoint, eventID, parsed string
|
||||||
var messageID int64
|
var messageID int64
|
||||||
var eventTime, receivedAt time.Time
|
var eventTime, receivedAt time.Time
|
||||||
if err := rows.Scan(&protocol, &vin, &eventID, &messageID, &eventTime, &receivedAt, &parsed); err != nil {
|
if err := rows.Scan(&protocol, &vin, &phone, &deviceID, &sourceEndpoint, &eventID, &messageID, &eventTime, &receivedAt, &parsed); err != nil {
|
||||||
return rawFrameRow{}, err
|
return rawFrameRow{}, err
|
||||||
}
|
}
|
||||||
return rawFrameRow{
|
return rawFrameRow{
|
||||||
Protocol: envelope.Protocol(strings.TrimSpace(protocol)),
|
Protocol: envelope.Protocol(strings.TrimSpace(protocol)),
|
||||||
VIN: strings.TrimSpace(vin),
|
VIN: strings.TrimSpace(vin),
|
||||||
EventID: strings.TrimSpace(eventID),
|
Phone: strings.TrimSpace(phone),
|
||||||
MessageID: fmt.Sprintf("0x%04X", messageID),
|
DeviceID: strings.TrimSpace(deviceID),
|
||||||
EventTimeMS: eventTime.UnixMilli(),
|
SourceEndpoint: strings.TrimSpace(sourceEndpoint),
|
||||||
ReceivedAtMS: receivedAt.UnixMilli(),
|
EventID: strings.TrimSpace(eventID),
|
||||||
ParsedJSON: parsed,
|
MessageID: fmt.Sprintf("0x%04X", messageID),
|
||||||
|
EventTimeMS: eventTime.UnixMilli(),
|
||||||
|
ReceivedAtMS: receivedAt.UnixMilli(),
|
||||||
|
ParsedJSON: parsed,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -647,11 +672,19 @@ func resetStats(ctx context.Context, db *sql.DB, cfg config) (int64, error) {
|
|||||||
}
|
}
|
||||||
clauses = append(clauses, "protocol IN ("+strings.Join(placeholders, ",")+")")
|
clauses = append(clauses, "protocol IN ("+strings.Join(placeholders, ",")+")")
|
||||||
}
|
}
|
||||||
result, err := db.ExecContext(ctx, "DELETE FROM vehicle_daily_mileage WHERE "+strings.Join(clauses, " AND "), args...)
|
var deleted int64
|
||||||
if err != nil {
|
for _, table := range []string{"vehicle_daily_mileage_source", "vehicle_daily_mileage"} {
|
||||||
return 0, err
|
result, err := db.ExecContext(ctx, "DELETE FROM "+table+" WHERE "+strings.Join(clauses, " AND "), args...)
|
||||||
|
if err != nil {
|
||||||
|
return deleted, err
|
||||||
|
}
|
||||||
|
affected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return deleted, err
|
||||||
|
}
|
||||||
|
deleted += affected
|
||||||
}
|
}
|
||||||
return result.RowsAffected()
|
return deleted, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseProtocols(value string) ([]envelope.Protocol, error) {
|
func parseProtocols(value string) ([]envelope.Protocol, error) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import "testing"
|
|||||||
func TestChooseTrustedSourceKeepsContinuingSourceAndRejectsNewJump(t *testing.T) {
|
func TestChooseTrustedSourceKeepsContinuingSourceAndRejectsNewJump(t *testing.T) {
|
||||||
previous := []dailySourceLast{{
|
previous := []dailySourceLast{{
|
||||||
VIN: "LA9GG64L7PBAF4001",
|
VIN: "LA9GG64L7PBAF4001",
|
||||||
SourceKey: normalizedSourceKey("13307765812", "115.231.168.135:42630"),
|
SourceKey: normalizedSourceKey("JT808", "13307765812", "", "115.231.168.135:42630"),
|
||||||
Phone: "13307765812",
|
Phone: "13307765812",
|
||||||
SourceEndpoint: "115.231.168.135:42630",
|
SourceEndpoint: "115.231.168.135:42630",
|
||||||
TotalKM: 4100.8,
|
TotalKM: 4100.8,
|
||||||
@@ -13,14 +13,14 @@ func TestChooseTrustedSourceKeepsContinuingSourceAndRejectsNewJump(t *testing.T)
|
|||||||
current := []dailySourceLast{
|
current := []dailySourceLast{
|
||||||
{
|
{
|
||||||
VIN: "LA9GG64L7PBAF4001",
|
VIN: "LA9GG64L7PBAF4001",
|
||||||
SourceKey: normalizedSourceKey("13307765812", "115.159.85.149:28316"),
|
SourceKey: normalizedSourceKey("JT808", "13307765812", "", "115.159.85.149:28316"),
|
||||||
Phone: "13307765812",
|
Phone: "13307765812",
|
||||||
SourceEndpoint: "115.159.85.149:28316",
|
SourceEndpoint: "115.159.85.149:28316",
|
||||||
TotalKM: 42447.2,
|
TotalKM: 42447.2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
VIN: "LA9GG64L7PBAF4001",
|
VIN: "LA9GG64L7PBAF4001",
|
||||||
SourceKey: normalizedSourceKey("13307765812", "115.231.168.135:20215"),
|
SourceKey: normalizedSourceKey("JT808", "13307765812", "", "115.231.168.135:20215"),
|
||||||
Phone: "13307765812",
|
Phone: "13307765812",
|
||||||
SourceEndpoint: "115.231.168.135:20215",
|
SourceEndpoint: "115.231.168.135:20215",
|
||||||
TotalKM: 4123.9,
|
TotalKM: 4123.9,
|
||||||
@@ -31,10 +31,30 @@ func TestChooseTrustedSourceKeepsContinuingSourceAndRejectsNewJump(t *testing.T)
|
|||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("chooseTrustedSource() did not choose a source")
|
t.Fatal("chooseTrustedSource() did not choose a source")
|
||||||
}
|
}
|
||||||
if chosen.current.SourceKey != normalizedSourceKey("13307765812", "115.231.168.135:20215") {
|
if chosen.current.SourceKey != normalizedSourceKey("JT808", "13307765812", "", "115.231.168.135:20215") {
|
||||||
t.Fatalf("chosen source = %q", chosen.current.SourceKey)
|
t.Fatalf("chosen source = %q", chosen.current.SourceKey)
|
||||||
}
|
}
|
||||||
if delta := chosen.current.TotalKM - chosen.previous.TotalKM; delta < 23 || delta > 24 {
|
if delta := chosen.current.TotalKM - chosen.previous.TotalKM; delta < 23 || delta > 24 {
|
||||||
t.Fatalf("delta = %v, want about 23.1", delta)
|
t.Fatalf("delta = %v, want about 23.1", delta)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDailySourceLastBuildsCandidateKeysBySourceIP(t *testing.T) {
|
||||||
|
sourceA := dailySourceLast{
|
||||||
|
VIN: "LA9GG64L7PBAF4001",
|
||||||
|
SourceKey: normalizedSourceKey("JT808", "13307765812", "", "115.231.168.135:20215"),
|
||||||
|
Phone: "13307765812",
|
||||||
|
SourceEndpoint: "115.231.168.135:20215",
|
||||||
|
TotalKM: 4123.9,
|
||||||
|
}
|
||||||
|
sourceB := dailySourceLast{
|
||||||
|
VIN: "LA9GG64L7PBAF4001",
|
||||||
|
SourceKey: normalizedSourceKey("JT808", "13307765812", "", "115.231.168.135:42630"),
|
||||||
|
Phone: "13307765812",
|
||||||
|
SourceEndpoint: "115.231.168.135:42630",
|
||||||
|
TotalKM: 4100.8,
|
||||||
|
}
|
||||||
|
if sourceA.SourceKey != sourceB.SourceKey {
|
||||||
|
t.Fatalf("same source IP should produce same source key: %q vs %q", sourceA.SourceKey, sourceB.SourceKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user