Files
browser/plugins/examples/scum-server-plugin/companion/trajectory_collector.go
T

167 lines
5.0 KiB
Go

package companion
import (
"context"
"fmt"
"strings"
"sync"
"time"
)
type TrajectorySource interface {
ReadPositionRows(context.Context, int) ([]map[string]any, error)
ReadVehicleRows(context.Context, int) ([]map[string]any, error)
}
type TrajectoryStore interface {
EnsureSchema(context.Context) error
StorePositionRows(context.Context, string, []map[string]any, time.Time) (int, error)
StoreVehicleRows(context.Context, string, []map[string]any, time.Time) (int, error)
}
type TrajectoryCollectionReport struct {
CollectedAt time.Time
PositionRows int
VehicleRows int
StoredSamples int
Status string
Reason string
}
type TrajectoryCollector struct {
Source TrajectorySource
Store TrajectoryStore
ServerInstanceID string
Interval time.Duration
MaxRows int
Now func() time.Time
schemaOnce sync.Once
schemaErr error
}
func NewTrajectoryCollector(config Config, source TrajectorySource, store TrajectoryStore) *TrajectoryCollector {
return &TrajectoryCollector{
Source: source,
Store: store,
ServerInstanceID: config.Component.ServerInstanceID,
Interval: time.Duration(config.Trajectory.IntervalSeconds) * time.Second,
MaxRows: config.Trajectory.MaxRows,
}
}
func (collector *TrajectoryCollector) CollectOnce(ctx context.Context) (TrajectoryCollectionReport, error) {
if collector == nil || collector.Source == nil || collector.Store == nil || strings.TrimSpace(collector.ServerInstanceID) == "" {
return TrajectoryCollectionReport{}, fmt.Errorf("SCUM trajectory collector is not configured")
}
collector.schemaOnce.Do(func() { collector.schemaErr = collector.Store.EnsureSchema(ctx) })
if collector.schemaErr != nil {
return TrajectoryCollectionReport{}, collector.schemaErr
}
sampledAt := collector.clock()().UTC()
report := TrajectoryCollectionReport{CollectedAt: sampledAt, Status: "healthy"}
positions, err := collector.Source.ReadPositionRows(ctx, collector.MaxRows)
if err != nil {
report.Status, report.Reason = "degraded", "position collection failed"
return report, err
}
report.PositionRows = len(positions)
written, err := collector.Store.StorePositionRows(ctx, collector.ServerInstanceID, positions, sampledAt)
if err != nil {
report.Status, report.Reason = "degraded", "position storage failed"
return report, err
}
report.StoredSamples += written
vehicles, err := collector.Source.ReadVehicleRows(ctx, collector.MaxRows)
if err != nil {
report.Status, report.Reason = "degraded", "vehicle collection failed"
return report, err
}
report.VehicleRows = len(vehicles)
written, err = collector.Store.StoreVehicleRows(ctx, collector.ServerInstanceID, vehicles, sampledAt)
if err != nil {
report.Status, report.Reason = "degraded", "vehicle storage failed"
return report, err
}
report.StoredSamples += written
report.Reason = "raw world coordinates stored"
return report, nil
}
func (collector *TrajectoryCollector) Run(ctx context.Context, status *TrajectoryCollectionStatus) error {
interval := collector.Interval
if interval < time.Second {
interval = time.Duration(DefaultTrajectoryCollectionIntervalSecs) * time.Second
}
if report, err := collector.CollectOnce(ctx); status != nil {
status.Record(report, err)
} else if err != nil {
return err
}
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
report, err := collector.CollectOnce(ctx)
if status != nil {
status.Record(report, err)
continue
}
if err != nil {
return err
}
}
}
}
func (collector *TrajectoryCollector) clock() func() time.Time {
if collector.Now != nil {
return collector.Now
}
return time.Now
}
type TrajectoryCollectionStatus struct {
mu sync.Mutex
latest TrajectoryCollectionReport
err error
}
func (status *TrajectoryCollectionStatus) Record(report TrajectoryCollectionReport, err error) {
if status == nil {
return
}
status.mu.Lock()
defer status.mu.Unlock()
status.latest = report
status.err = err
}
func (status *TrajectoryCollectionStatus) HealthReport() HealthReport {
if status == nil {
return HealthReport{Status: "healthy", Reason: "typed companion dispatcher ready"}
}
status.mu.Lock()
defer status.mu.Unlock()
if status.latest.Status == "healthy" && status.err == nil {
return HealthReport{Status: "healthy", Reason: safeHealthReason(status.latest.Reason, "typed companion dispatcher ready")}
}
if !status.latest.CollectedAt.IsZero() && status.err == nil {
return HealthReport{Status: "healthy", Reason: "raw world coordinate collection ready"}
}
if status.err != nil {
return HealthReport{Status: "degraded", Reason: safeHealthReason(status.latest.Reason, "trajectory collection waiting for source data")}
}
return HealthReport{Status: "degraded", Reason: "trajectory collection waiting for first sample"}
}
func safeHealthReason(value string, fallback string) string {
value = strings.TrimSpace(value)
if value == "" {
return fallback
}
return value
}