Просмотр исходного кода

feat: track runtime states

tags/v0.9.0
Jan Svabenik 4 месяцев назад
Родитель
Сommit
1dbe150675
4 измененных файлов: 74 добавлений и 8 удалений
  1. +4
    -0
      docs/API.md
  2. +4
    -1
      docs/pro-runtime-hardening-workboard.md
  3. +40
    -7
      internal/app/engine.go
  4. +26
    -0
      internal/app/runtime_state_test.go

+ 4
- 0
docs/API.md Просмотреть файл

@@ -15,6 +15,8 @@ Health check.
{"ok": true} {"ok": true}
``` ```


`engine.state` spiegelt jetzt die Runtime-State-Maschine wider (idle, arming, prebuffering, running, degraded, muted, faulted, stopping) und bietet eine erste beobachtbare Basis für Fault-Transitions.

--- ---


### `GET /status` ### `GET /status`
@@ -73,6 +75,8 @@ Live engine and driver telemetry. Only populated when TX is active.
} }
``` ```


`engine.state` spiegelt jetzt die Runtime-State-Maschine wider (idle, arming, prebuffering, running, degraded, muted, faulted, stopping) und bietet eine erste beobachtbare Basis für Fault-Transitions.

--- ---


### `GET /config` ### `GET /config`


+ 4
- 1
docs/pro-runtime-hardening-workboard.md Просмотреть файл

@@ -266,11 +266,14 @@ Generator/Upsampler und Hardwarewriter werden als getrennte Stufen mit kleinem,


# WS-02 — Explizite Runtime-State-Maschine und Fault-Handling # WS-02 — Explizite Runtime-State-Maschine und Fault-Handling
**Priorität:** P0 **Priorität:** P0
**Gesamtstatus:** TODO
**Gesamtstatus:** IN PROGRESS


## Ziel ## Ziel
Einführen eines klaren Betriebsmodells mit Fault-, Recovery- und Muted-Zuständen. Einführen eines klaren Betriebsmodells mit Fault-, Recovery- und Muted-Zuständen.


## Fortschritt
- EngineStats liefert das Runtime-State-Feld (`idle`, `arming`, `prebuffering`, `running`) und schafft eine beobachtbare Baseline für die nächste Fault-Maschine.

## Zielzustände laut Konzept ## Zielzustände laut Konzept
- `idle` - `idle`
- `arming` - `arming`


+ 40
- 7
internal/app/engine.go Просмотреть файл

@@ -38,6 +38,19 @@ func (s EngineState) String() string {
} }
} }


type RuntimeState string

const (
RuntimeStateIdle RuntimeState = "idle"
RuntimeStateArming RuntimeState = "arming"
RuntimeStatePrebuffering RuntimeState = "prebuffering"
RuntimeStateRunning RuntimeState = "running"
RuntimeStateDegraded RuntimeState = "degraded"
RuntimeStateMuted RuntimeState = "muted"
RuntimeStateFaulted RuntimeState = "faulted"
RuntimeStateStopping RuntimeState = "stopping"
)

