peerLinks.list blocks on the slowest linked environment's reachability probe (10 s when a peer sleeps)#28531

opened10d agobyNniieani

Summary

peerLinks.list (and the t3_environment_links MCP tool built on it) probes every linked environment's /.well-known/t3/environment before answering, so a single asleep or offline peer makes the whole list take the full probe timeout (10 s for non-loopback origins). A client that only wants the inventory — environment ids, labels, URLs, expiry — waits on the slowest machine.

Reproduction

  1. Link four peers over the tailnet (http://<host>.<tailnet>.ts.net:3773).
  2. Put one of them (a laptop) to sleep.
  3. Call peerLinks.list over the WebSocket RPC.

Server trace from the hub (0.1.6-nightly.20261031.3956):

1ws.rpc.peerLinks.list 10007 ms Success
2 sqlite SELECT * FROM peer_environment_links 0 ms
3 PeerLinks.toLink ×4 0 ms
4 GET dev-linux…/.well-known/t3/environment 13 ms
5 GET dev-mac…/.well-known/t3/environment 15 ms
6 GET bazylis-macbook-pro-max…/.well-known/… 100 ms
7 GET bb-mbp-m5…/.well-known/t3/environment 10003 ms Interrupted

The same call takes 17–25 ms when every peer is awake.

Impact

A client with its own 10 s RPC deadline sees the hub itself as unresponsive whenever any laptop sleeps, and blames the wrong machine. Every caller of list pays the slowest peer's latency, even when it does not read status.

Cause

PeerLinks.list in apps/server/src/peer/PeerLinks.ts runs statusOf(link, now) for every link inside the request (Effect.forEach(..., { concurrency: "unbounded" })). statusOf does a live descriptor probe bounded by probeTimeout (10 s off-loopback).

Suggested fix

Answer list from stored state instead of a live probe: the rows already carry last_reached_at / last_error, so the status can come from the most recent probe, with probing done in the background or on demand. If a live status is still wanted, a separate per-link probe call — or a shorter probe budget with the stored status as fallback — keeps the inventory read fast and bounded.