diff --git a/docs/BACKEND-SERVER.md b/docs/BACKEND-SERVER.md index cea7f91..25aa0d2 100644 --- a/docs/BACKEND-SERVER.md +++ b/docs/BACKEND-SERVER.md @@ -20,7 +20,7 @@ Open http://127.0.0.1:8080. If local port 8080 is occupied, use `-L 8081:127.0.0 The runner configuration allows `/opt/test-mqtt-5/backend` in `container.valid_volumes`; only the deploy job requests that mount as `/runtime`. The prior runner configuration is backed up before this addition. Existing frontend job mounts remain unchanged. This workflow assumes that label continues to use the same server Docker daemon; moving the runner requires updating the deployment arrangement. -`deploy.sh` locks the runtime directory, validates Compose, preserves the previous image-tag file and Compose, deploys only backend, and checks container health for up to 60 seconds. Health requires the UI snapshot to report MQTT connected. Failed updates restore the previous release; a failed first deployment stops the backend. Images are retained for rollback. No host-wide image prune is run. Remote device return-path checks should still be performed after significant MQTT changes. +`deploy.sh` locks the runtime directory, validates Compose, preserves the previous image-tag file and Compose, deploys only backend, and checks container health for up to 60 seconds. Health requires `/healthz` to confirm current MQTT readiness, independent of bounded event-history eviction. Failed updates restore the previous release; a failed first deployment stops the backend. Images are retained for rollback. No host-wide image prune is run. Remote device return-path checks should still be performed after significant MQTT changes. ## Server operations diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 91c99cf..d8482cc 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -168,6 +168,7 @@ func Run(ctx context.Context, args []string, out, errOut io.Writer, sim bool) in var viewer *web.Viewer if cfg.Web.Enabled { viewer = web.New(cfg.Web, store, cfg.Deadlines.Shutdown) + viewer.SetReadiness(client.Ready) if err = viewer.Start(); err != nil { fmt.Fprintln(errOut, err) return 1 diff --git a/internal/web/server.go b/internal/web/server.go index c1d475d..deb94d7 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -40,6 +40,7 @@ type Snapshot struct { Compatibility []Compatibility `json:"compatibility"` } type Viewer struct { + ready func() bool cfg config.Web store *events.Store shutdown time.Duration @@ -60,10 +61,25 @@ func New(cfg config.Web, store *events.Store, shutdown time.Duration) *Viewer { mux.Handle("GET /assets/", http.StripPrefix("/assets/", http.FileServer(http.FS(sub)))) mux.HandleFunc("GET /api/snapshot", v.snapshot) mux.HandleFunc("GET /api/events", v.stream) + mux.HandleFunc("GET /healthz", v.health) v.handler = v.guard(mux) v.server = &http.Server{Handler: v.handler, ReadHeaderTimeout: 3 * time.Second, IdleTimeout: 20 * time.Second, MaxHeaderBytes: 8 << 10} return v } + +// SetReadiness must be called before Start. Readiness reflects live MQTT state, +// independent of connection events rolling out of the bounded viewer history. +func (v *Viewer) SetReadiness(ready func() bool) { v.ready = ready } + +func (v *Viewer) health(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Cache-Control", "no-store") + if v.ready == nil || !v.ready() { + http.Error(w, "MQTT not ready", http.StatusServiceUnavailable) + return + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"status":"ready"}`)) +} func (v *Viewer) Address() string { if v.listener == nil { return v.cfg.Address diff --git a/internal/web/server_test.go b/internal/web/server_test.go index 397dd3c..eeea12f 100644 --- a/internal/web/server_test.go +++ b/internal/web/server_test.go @@ -162,3 +162,19 @@ func TestReportedScenariosDoNotClaimHardwareCompatibility(t *testing.T) { t.Fatal("scenario aliases retained history") } } + +func TestHealthUsesCurrentMQTTReadiness(t *testing.T) { + v := testViewer() + ready := false + v.SetReadiness(func() bool { return ready }) + for _, want := range []int{503, 200, 503} { + ready = want == 200 + // No connection event retained: current readiness must still work. + r := httptest.NewRequest("GET", "http://127.0.0.1:8080/healthz", nil) + w := httptest.NewRecorder() + v.Handler().ServeHTTP(w, r) + if w.Code != want { + t.Fatalf("health status %d, want %d", w.Code, want) + } + } +} diff --git a/server/compose.yaml b/server/compose.yaml index 919e224..6557bfb 100644 --- a/server/compose.yaml +++ b/server/compose.yaml @@ -45,7 +45,7 @@ services: test: - CMD-SHELL - >- - wget -qO- http://127.0.0.1:18080/api/snapshot | grep -q '"connection":"connected"' + wget -qO- http://127.0.0.1:18080/healthz >/dev/null interval: 10s timeout: 4s retries: 6