func updateMaxDuration(dst *atomic.Uint64, d time.Duration) { func updateMaxDuration(dst *atomic.Uint64, d time.Duration) {
v := uint64(d) v := uint64(d)
for { for {
@@ -96,11 +109,12 @@ type Engine struct {
deviceRate float64 deviceRate float64
frameQueue *output.FrameQueue frameQueue *output.FrameQueue


mu sync.Mutex
state EngineState
cancel context.CancelFunc
startedAt time.Time
wg sync.WaitGroup
mu sync.Mutex
state EngineState
cancel context.CancelFunc
startedAt time.Time
wg sync.WaitGroup
runtimeState atomic.Value


chunksProduced atomic.Uint64 chunksProduced atomic.Uint64
totalSamples atomic.Uint64 totalSamples atomic.Uint64
@@ -177,7 +191,7 @@ func NewEngine(cfg cfgpkg.Config, driver platform.SoapyDriver) *Engine {
log.Printf("engine: same-rate mode — DSP@%dHz", cfg.FM.CompositeRateHz) log.Printf("engine: same-rate mode — DSP@%dHz", cfg.FM.CompositeRateHz)
} }


return &Engine{
engine := &Engine{
cfg: cfg, cfg: cfg,
driver: driver, driver: driver,
generator: offpkg.NewGenerator(cfg), generator: offpkg.NewGenerator(cfg),
@@ -187,6 +201,8 @@ func NewEngine(cfg cfgpkg.Config, driver platform.SoapyDriver) *Engine {
state: EngineIdle, state: EngineIdle,
frameQueue: output.NewFrameQueue(cfg.Runtime.FrameQueueCapacity), frameQueue: output.NewFrameQueue(cfg.Runtime.FrameQueueCapacity),
} }
engine.setRuntimeState(RuntimeStateIdle)
return engine
} }


func (e *Engine) SetChunkDuration(d time.Duration) { func (e *Engine) SetChunkDuration(d time.Duration) {
@@ -306,6 +322,7 @@ func (e *Engine) Start(ctx context.Context) error {
runCtx, cancel := context.WithCancel(ctx) runCtx, cancel := context.WithCancel(ctx)
e.cancel = cancel e.cancel = cancel
e.state = EngineRunning e.state = EngineRunning
e.setRuntimeState(RuntimeStateArming)
e.startedAt = time.Now() e.startedAt = time.Now()
e.wg.Add(1) e.wg.Add(1)
e.mu.Unlock() e.mu.Unlock()
@@ -321,6 +338,7 @@ func (e *Engine) Stop(ctx context.Context) error {
return nil return nil
} }
e.state = EngineStopping e.state = EngineStopping
e.setRuntimeState(RuntimeStateStopping)
e.cancel() e.cancel()
e.mu.Unlock() e.mu.Unlock()


@@ -336,6 +354,7 @@ func (e *Engine) Stop(ctx context.Context) error {


e.mu.Lock() e.mu.Lock()
e.state = EngineIdle e.state = EngineIdle
e.setRuntimeState(RuntimeStateIdle)
e.mu.Unlock() e.mu.Unlock()
return nil return nil
} }
@@ -359,7 +378,7 @@ func (e *Engine) Stats() EngineStats {
hasRecentLateBuffers := lateAlertAt > 0 && now.Sub(time.Unix(0, int64(lateAlertAt))) <= lateBufferIndicatorWindow hasRecentLateBuffers := lateAlertAt > 0 && now.Sub(time.Unix(0, int64(lateAlertAt))) <= lateBufferIndicatorWindow
ri := runtimeIndicator(queue.Health, hasRecentLateBuffers) ri := runtimeIndicator(queue.Health, hasRecentLateBuffers)
return EngineStats{ return EngineStats{
State: state.String(),
State: string(e.currentRuntimeState()),
ChunksProduced: e.chunksProduced.Load(), ChunksProduced: e.chunksProduced.Load(),
TotalSamples: e.totalSamples.Load(), TotalSamples: e.totalSamples.Load(),
Underruns: e.underruns.Load(), Underruns: e.underruns.Load(),
@@ -401,6 +420,7 @@ func runtimeAlert(queueHealth output.QueueHealth, recentLateBuffers bool) string
} }


func (e *Engine) run(ctx context.Context) { func (e *Engine) run(ctx context.Context) {
e.setRuntimeState(RuntimeStateRunning)
e.wg.Add(1) e.wg.Add(1)
go e.writerLoop(ctx) go e.writerLoop(ctx)
defer e.wg.Done() defer e.wg.Done()
@@ -530,3 +550,16 @@ func cloneFrame(src *output.CompositeFrame) *output.CompositeFrame {
Sequence: src.Sequence, Sequence: src.Sequence,
} }
} }

func (e *Engine) setRuntimeState(state RuntimeState) {
e.runtimeState.Store(state)
}

func (e *Engine) currentRuntimeState() RuntimeState {
if v := e.runtimeState.Load(); v != nil {
if rs, ok := v.(RuntimeState); ok {
return rs
}
}
return RuntimeStateIdle
}

+ 26
- 0
internal/app/runtime_state_test.go Просмотреть файл

@@ -0,0 +1,26 @@
package app

import (
"testing"

cfgpkg "github.com/jan/fm-rds-tx/internal/config"
"github.com/jan/fm-rds-tx/internal/platform"
)

func TestEngineRuntimeStateReporting(t *testing.T) {
e := NewEngine(cfgpkg.Default(), platform.NewSimulatedDriver(nil))

if got := e.Stats().State; got != string(RuntimeStateIdle) {
t.Fatalf("expected initial state idle, got %s", got)
}

e.setRuntimeState(RuntimeStatePrebuffering)
if got := e.Stats().State; got != string(RuntimeStatePrebuffering) {
t.Fatalf("expected prebuffering, got %s", got)
}

e.setRuntimeState(RuntimeStateRunning)
if got := e.currentRuntimeState(); got != RuntimeStateRunning {
t.Fatalf("currentRuntimeState mismatch: %s", got)
}
}

Загрузка…
Отмена
Сохранить