-
Notifications
You must be signed in to change notification settings - Fork 0
fix: reconnect Z21 UDP and fail /healthz when the station is down #66
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -366,7 +366,7 @@ func New(ctx context.Context, log *logrus.Logger, cfg Config) (*Daemon, error) { | |
| log.Info("dcc-bus ws metrics enabled") | ||
| } | ||
|
|
||
| wsSrv := ws.NewServer(ws.ServerConfig{ | ||
| wsCfg := ws.ServerConfig{ | ||
| Verifier: verifier, | ||
| Hub: hub, | ||
| Router: ws.NewRouterAdapter(router), | ||
|
|
@@ -386,7 +386,11 @@ func New(ctx context.Context, log *logrus.Logger, cfg Config) (*Daemon, error) { | |
| AllowedOrigins: cfg.AllowedOrigins, | ||
| Verifier: verifier, | ||
| }), | ||
| }) | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✨ [POSITIVE] Integrating the |
||
| if h, ok := st.(ws.StationHealth); ok { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✨ [POSITIVE] Adding the |
||
| wsCfg.StationHealth = h | ||
| } | ||
| wsSrv := ws.NewServer(wsCfg) | ||
|
|
||
| srv := &http.Server{ | ||
| Addr: net.JoinHostPort(cfg.BindAddr, strconv.Itoa(int(cfg.Port))), | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -82,6 +82,16 @@ type Server struct { | |
| // the daemon binds to loopback because the reverse proxy on | ||
| // loco-server already validates Origin). | ||
| AllowedOrigins []string | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✨ [POSITIVE] Defining the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✨ [POSITIVE] The introduction of the |
||
| // stationHealth is optional; Z21 reports UDP reachability so | ||
| // /healthz can fail closed while the station is reconnecting. | ||
| stationHealth StationHealth | ||
| } | ||
|
|
||
| // StationHealth is implemented by command-station drivers that can | ||
| // report whether the physical bus is reachable (Z21 serial heartbeat). | ||
| type StationHealth interface { | ||
| Reachable() bool | ||
| } | ||
|
|
||
| // ServerConfig collects the few knobs Server takes at construction. | ||
|
|
@@ -101,6 +111,8 @@ type ServerConfig struct { | |
| // ProgrammingEnabled opens the loco.cvRead / cvWrite / addrGet / | ||
| // addrSet frames. Off by default. | ||
| ProgrammingEnabled bool | ||
| // StationHealth, when set, is consulted by /healthz. | ||
| StationHealth StationHealth | ||
| } | ||
|
|
||
| // NewServer returns a ready-to-mount Server. Heartbeat and dead-man | ||
|
|
@@ -124,19 +136,20 @@ func NewServer(cfg ServerConfig) *Server { | |
| log = logrus.New() | ||
| } | ||
| return &Server{ | ||
| verifier: cfg.Verifier, | ||
| hub: cfg.Hub, | ||
| router: cfg.Router, | ||
| log: log, | ||
| layoutID: cfg.LayoutID, | ||
| csID: cfg.CommandStation, | ||
| speedSteps: steps, | ||
| heartbeatSecs: hb, | ||
| deadmanSecs: dms, | ||
| AllowedOrigins: cfg.AllowedOrigins, | ||
| metrics: cfg.Metrics, | ||
| slotsDiag: cfg.SlotsDiag, | ||
| verifier: cfg.Verifier, | ||
| hub: cfg.Hub, | ||
| router: cfg.Router, | ||
| log: log, | ||
| layoutID: cfg.LayoutID, | ||
| csID: cfg.CommandStation, | ||
| speedSteps: steps, | ||
| heartbeatSecs: hb, | ||
| deadmanSecs: dms, | ||
| AllowedOrigins: cfg.AllowedOrigins, | ||
| metrics: cfg.Metrics, | ||
| slotsDiag: cfg.SlotsDiag, | ||
| programmingEnabled: cfg.ProgrammingEnabled, | ||
| stationHealth: cfg.StationHealth, | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -160,13 +173,23 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |
| http.NotFound(w, r) | ||
| } | ||
| case "/healthz": | ||
| w.WriteHeader(http.StatusOK) | ||
| _, _ = w.Write([]byte(`{"status":"ok"}`)) | ||
| s.handleHealthz(w) | ||
| default: | ||
| http.NotFound(w, r) | ||
| } | ||
| } | ||
|
|
||
| func (s *Server) handleHealthz(w http.ResponseWriter) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✨ [POSITIVE] The refactoring of the |
||
| w.Header().Set("Content-Type", "application/json") | ||
| if s.stationHealth != nil && !s.stationHealth.Reachable() { | ||
| w.WriteHeader(http.StatusServiceUnavailable) | ||
| _, _ = w.Write([]byte(`{"status":"unhealthy","code":"station_unreachable"}`)) | ||
| return | ||
| } | ||
| w.WriteHeader(http.StatusOK) | ||
| _, _ = w.Write([]byte(`{"status":"ok"}`)) | ||
| } | ||
|
|
||
| // handleWS authenticates, upgrades, registers the session and runs | ||
| // the read loop until ctx ends or the client disconnects. | ||
| func (s *Server) handleWS(w http.ResponseWriter, r *http.Request) { | ||
|
|
@@ -250,8 +273,8 @@ func (s *Server) readLoop(ctx context.Context, sess *Session) { | |
| sess.Close(errors.WsCodeSessionReadLoopDone) | ||
| } | ||
| s.log.WithFields(logrus.Fields{ | ||
| "sessionId": sess.ID, | ||
| "userId": sess.UserID, | ||
| "sessionId": sess.ID, | ||
| "userId": sess.UserID, | ||
| "userSessionsRemaining": len(s.hub.SessionsForUser(sess.UserID)), | ||
| }).Info("dcc-bus session closed") | ||
| // Give the browser time to reconnect before firing the dead-man's | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| package ws | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✨ [POSITIVE] Adding dedicated unit tests for the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✨ [POSITIVE] Adding comprehensive unit tests for the |
||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "net/http" | ||
| "net/http/httptest" | ||
| "testing" | ||
| ) | ||
|
|
||
| type fakeStationHealth struct{ ok bool } | ||
|
|
||
| func (f fakeStationHealth) Reachable() bool { return f.ok } | ||
|
|
||
| func TestHealthzOKWhenNoStationHealth(t *testing.T) { | ||
| s := NewServer(ServerConfig{}) | ||
| rec := httptest.NewRecorder() | ||
| s.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/healthz", nil)) | ||
| if rec.Code != http.StatusOK { | ||
| t.Fatalf("status = %d, want 200", rec.Code) | ||
| } | ||
| } | ||
|
|
||
| func TestHealthzOKWhenStationReachable(t *testing.T) { | ||
| s := NewServer(ServerConfig{StationHealth: fakeStationHealth{ok: true}}) | ||
| rec := httptest.NewRecorder() | ||
| s.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/healthz", nil)) | ||
| if rec.Code != http.StatusOK { | ||
| t.Fatalf("status = %d, want 200", rec.Code) | ||
| } | ||
| } | ||
|
|
||
| func TestHealthzUnavailableWhenStationUnreachable(t *testing.T) { | ||
| s := NewServer(ServerConfig{StationHealth: fakeStationHealth{ok: false}}) | ||
| rec := httptest.NewRecorder() | ||
| s.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/healthz", nil)) | ||
| if rec.Code != http.StatusServiceUnavailable { | ||
| t.Fatalf("status = %d, want 503", rec.Code) | ||
| } | ||
| var body struct { | ||
| Status string `json:"status"` | ||
| Code string `json:"code"` | ||
| } | ||
| if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { | ||
| t.Fatalf("json: %v", err) | ||
| } | ||
| if body.Status != "unhealthy" || body.Code != "station_unreachable" { | ||
| t.Fatalf("body = %+v", body) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✨ [POSITIVE] The addition of
deploy-hubtargets provides a robust and atomic deployment mechanism for ARM64 binaries to a remote hub. This is a significant improvement for the project's operational efficiency and maintainability.