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:
parent
2c780d3e60
commit
a69135c0b9
4 changed files with 76 additions and 8 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue