mirror of
https://github.com/fosrl/newt.git
synced 2026-07-16 03:46:25 -05:00
[GH-ISSUE #411] Health check config collides between two targets sharing the same backend host:port on reconnect/reload #8718
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @gravis on GitHub (Jul 14, 2026).
Original GitHub issue: https://github.com/fosrl/newt/issues/411
Describe the Bug
When two targets point at the same backend
host:portbut have different Expected Response Codes, a websocket reconnect / config reload cross-contaminates their health-check configuration: one target inherits the other's expected status code and is then reported unhealthy forever even though it responds correctly.This is distinct from #240 (which reverts custom codes to the 2xx default). Here the code does not revert to 2xx — it is overwritten with the other target's custom code.
Setup that triggers it:
GET /manifest.json) → backend10.0.0.10:8123GET /api/mcp) → same backend10.0.0.10:8123On a clean start both load correctly and are healthy. After a websocket drop + reconnect, targets are re-registered (
Replacing existing target with ID ...) and Target A ends up expecting 401 (Target B's code). Target A then logsunexpected status code: 200 (expected: 401)every health-check interval and shows Unhealthy, even though 200 is its correct/expected response.A clean
newtrestart fixes it (both targets healthy again) — but the next reconnect can re-trigger it.Environment
To Reproduce
host:port, with different Expected Response Codes (e.g. Target A → 200, Target B → 401).Target X initial status: healthy.not connectedthenWebsocket connected).Replacing existing target with ID A/ID B, then flips one target toinitial status: unhealthy.Target A: unexpected status code: 200 (expected: 401)on every interval and stays Unhealthy until newt is restarted.Actual log (reconnect race, IDs/IPs genericized)
The
initial status: healthy→Replacing existing target→initial status: unhealthysequence for Target A within the same second strongly suggests the re-registration keys health-check config by backend address, so the second target (B) sharing10.0.0.10:8123overwrites Target A's expected code.Expected Behavior
On reconnect/reload, each target should retain its own configured Expected Response Code regardless of whether another target shares the same backend
host:port.Workaround
Restart newt for a clean reload (both targets load correct codes), or avoid two targets sharing an identical backend
host:portfor health checks (disable the health check on one, or differentiate backends). Both are temporary — the collision returns on the next reconnect.@gravis commented on GitHub (Jul 14, 2026):
Update after tracing this end-to-end in the Newt source: this is not a Newt bug — moving it to the server. Filed as fosrl/pangolin#3444.
Newt stores each health-check target's expected code by target ID (
healthcheck.Config.Status) and applies whatever the server sends; it never derives, mutates, or keys the expected code by backend address. I confirmed the reconnect "Replacing existing target" path preserves each target's own expected code (reproduced with a unit test).The real trigger is a server payload disagreement on reconnect: the
newt/syncmessage carries the target's correct expected code (200), but thenewt/wg/connectregistration message that arrives right after carries the other co-located resource's code (401), and it overwrites the good value. Details + logs in pangolin#3444.Separately, while investigating I did find a genuine (unrelated) data race in
healthcheck.go:monitorTarget/performHealthCheckmutate a target's status fields with no lock whilegetAllTargetsUnsafesnapshots the whole struct — confirmed with-race. Happy to open a small PR for that if useful. Closing this issue in favor of pangolin#3444.@gravis commented on GitHub (Jul 14, 2026):
Reopening with the correct root cause — I was wrong twice above (it is neither a server-side payload bug nor a host:port keying bug). It is a client bug in
handleConnect, and I've reproduced it with a unit test.handleConnectdecodes thenewt/wg/connectpayload into the persistentn.wgData:Go's
encoding/jsonmerges a JSON array into an existing slice by position, and a JSONnullis a no-op for a non-pointerint(healthcheck.Config.Status). On reconnect the health-check rows come back in a different order (the server's config query has noORDER BY), so a target withhcStatus: null(no custom expected code) lands on the slice slot previously held by another target and inherits its stale expected code — e.g. a/manifest.jsontarget that should accept any 2xx inherits a sibling's401and is then reported Unhealthy forever until restart.handleSyncis immune because it decodes into a fresh localSyncData; clean boot is immune becausewgDatastarts empty. Fix + regression test in #413 (decode into a freshWgData, assign on success).@AstralDestiny commented on GitHub (Jul 14, 2026):
Are the repro steps different or just the final root cause different?