Kaynağa Gözat

feat: track runtime states

tags/v0.9.0
Jan Svabenik 1 ay önce
ebeveyn
işleme
1dbe150675
4 değiştirilmiş dosya ile 74 ekleme ve 8 silme
  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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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)
}
}

Yükleniyor…
İptal
Kaydet