feat: build vehicle data platform and production pipeline

This commit is contained in:
lingniu
2026-07-14 12:35:33 +08:00
parent b452be3b94
commit bb59303a4b
270 changed files with 88016 additions and 1975 deletions

View File

@@ -2,19 +2,26 @@ package main
import (
"context"
"database/sql"
"flag"
"fmt"
"log"
"os"
"os/signal"
"strings"
"syscall"
_ "github.com/go-sql-driver/mysql"
"lingniu-vehicle-ingest/go/vehicle-gateway/internal/loadsim"
)
func main() {
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
flagCfg := loadsim.RegisterFlags(flag.CommandLine)
cleanupRegistration := flag.Bool("cleanup-registration", false, "delete this run's loopback JT808 registration rows after the simulation")
cleanupOnly := flag.Bool("cleanup-only", false, "skip the simulation and only clean the configured JT808 phone range")
mysqlDSN := flag.String("mysql-dsn", strings.TrimSpace(os.Getenv("MYSQL_DSN")), "MySQL DSN used only by JT808 registration cleanup")
if err := flag.CommandLine.Parse(os.Args[1:]); err != nil {
log.Fatal(err)
}
@@ -25,6 +32,14 @@ func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
if *cleanupOnly {
deleted, err := cleanupJT808Registrations(ctx, cfg, *mysqlDSN)
if err != nil {
log.Fatal(err)
}
log.Printf("jt808 synthetic registration cleanup completed rows_deleted=%d", deleted)
return
}
log.Printf("load simulation started protocol=%s addr=%s connections=%d connect_rate=%d send_interval=%s duration=%s template=%s",
cfg.Protocol, cfg.Addr, cfg.Connections, cfg.ConnectRatePerSecond, cfg.SendInterval, cfg.Duration, cfg.Template)
@@ -33,13 +48,57 @@ func main() {
log.Fatal(err)
}
log.Print(formatStats(stats))
if *cleanupRegistration {
deleted, err := cleanupJT808Registrations(ctx, cfg, *mysqlDSN)
if err != nil {
log.Fatal(err)
}
log.Printf("jt808 synthetic registration cleanup completed rows_deleted=%d", deleted)
}
}
func cleanupJT808Registrations(ctx context.Context, cfg loadsim.Config, dsn string) (int64, error) {
if cfg.Protocol != loadsim.ProtocolJT808 {
return 0, fmt.Errorf("registration cleanup only supports jt808")
}
if strings.TrimSpace(dsn) == "" {
return 0, fmt.Errorf("mysql-dsn or MYSQL_DSN is required for registration cleanup")
}
db, err := sql.Open("mysql", dsn)
if err != nil {
return 0, fmt.Errorf("open mysql for registration cleanup: %w", err)
}
defer db.Close()
return cleanupJT808RegistrationsWithDB(ctx, db, cfg)
}
type cleanupExecer interface {
ExecContext(context.Context, string, ...any) (sql.Result, error)
}
func cleanupJT808RegistrationsWithDB(ctx context.Context, exec cleanupExecer, cfg loadsim.Config) (int64, error) {
firstPhone := fmt.Sprintf("%012d", cfg.JT808PhoneBase)
lastPhone := fmt.Sprintf("%012d", cfg.JT808PhoneBase+int64(cfg.Connections)-1)
result, err := exec.ExecContext(ctx, `DELETE FROM jt808_registration
WHERE phone BETWEEN ? AND ?
AND source_ip IN ('127.0.0.1', '::1')`, firstPhone, lastPhone)
if err != nil {
return 0, fmt.Errorf("delete loopback jt808 registrations: %w", err)
}
deleted, err := result.RowsAffected()
if err != nil {
return 0, fmt.Errorf("read registration cleanup result: %w", err)
}
return deleted, nil
}
func formatStats(stats loadsim.Stats) string {
return fmt.Sprintf("connections_opened=%d connections_failed=%d frames_written=%d write_errors=%d",
return fmt.Sprintf("connections_opened=%d connections_failed=%d frames_written=%d write_errors=%d response_bytes=%d read_errors=%d",
stats.ConnectionsOpened,
stats.ConnectionsFailed,
stats.FramesWritten,
stats.WriteErrors,
stats.ResponseBytes,
stats.ReadErrors,
)
}

View File

@@ -1,9 +1,12 @@
package main
import (
"context"
"strings"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"lingniu-vehicle-ingest/go/vehicle-gateway/internal/loadsim"
)
@@ -13,6 +16,8 @@ func TestFormatStatsIncludesCapacityCounters(t *testing.T) {
ConnectionsFailed: 2,
FramesWritten: 300,
WriteErrors: 1,
ResponseBytes: 2048,
ReadErrors: 0,
})
for _, want := range []string{
@@ -20,9 +25,37 @@ func TestFormatStatsIncludesCapacityCounters(t *testing.T) {
"connections_failed=2",
"frames_written=300",
"write_errors=1",
"response_bytes=2048",
"read_errors=0",
} {
if !strings.Contains(out, want) {
t.Fatalf("formatStats() = %q, missing %q", out, want)
}
}
}
func TestCleanupJT808RegistrationsUsesBoundedLoopbackDelete(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatal(err)
}
defer db.Close()
mock.ExpectExec(`DELETE FROM jt808_registration`).
WithArgs("139000000000", "139000000999").
WillReturnResult(sqlmock.NewResult(0, 1000))
deleted, err := cleanupJT808RegistrationsWithDB(context.Background(), db, loadsim.Config{
Protocol: loadsim.ProtocolJT808,
Connections: 1000,
JT808PhoneBase: loadsim.DefaultJT808PhoneBase,
})
if err != nil {
t.Fatalf("cleanupJT808RegistrationsWithDB() error = %v", err)
}
if deleted != 1000 {
t.Fatalf("deleted = %d, want 1000", deleted)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}