Skip to content

fix(api): keep unreachable content hosts out of the rendezvous rotation - #1016

Closed
dylanjeffers wants to merge 1 commit into
mainfrom
fix/exclude-unhealthy-content-hosts
Closed

fix(api): keep unreachable content hosts out of the rendezvous rotation#1016
dylanjeffers wants to merge 1 commit into
mainfrom
fix/exclude-unhealthy-content-hosts

Conversation

@dylanjeffers

Copy link
Copy Markdown
Contributor

What's broken

updateNodes builds the rendezvous rotation straight from the on-chain registry. Registration says a node is entitled to serve content — it doesn't say the box is up. The only way to exclude a dead node today is for a human to notice and ship a config.DeadNodes entry.

audius.zeogrid.com has been sitting in that gap: refusing connections, still registered as validator Id 99, still in the rotation.

$ curl https://audius.zeogrid.com/health_check
curl: (7) Failed to connect to audius.zeogrid.com port 443 after 35 ms

Measured against prod on 2026-08-11:

  • returned as the primary host on 9 of 25 sampled requests (36%) for gotamaX's cover photo
  • ~8% of sampled trending artists have it in their image host set

Why this looked like a data bug

The blobs are fine. Every affected image serves 200 from its mirrors — gotamaX changed their cover photo three times and all three writes landed correctly with distinct CIDs. Only the URL was bad, so the browser fell back to its cached copy and the user reported their new profile picture or album art "reverting" on refresh.

This is one of two causes behind the current batch of "my profile photo keeps reverting" support reports. The other is a genuine write-path bug, fixed separately in OpenAudio/go-openaudio#510.

The fix

The existing 1-minute nodesPoller now probes each registered node's /health_check and drops the ones that don't answer.

Guards against the obvious ways active health checking could make things worse:

Guard Why
3 consecutive failures before ejection (~3 min) A restart or blip shouldn't pull a node out from under live traffic
One success re-admits and resets the count Recovery shouldn't need a deploy
Fail open if >50% would be ejected Most probes failing at once means our network is the problem; emptying the rotation would break every asset URL we serve

This also removes the need to hand-maintain config.DeadNodes.

Tests

Six cases in content_host_health_test.go covering each guard: healthy nodes retained, ejection only after the threshold, re-admission on recovery with the counter reset, fail-open, empty registry, and deregistered endpoints being forgotten.

Full ./api/ suite passes (0 failures). TestSearch fails in my sandbox for want of Elasticsearch — verified identical on clean main, so unrelated.

🤖 Generated with Claude Code

updateNodes built the rendezvous rotation straight from the on-chain
registry. Registration says a node is entitled to serve content; it doesn't
say the box is up. The only way to exclude a dead node was for a human to
notice and ship a config.DeadNodes entry.

audius.zeogrid.com hit that gap: refusing connections since at least
2026-08-11, still registered as validator Id 99, still in the rotation. It
came back as the *primary* host on 9 of 25 sampled requests (36%) for
gotamaX's cover photo, and ~8% of sampled trending artists had it in their
host set.

The blobs were fine — every affected image served 200 from its mirrors. Only
the URL was bad, so the browser fell back to its cached copy and users
reported their new profile picture or album art "reverting" on refresh, with
the write having landed correctly. Support saw it as a data bug; it wasn't.

The 1-minute nodesPoller now probes each registered node's /health_check and
drops the ones that don't answer. Guards against the obvious ways this could
make things worse:

- A node must fail 3 consecutive probes (~3 min) before ejection, so a
  restart or a blip doesn't pull it out from under live traffic.
- One success re-admits it and resets the count.
- If ejecting would remove more than half the registry, the filter fails
  open and keeps everything. Most probes failing at once means this API's
  own network is the problem, and emptying the rotation would break every
  asset URL we serve.

This also removes the need to hand-maintain config.DeadNodes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@raymondjacobson

Copy link
Copy Markdown
Member

Don't love this change feels over engineered. We already have the store all hosts - why is it needed?

@dylanjeffers

Copy link
Copy Markdown
Contributor Author

You're right, and the premise I opened this on was wrong. Closing.

I checked the client path I should have checked before filing. useProfilePicture and useCoverPhoto both pass preloadImageFn: preload into useImageSize, and fetchWithFallback walks artwork.mirrors swapping the hostname until one loads — with preload rejecting on both onerror and a 5s timeout. There's a second recovery path via onError + getNextMirrorUrl for <img> failures too.

And your point about store-all is the load-bearing one: Select() unconditionally appends StoreAllNodes, so every asset is guaranteed a mirror that holds everything. A dead primary costs a failed request and a retry, not a broken image.

The one way this could still have bitten is that fetchWithFallback calls preloadImageFn?.(url) with optional chaining, so a caller that omits it silently gets no fallback — but both profile hooks pass it, so that's theoretical.

What I actually verified stands: zeogrid is down, still registered as validator Id 99, and came back as primary on 9 of 25 sampled requests. I just drew the wrong user-facing conclusion from it. #1017 is the one-liner adding it to config.DeadNodes instead.

Worth noting for the original support thread: this means the dead node never explained gotamaX. Their cover photo writes all landed and would have rendered via mirrors, and profile_picture_sizes was never submitted in any transaction — so they're the same client-side upload failure as the other reporter, still undiagnosed. OpenAudio/go-openaudio#510 is unaffected; different code path, and it stands on the 1,241 stuck rows and a test that reproduces the prod error.

dylanjeffers added a commit that referenced this pull request Aug 12, 2026
Diffing the eth registry (76) against core's non-jailed validator set (70)
turns up six nodes core has already concluded are gone. All six fail to
serve content:

  content.grassfed.network      connection refused (already listed)
  audius.zeogrid.com            connection refused
  cn0.mainnet.audiusindex.org   521 on health + content
  cn3.mainnet.audiusindex.org   521 on health + content
  cn4.mainnet.audiusindex.org   521 on health + content
  audius-nodes.com              health_check 200, 502 on every content CID

That last one is the interesting case: it passes a health check while
serving nothing, so the active-probing approach in #1016 would have waved it
through. Core's jailed flag caught it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants