feat(platform): harden telemetry pipeline and unify Semi UI workspaces
This commit is contained in:
160
go/vehicle-gateway/internal/feichibridge/state.go
Normal file
160
go/vehicle-gateway/internal/feichibridge/state.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package feichibridge
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type VehicleState struct {
|
||||
LastRealtimeTime time.Time `json:"last_realtime_time,omitempty"`
|
||||
LastRealtimeHash string `json:"last_realtime_hash,omitempty"`
|
||||
LastRealtimeACKAt time.Time `json:"last_realtime_ack_at,omitempty"`
|
||||
LastSnapshotReissueTime time.Time `json:"last_snapshot_reissue_time,omitempty"`
|
||||
LastSnapshotReissueHash string `json:"last_snapshot_reissue_hash,omitempty"`
|
||||
LastSnapshotReissueACKAt time.Time `json:"last_snapshot_reissue_ack_at,omitempty"`
|
||||
BackfillCursor time.Time `json:"backfill_cursor,omitempty"`
|
||||
LastBackfillACKAt time.Time `json:"last_backfill_ack_at,omitempty"`
|
||||
}
|
||||
|
||||
func (s *StateStore) CommitSnapshotReissue(vin string, at time.Time, hash string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
current := s.state.Vehicles[vin]
|
||||
current.LastSnapshotReissueTime = at
|
||||
current.LastSnapshotReissueHash = hash
|
||||
current.LastSnapshotReissueACKAt = time.Now()
|
||||
s.state.Vehicles[vin] = current
|
||||
return s.saveLocked()
|
||||
}
|
||||
|
||||
type persistentState struct {
|
||||
Version int `json:"version"`
|
||||
PlatformSerial uint16 `json:"platform_serial"`
|
||||
Vehicles map[string]VehicleState `json:"vehicles"`
|
||||
}
|
||||
|
||||
type StateStore struct {
|
||||
mu sync.Mutex
|
||||
path string
|
||||
state persistentState
|
||||
}
|
||||
|
||||
func OpenStateStore(path string) (*StateStore, error) {
|
||||
store := &StateStore{
|
||||
path: path,
|
||||
state: persistentState{
|
||||
Version: 1,
|
||||
Vehicles: map[string]VehicleState{},
|
||||
},
|
||||
}
|
||||
encoded, err := os.ReadFile(path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return store, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read bridge state: %w", err)
|
||||
}
|
||||
if err := json.Unmarshal(encoded, &store.state); err != nil {
|
||||
return nil, fmt.Errorf("decode bridge state: %w", err)
|
||||
}
|
||||
if store.state.Vehicles == nil {
|
||||
store.state.Vehicles = map[string]VehicleState{}
|
||||
}
|
||||
return store, nil
|
||||
}
|
||||
|
||||
func (s *StateStore) Vehicle(vin string) VehicleState {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.state.Vehicles[vin]
|
||||
}
|
||||
|
||||
func (s *StateStore) CommitRealtime(vin string, at time.Time, hash string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
current := s.state.Vehicles[vin]
|
||||
current.LastRealtimeTime = at
|
||||
current.LastRealtimeHash = hash
|
||||
current.LastRealtimeACKAt = time.Now()
|
||||
s.state.Vehicles[vin] = current
|
||||
return s.saveLocked()
|
||||
}
|
||||
|
||||
func (s *StateStore) CommitBackfill(vin string, at time.Time) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
current := s.state.Vehicles[vin]
|
||||
current.BackfillCursor = at
|
||||
current.LastBackfillACKAt = time.Now()
|
||||
s.state.Vehicles[vin] = current
|
||||
return s.saveLocked()
|
||||
}
|
||||
|
||||
func (s *StateStore) AdvanceBackfillCursor(vin string, at time.Time) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
current := s.state.Vehicles[vin]
|
||||
current.BackfillCursor = at
|
||||
s.state.Vehicles[vin] = current
|
||||
return s.saveLocked()
|
||||
}
|
||||
|
||||
func (s *StateStore) NextPlatformSerial() (uint16, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.state.PlatformSerial++
|
||||
if s.state.PlatformSerial == 0 {
|
||||
s.state.PlatformSerial = 1
|
||||
}
|
||||
if err := s.saveLocked(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return s.state.PlatformSerial, nil
|
||||
}
|
||||
|
||||
func (s *StateStore) saveLocked() error {
|
||||
if err := os.MkdirAll(filepath.Dir(s.path), 0o750); err != nil {
|
||||
return fmt.Errorf("create bridge state directory: %w", err)
|
||||
}
|
||||
encoded, err := json.MarshalIndent(s.state, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
temp, err := os.CreateTemp(filepath.Dir(s.path), ".state-*.json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tempName := temp.Name()
|
||||
defer os.Remove(tempName)
|
||||
if err := temp.Chmod(0o600); err != nil {
|
||||
temp.Close()
|
||||
return err
|
||||
}
|
||||
if _, err := temp.Write(encoded); err != nil {
|
||||
temp.Close()
|
||||
return err
|
||||
}
|
||||
if err := temp.Sync(); err != nil {
|
||||
temp.Close()
|
||||
return err
|
||||
}
|
||||
if err := temp.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Rename(tempName, s.path); err != nil {
|
||||
return fmt.Errorf("replace bridge state: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hashBytes(value []byte) string {
|
||||
sum := sha256.Sum256(value)
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
Reference in New Issue
Block a user