feat(tailscale): opt-in tailnet monitoring per Spark - #43
Open
siraustin wants to merge 1 commit into
Open
Conversation
A Spark can be entirely healthy — SSH answering, GPU reporting, LLM serving —
while tailscaled has lost its session with the coordination server. Short
connections retry and survive, so every LAN-based check (including sparkDash's
own SSH liveness) keeps reporting "up" and is correct, but the box is
unreachable from anywhere off the LAN: phone on cellular, admin console, other
tailnet nodes. The dashboard looks right and the Spark is invisible.
Adds `tailscaleMonitoring` (opt-in per Spark, default off), a TailscaleProbe
that reads `tailscale status --json`, and a Tailnet card beside Network. It
reports the node's OWN `Self.Online` rather than a peer's possibly-stale view,
and surfaces Tailscale's `Health` messages so the card explains the failure
("hasn't received a network map from the coordination server in 2m7s") instead
of just flagging it.
Read-only: never runs tailscale up/down/login.
Notes:
- Default off, so nothing changes for users who don't run Tailscale.
- 30s poll (POLL_INTERVAL_TAILSCALE) — each poll is an SSH round-trip and
tailnet state moves slowly.
- Local Sparks under Docker: tailscaled's socket lives on the host, so the
probe enters the host mount namespace via nsenter, mirroring the existing
nvidia-smi approach in SystemCollector.
- No timestamps in the snapshot payload, preserving the byte-identical
broadcast dedupe.
- Parsing is an exported pure function (parseTailscaleStatus) so it is unit
tested without I/O, matching how summarizeComfyPrompt is tested.
Tests: 9 new (parser incl. wedged-netmap/expired-key/malformed cases, probe
default-shape on failure, error-clearing on recovery). Suite 97/97, tsc clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
A Spark can be entirely healthy — SSH answering, GPU reporting, LLM serving — while
tailscaledhas lost its session with the coordination server. Short connections retry and survive, so every LAN-based check keeps reporting up and is correct. But the box is unreachable from anywhere off the LAN: phone on cellular, the admin console, another tailnet node.This bit me this morning on a 7-Spark fleet. sparkDash said all 7 up. Three of them (
aria,nova,lynx) had been invisible on the tailnet for ~15 hours, logging this every ~2 minutes since 43 minutes after boot:540 occurrences on one node. Node keys were fine, DNS fine, MTU fine, routes fine, clocks fine. Just no control-plane session.
systemctl restart tailscaledfixed all three. The dashboard never had a reason to say anything, because nothing it measures was wrong.What this adds
Opt-in
tailscaleMonitoringper Spark (default off), aTailscaleProbereadingtailscale status --json, and a Tailnet card next to Network.The card, rendered on a live Spark (dark theme, native styling):
When the node reports itself off the tailnet the card goes red with OFF TAILNET and prints Tailscale's own
Healthstrings — so it explains the failure rather than just flagging it:Also surfaced:
BackendState(Running/Stopped/NeedsLogin), tailnet IP, DERP relay, version, and an expired-node-key warning (worth distinguishing, because a restart will not fix that one — it needs re-auth).Design notes
Self.Online, never a peer's view. During the incident my Mac'stailscale statusclaimednovawas online while nova's owntailscaledsaidOnline: false— one node's view of a peer can be stale, so peer state is never the verdict.POLL_INTERVAL_TAILSCALE). Each poll is an SSH round-trip and tailnet state moves slowly; 2s like the Comfy probe would be wasteful.tailscale up/down/login.sh -cin the container finds neither the CLI nor the socket. The probe enters the host mount namespace viansenter, mirroringSystemCollector._execOnHostfornvidia-smi, and falls back to plain exec when/host/procisn't mounted.SparkMonitor.snapshot().parseTailscaleStatus) so it unit-tests without I/O, matching howsummarizeComfyPrompt/estimateQueueEtaMsare tested. There's no existing DI seam forsshExec, so the two probe-level tests stub the instance's exec method; happy to switch to a constructor-injectedexecif you'd prefer a consistent pattern.Followed the
comfyMonitoringprecedent throughout:Boolean()coercion in_normalizeConfig, flag-gated probe + interval inSparkMonitor, mirrored flag insnapshot(), Edit Spark checkbox,validate.jsuntouched (nothing host/user-shaped).Testing
tsc --noEmitclean;vite buildclean.nsenterpath works.tailscaledon one node and watched the card degrade to an error and then clear on recovery.One honest gap: I did not capture the red OFF TAILNET state live. Reproducing it needs a wedged control session (not a stopped daemon), and I wasn't willing to wedge a production node to stage a screenshot. That state is covered by unit tests against captured real-world output.
Version
Bumped to 1.7.0 with README + CHANGELOG entries, since a new opt-in monitoring feature matched the 1.6.0 precedent. Happy to drop the bump if you'd rather own versioning — just say and I'll amend.