Fuehre Offline-Schwelle fuer Server-Connectivity ein

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Jesko Anschütz 2026-03-22 18:25:01 +01:00
parent 2c780d3e60
commit a69135c0b9
4 changed files with 76 additions and 8 deletions

View file

@ -187,7 +187,7 @@ go run ./cmd/agent
1. Backend: einheitliches Fehlerformat und Routing-Grundstruktur anlegen 1. Backend: einheitliches Fehlerformat und Routing-Grundstruktur anlegen
2. Backend: Konfigurations- und App-Lifecycle stabilisieren 2. Backend: Konfigurations- und App-Lifecycle stabilisieren
3. Agent und Backend: den HTTP-Statuspfad als Grundlage fuer Identitaet, Persistenz und spaetere Admin-Vorschau erweitern 3. Agent und Backend: den HTTP-Statuspfad als Grundlage fuer Identitaet, Persistenz und spaetere Admin-Vorschau erweitern
4. Agent: danach einen expliziten `offline`-Zustand und weitere Connectivity-Schwellenlogik aufsetzen 4. Agent: danach MQTT-spezifische Reachability und feinere Connectivity-Schwellenlogik aufsetzen
5. Danach die Netzwerk-, Sync- und Kommandopfade schrittweise produktionsnah ausbauen 5. Danach die Netzwerk-, Sync- und Kommandopfade schrittweise produktionsnah ausbauen
Ergaenzt seit dem ersten Geruest: Ergaenzt seit dem ersten Geruest:
@ -199,7 +199,7 @@ Ergaenzt seit dem ersten Geruest:
- dateibasierte Agent-Konfiguration zusaetzlich zu Env-Overrides - dateibasierte Agent-Konfiguration zusaetzlich zu Env-Overrides
- strukturierte Agent-Logs mit internem Health-Snapshot und signalgesteuertem Shutdown - strukturierte Agent-Logs mit internem Health-Snapshot und signalgesteuertem Shutdown
- erster periodischer HTTP-Status-Reporter im Agent - erster periodischer HTTP-Status-Reporter im Agent
- Server-Connectivity-Zustand im Agent (`unknown`, `online`, `degraded`) auf Basis der Report-Ergebnisse - Server-Connectivity-Zustand im Agent (`unknown`, `online`, `degraded`, `offline`) auf Basis der Report-Ergebnisse
- lokales Compose-Grundgeruest fuer PostgreSQL und Mosquitto - lokales Compose-Grundgeruest fuer PostgreSQL und Mosquitto
## Arbeitsweise ## Arbeitsweise

View file

@ -47,10 +47,17 @@ Getrennt vom Lifecycle fuehrt der Agent fuer die Server-Erreichbarkeit aktuell d
- `unknown` vor dem ersten erfolgreichen oder fehlgeschlagenen Status-Report - `unknown` vor dem ersten erfolgreichen oder fehlgeschlagenen Status-Report
- `online` nach einem erfolgreich bestaetigten HTTP-Status-Report - `online` nach einem erfolgreich bestaetigten HTTP-Status-Report
- `degraded` nach einem fehlgeschlagenen HTTP-Status-Report - `degraded` nach einem fehlgeschlagenen HTTP-Status-Report
- `offline` nach wiederholten fehlgeschlagenen HTTP-Status-Reports
Damit bleibt der Lifecycle sauber von Netz- und Gegenstellenproblemen getrennt. Damit bleibt der Lifecycle sauber von Netz- und Gegenstellenproblemen getrennt.
Ein Report-Fehler stoppt den Agenten nicht, sondern veraendert nur den Connectivity-Zustand. Ein Report-Fehler stoppt den Agenten nicht, sondern veraendert nur den Connectivity-Zustand.
Aktuell gilt fuer diese Schwellenlogik bewusst einfach:
- erster Fehl-Report: `degraded`
- ab dem dritten aufeinanderfolgenden Fehl-Report: `offline`
- naechster erfolgreicher Report: Rueckkehr nach `online`
## Strukturierte Log-Ereignisse ## Strukturierte Log-Ereignisse
Der Agent emittiert in v1 mindestens diese Ereignisse: Der Agent emittiert in v1 mindestens diese Ereignisse:
@ -88,6 +95,6 @@ Nicht Teil dieser Stufe:
- Kommandos oder Sync-Status - Kommandos oder Sync-Status
Die erste Backend-Reachability-Pruefung ist in dieser Stufe bereits ueber den HTTP-Status-Report abgebildet. Die erste Backend-Reachability-Pruefung ist in dieser Stufe bereits ueber den HTTP-Status-Report abgebildet.
Ein expliziter `offline`-Zustand, MQTT-Reachability und weitergehende Schwellenlogik folgen spaeter. MQTT-Reachability und weitergehende Schwellenlogik folgen spaeter.
Diese Punkte folgen erst, wenn echte Netzwerk- und Sync-Funktionalitaet eingebaut wird. Diese Punkte folgen erst, wenn echte Netzwerk- und Sync-Funktionalitaet eingebaut wird.

View file

@ -24,8 +24,11 @@ const (
ConnectivityUnknown Connectivity = "unknown" ConnectivityUnknown Connectivity = "unknown"
ConnectivityOnline Connectivity = "online" ConnectivityOnline Connectivity = "online"
ConnectivityDegraded Connectivity = "degraded" ConnectivityDegraded Connectivity = "degraded"
ConnectivityOffline Connectivity = "offline"
) )
const offlineFailureThreshold = 3
type HealthSnapshot struct { type HealthSnapshot struct {
Status Status Status Status
ServerConnectivity Connectivity ServerConnectivity Connectivity
@ -46,6 +49,7 @@ type App struct {
mu sync.RWMutex mu sync.RWMutex
status Status status Status
serverConnectivity Connectivity serverConnectivity Connectivity
consecutiveReportFailures int
startedAt time.Time startedAt time.Time
lastHeartbeatAt time.Time lastHeartbeatAt time.Time
} }
@ -181,13 +185,18 @@ func (a *App) reportStatus(ctx context.Context) {
}) })
if err != nil { if err != nil {
a.mu.Lock() a.mu.Lock()
a.consecutiveReportFailures++
a.serverConnectivity = ConnectivityDegraded a.serverConnectivity = ConnectivityDegraded
if a.consecutiveReportFailures >= offlineFailureThreshold {
a.serverConnectivity = ConnectivityOffline
}
a.mu.Unlock() a.mu.Unlock()
a.logger.Printf("event=status_report_failed screen_id=%s error=%v", a.Config.ScreenID, err) a.logger.Printf("event=status_report_failed screen_id=%s error=%v", a.Config.ScreenID, err)
return return
} }
a.mu.Lock() a.mu.Lock()
a.consecutiveReportFailures = 0
a.serverConnectivity = ConnectivityOnline a.serverConnectivity = ConnectivityOnline
a.mu.Unlock() a.mu.Unlock()
a.logger.Printf("event=status_report_sent screen_id=%s", a.Config.ScreenID) a.logger.Printf("event=status_report_sent screen_id=%s", a.Config.ScreenID)

View file

@ -15,10 +15,16 @@ import (
type recordingReporter struct { type recordingReporter struct {
callCount int callCount int
err error err error
errs []error
} }
func (r *recordingReporter) Send(_ context.Context, _ statusreporter.Snapshot) error { func (r *recordingReporter) Send(_ context.Context, _ statusreporter.Snapshot) error {
r.callCount++ r.callCount++
if len(r.errs) > 0 {
err := r.errs[0]
r.errs = r.errs[1:]
return err
}
return r.err return r.err
} }
@ -236,3 +242,49 @@ func TestAppRunMarksServerConnectivityOnlineAfterSuccessfulReport(t *testing.T)
cancel() cancel()
<-errCh <-errCh
} }
func TestReportStatusMarksServerConnectivityOfflineAfterRepeatedFailures(t *testing.T) {
reporter := &recordingReporter{err: context.DeadlineExceeded}
application := newApp(config.Config{
ScreenID: "screen-offline",
ServerBaseURL: "http://127.0.0.1:8080",
MQTTBroker: "tcp://127.0.0.1:1883",
HeartbeatEvery: 30,
StatusReportEvery: 30,
}, log.New(&bytes.Buffer{}, "", 0), time.Now, reporter)
application.reportStatus(context.Background())
if got, want := application.Snapshot().ServerConnectivity, ConnectivityDegraded; got != want {
t.Fatalf("after first failure ServerConnectivity = %q, want %q", got, want)
}
application.reportStatus(context.Background())
application.reportStatus(context.Background())
if got, want := application.Snapshot().ServerConnectivity, ConnectivityOffline; got != want {
t.Fatalf("after repeated failures ServerConnectivity = %q, want %q", got, want)
}
}
func TestReportStatusRecoversFromOfflineToOnline(t *testing.T) {
reporter := &recordingReporter{errs: []error{context.DeadlineExceeded, context.DeadlineExceeded, context.DeadlineExceeded, nil}}
application := newApp(config.Config{
ScreenID: "screen-recover",
ServerBaseURL: "http://127.0.0.1:8080",
MQTTBroker: "tcp://127.0.0.1:1883",
HeartbeatEvery: 30,
StatusReportEvery: 30,
}, log.New(&bytes.Buffer{}, "", 0), time.Now, reporter)
application.reportStatus(context.Background())
application.reportStatus(context.Background())
application.reportStatus(context.Background())
if got, want := application.Snapshot().ServerConnectivity, ConnectivityOffline; got != want {
t.Fatalf("offline state = %q, want %q", got, want)
}
application.reportStatus(context.Background())
if got, want := application.Snapshot().ServerConnectivity, ConnectivityOnline; got != want {
t.Fatalf("recovered state = %q, want %q", got, want)
}
}