Commit d66d58a
committed
Multi-site: keep external sites monitoring through tunnel outages, without data loss or UI hangs (#978)
* Agent: keep collecting through tunnel outages, replay buffered results on reconnect
Probe and SNMP runners now live for the whole agent process instead of
per tunnel connection, feeding a store-and-forward ResultBuffer (12 h /
64 MB caps, oldest dropped first). While the tunnel is down the agent
keeps polling on the last pushed config - previously the runners were
torn down with the connection and the outage window was simply lost.
On reconnect the backlog drains in FIFO order, coalescing consecutive
batches to cut server-side per-batch overhead and throttling to keep
channel headroom for heartbeats and proxied console traffic. Teardown
salvages unsent messages from the outbound channel so a drop or failed
connect loses nothing.
Server side: custom-OID results now land at their sample timestamps
instead of write time (a replayed backlog would have collapsed onto the
flush time), and samples older than 10 minutes skip the alert state
machines so a replayed down/up sequence doesn't fire alerts hours late.
Interface rates come out correct across the gap for free - the counter
delta cache persists through the outage and samples carry their own
timestamps.
* Tunnel liveness watchdogs: fail fast on black-holed agent tunnels
A WAN outage black-holes the tunnel TCP connection rather than
resetting it, and neither side noticed for the full TCP timeout
(~15 min). Server side that meant the registry kept the dead
connection, IsAgentOnline() stayed true, the console never flipped to
awaiting-agent, and every page of the site - including a site switch -
hung on proxy opens into the dead tunnel.
Server: drop a connection silent past 90 s (agents heartbeat every
30 s; LastMessageAt is stamped on every message). The drop runs the
normal teardown, so the awaiting-agent fail-fast now engages within
~2 min of a real outage instead of ~15.
Agent: force a reconnect when nothing arrives for 150 s (the server
re-pushes configs every 60 s, so a healthy tunnel is never that
quiet). Without this the agent's read loop also hung on dead TCP,
delaying the reconnect - and the buffered-backlog flush - by the same
~15 min. Forced reconnects lose nothing: the outbound channel is
salvaged and results keep buffering.
* Site-scope LAN flow map view state; reload PWA on stale resume
Two multi-site UX fixes:
Map view state (camera, overlays, scrub span, signal-hint dismissal)
was keyed by a fixed localStorage prefix shared across every site, so
switching sites inherited the previous site's 3D camera and 2D
overlays. SiteContextService.ScopeStorageKey now prefixes the key with
the site slug for non-default sites; the default site keeps the bare
key so existing saved state and single-site installs are untouched.
Applied at all four map mount sites (Monitoring 3D/2D, dashboard Live
View 3D/2D).
PWA resume: backgrounding the app on mobile suspends the tab and kills
its Blazor WebSocket, but on resume Blazor often doesn't surface the
reconnect modal, leaving the dashboard on its stale prerender with
frozen loading spinners until a manual refresh. A visibilitychange
handler now reloads when the tab was hidden past ~30 s and Blazor
isn't already handling the drop, so a long-backgrounded page recovers
on its own.
* Fast-fail proxy opens on a black-holed agent tunnel
A dead-but-registered tunnel (WAN black-hole, before the 90s server
watchdog drops it) left every console call dialing the loopback proxy
and blocking the full OpenTimeout for an answer that never comes, so a
site switch or page load froze the browser for up to ~90s.
- Refuse the proxy connect immediately when the agent has been silent
past 45s (well above the 30s heartbeat, below the 90s watchdog).
- Shorten OpenTimeout 10s to 3s; a live agent answers a ProxyOpen in
well under a second.
* 2D LAN flow map: keep bottom devices clear of the scrubber bar
_fitAll centered on the raw canvas midpoint, so the desktop scrubber
bar (which overlays the stage bottom) clipped the lowest devices.
Reserve the bar's height as a bottom inset and center in the
timeline-free region, lifting the map a touch. Mobile is unchanged
(the scrubber sits below the stage there).
* 2D LAN flow map: tighten the scrubber-bar clearance
The first pass reserved the bar height plus a breather plus the bottom
margin, leaving ~16px of dead space above the bar. Center in a proper
[top margin .. just above the bar] band with a 4px gap instead. Mobile
is unchanged (no overlay, so the band is symmetric top/bottom).
* Agent: bound the pre-hello handshake so a black-hole can't wedge it
The inbound-silence watchdog only arms after the server hello arrives,
so a link black-holed in the connect->hello window would hang the read
on dead TCP for the full ~15 min OS timeout before the reconnect loop
could retry. Cap the handshake at 20s and fail fast into a retry.
* Agent buffer: ack-based store-and-forward so black-holes don't lose data
The buffer dropped a message as soon as WriteAsync returned, but TCP
reports a write into a black-holed connection as success (the bytes sit
in the kernel send buffer and are discarded on teardown). So during a
WAN black-hole the buffer drained into the void and an outage's worth of
monitoring data was lost - confirmed by a DROP test: zero latency points
landed for the 2-minute outage window on either external site.
Now a result frame stays buffered until the SERVER acks it:
- Each frame carries a monotonic sequence; the server sends a cumulative
ResultAck once the batch is persisted.
- The drain only PEEKS frames (never removes on send); MarkAcked trims
the acked run, and an unacked frame replays from the oldest on
reconnect. This deletes the salvage machinery (SalvageUnsentInto,
_pumpInFlight, RequeueFront) - nothing to salvage when the buffer
already retains everything unacked.
Rolling-upgrade safe via a ServerHello.supports_result_ack capability: a
new agent against an older (non-acking) server falls back to trim-on-send
so it doesn't retain and re-flush its whole buffer every reconnect. Old
agent + new server is unaffected (sequence 0 => no ack).
* PWA resume: probe the circuit before reloading, don't reload on a timer
The visibilitychange handler reloaded whenever the tab was hidden past
30s, discarding scroll position and in-page state even when the Blazor
circuit had survived the backgrounding. Now it round-trips a lightweight
[JSInvokable] Ping on resume and only reloads when the circuit is
genuinely dead (the probe times out); a live circuit resumes seamlessly.
* Live WAN chart: overflow visible so it isn't clipped by the card
The base .card clips to its rounded corners (overflow hidden), cutting
off the live WAN chart's overhang on both the dashboard and the
Monitoring Live View. Scope overflow: visible to the WAN chart card and
its body.
* Proxy: circuit-breaker so a site switch during an outage doesn't hang
The liveness gate can't trip until 45s (it must stay above the 30s
heartbeat to avoid false-failing a healthy tunnel), so in the first ~45s
of a black-hole every proxy open still ate the full 3s OpenTimeout. A
site switch fires a burst of console + SSH opens that serialize, so the
switch hung ~15s until the gate caught up.
Now the first open timeout trips a per-site breaker; opens queued behind
it fast-fail instantly instead of each blocking OpenTimeout, collapsing
the burst to a single timeout. The breaker self-clears the moment the
tunnel produces fresh inbound (recovery) - keyed on LastMessageAt, so no
probe is needed - or when the 45s gate takes over.
* Proxy: hold the open breaker past the liveness gate, no mid-outage re-probe
The breaker's 15s hold expired mid-outage and let a fresh burst of opens
time out (3s each) before re-tripping - a ~10s stall on any site switch
that landed in that window, recurring every ~15s until the 45s gate took
over. Hold the breaker 60s so it stays tripped continuously until the
gate handles it; it still clears the instant fresh inbound arrives, so a
healthy tunnel is never held (its next heartbeat lets opens through).
* Live View: show a console-down banner instead of "not configured" on outage
A down console (agent-tunnel outage) made the gateway device fetch throw,
which flipped the whole panel to the "Monitoring not configured / set up
monitoring" empty state - wrong and confusing, since monitoring IS
configured and the console is just unreachable. Isolate that fetch so it
no longer blanks the panel, and surface a console-down / awaiting-agent
banner (with the connection's own message) so the staleness is explained.
* Live WAN chart: periodically re-pull history to fill post-outage/startup gaps
pollLive only appends the latest sample forward, and loadHistory ran only
on mount and tab-refocus - so data backfilled into Influx after the buffer
scrolled past that time (an outage's buffered samples replaying on
reconnect, or the cold-start gap before data first flowed) never appeared.
A 60s backfill timer re-pulls the window and merges it, keeping the newest
live samples ahead of the re-pulled history so the smooth live edge never
regresses. Live mode + foreground only; stopped during historic playback
and teardown.
* Console: fail fast on a black-holed agent tunnel instead of a 14s retry
A console reached via an agent tunnel is dialed through a loopback proxy.
When the tunnel is black-holed the proxy fast-fails the open, but the HTTP
client's transient-failure retry sits ABOVE the proxy and stacked the full
2+4+8s (~14s) exponential backoff onto every request - so a switch to that
site hung ~10-15s for the whole ~90s before the watchdog flipped the
console to awaiting-agent. Agent-proxied consoles (loopback 127.0.0.1) now
get a single quick retry; directly-connected consoles keep the full
backoff, so a real transient blip there is still ridden out.
* Console: flip to awaiting-agent on the proxy's dead-tunnel signal
Retry-scoping cut per-call cost from ~14s to ~0.5s, but a site switch
still ran ~12 console calls while the console reported connected (stale
green) during the ~90s before the watchdog flips it - so the switch still
took ~9s. Now the tunnel proxy, on the first open timeout of an outage,
tells the site's UniFiConnectionService to flip to awaiting-agent
immediately, so page renders short-circuit console calls at the
IsConnected guard instead of each dialing the dead proxy. The agent is
still registered so OnAgentTunnelDroppedAsync can't fire yet; this is a
separate path that skips that bail, is idempotent, and the agent-connected
hook re-establishes the console on recovery.
* Banners: dismiss app-wide nudges globally, not per-site
SystemSettingsService.GetAsync/SetAsync route to the CURRENT site's DB, so
a banner dismissed on one site reappeared when switching sites. The PWA
install nudge and the WiFi channel-analysis disclaimer are feature-level
(not site data), so dismiss them globally via GetGlobalAsync/SetGlobalAsync,
matching the prerelease-updates and iperf3-preferences convention.
Site-specific dismissals (upstream-discovery results, per-site speed-test
schedule nudges) stay per-site.
* Gate console wait/connect on tunnel LIVENESS, not registration
The real source of the slow site switch during a black-hole: every page
awaits WaitForConnectionAsync before rendering, and its only agent
early-out checked registration (IsAgentOnline). A black-holed tunnel
stays registered until the 90s watchdog, so the early-out never fired
and every page load polled the full 3s timeout - twice per navigation
(prerender + interactive) = ~9s per switch, ~13s with the first breaker
probe, and instant only once the watchdog unregistered the agent. The
proxy/retry fixes never moved this because the render was stalled in the
poll, not the dials.
- AgentTunnelConnection gains the shared staleness primitive
(StaleThreshold 45s / IsStale); the proxy's private copy is replaced.
- WaitForConnectionAsync bails immediately on IsAwaitingAgent and treats
a dead-but-registered tunnel as absent (HasLiveAgentTunnel).
- The connect paths and Test Connection use the same liveness check, so
reconnect attempts during the interim land in awaiting-agent instead
of dialing the dead proxy and misreporting an SSL error.
- The 60s config refresh no longer un-flips awaiting-agent mid-outage:
ReconnectConsoleIfViaAgentAsync skips a stale connection (the log
showed it "reconnecting its console" through the dead tunnel every
60s, resurrecting the delay the flip had just removed).
Registration-only checks remain only where they belong (teardown
bookkeeping in OnAgentTunnelDroppedAsync).
* Flip an outaged site's console proactively, not on first dial
The awaiting-agent flip only fired from the proxy-open TIMEOUT path, so a
site nobody touched during the outage never flipped: its opens were
refused by the stale gate (which didn't flip), its console stayed
stale-green, and the first switch to it paid a dial-plus-retry on every
console call - ~9s per page load - until the 90s watchdog unregistered
the agent. The user's own site flipped early only because its open pages
happened to dial it.
- The tunnel watchdog now flips the site's console the moment silence
crosses the stale threshold (checked every 15s), so every site of a
black-holed agent goes awaiting-agent by ~60s with no dial needed.
- The proxy's stale-gate refusal also fires the flip (belt-and-braces
for a dial landing between watchdog ticks).
- NoteProxyUnreachableAsync renamed NoteTunnelUnreachableAsync; the
proxy's two flip paths share one helper.
Coverage: 0-45s = breaker flip on first timeout (~3s); 45s+ = proactive
watchdog flip; dial-before-tick = stale-gate flip. Recovery unchanged
(agent reconnect re-establishes the console).
* Report awaiting-agent, not a bogus SSL error, when a connect hits a dead tunnel
A connect attempt against a black-holed tunnel collapses mid-TLS at the
loopback proxy and parsed as "SSL certificate error: enable Ignore SSL
Errors" - advice that can't apply to an agent-proxied console (those force
ignore-SSL; no certificate can match 127.0.0.1). The banner flashed that
error until the awaiting-agent flip corrected it.
After a failed connect, if the console is agent-routed and the proxy says
the tunnel is suspect (no agent, stale, or open-breaker tripped with no
fresh inbound), land in awaiting-agent with its message instead. A genuine
console-side failure over a healthy tunnel keeps its real error.
* Settings: show Ignore SSL Errors as forced-on for agent-connected consoles
The setting can't apply through the agent tunnel (the console is dialed at
a loopback proxy endpoint its certificate can never match), and the code
has always forced it on for that path. Show the checkbox checked+disabled
with verbiage explaining why, instead of implying the toggle does
something. Directly-connected consoles keep the normal toggle.
* Settings: trim the forced-on SSL help text1 parent 6ea5b45 commit d66d58a
26 files changed
Lines changed: 1371 additions & 119 deletions
File tree
- src
- NetworkOptimizer.AgentProtocol
- Protos
- NetworkOptimizer.Agent
- NetworkOptimizer.UniFi
- NetworkOptimizer.Web
- Components
- Pages
- Services
- wwwroot
- css
- js
- tests/NetworkOptimizer.AgentProtocol.Tests
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
59 | 59 | | |
60 | 60 | | |
61 | 61 | | |
| 62 | + | |
| 63 | + | |
62 | 64 | | |
63 | 65 | | |
64 | 66 | | |
| |||
381 | 383 | | |
382 | 384 | | |
383 | 385 | | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
384 | 398 | | |
385 | 399 | | |
386 | 400 | | |
| |||
401 | 415 | | |
402 | 416 | | |
403 | 417 | | |
| 418 | + | |
404 | 419 | | |
405 | 420 | | |
406 | 421 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
13 | | - | |
14 | | - | |
15 | | - | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
16 | 19 | | |
17 | 20 | | |
18 | 21 | | |
19 | | - | |
| 22 | + | |
20 | 23 | | |
21 | 24 | | |
22 | 25 | | |
| |||
27 | 30 | | |
28 | 31 | | |
29 | 32 | | |
30 | | - | |
| 33 | + | |
31 | 34 | | |
32 | 35 | | |
33 | 36 | | |
| |||
42 | 45 | | |
43 | 46 | | |
44 | 47 | | |
45 | | - | |
46 | | - | |
47 | | - | |
48 | | - | |
49 | | - | |
50 | | - | |
51 | | - | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
52 | 55 | | |
53 | 56 | | |
54 | 57 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
| 6 | + | |
6 | 7 | | |
7 | 8 | | |
8 | 9 | | |
| |||
185 | 186 | | |
186 | 187 | | |
187 | 188 | | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
188 | 209 | | |
189 | 210 | | |
190 | 211 | | |
191 | 212 | | |
192 | 213 | | |
193 | 214 | | |
194 | 215 | | |
195 | | - | |
196 | | - | |
197 | | - | |
198 | 216 | | |
199 | 217 | | |
200 | | - | |
201 | | - | |
202 | 218 | | |
203 | 219 | | |
204 | 220 | | |
205 | 221 | | |
206 | | - | |
207 | | - | |
| 222 | + | |
| 223 | + | |
208 | 224 | | |
209 | 225 | | |
210 | 226 | | |
| |||
215 | 231 | | |
216 | 232 | | |
217 | 233 | | |
218 | | - | |
219 | | - | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
220 | 245 | | |
221 | 246 | | |
222 | 247 | | |
| |||
233 | 258 | | |
234 | 259 | | |
235 | 260 | | |
236 | | - | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
237 | 264 | | |
238 | 265 | | |
239 | 266 | | |
| |||
270 | 297 | | |
271 | 298 | | |
272 | 299 | | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
273 | 308 | | |
274 | 309 | | |
275 | 310 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
11 | | - | |
12 | | - | |
13 | | - | |
14 | | - | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
15 | 17 | | |
16 | 18 | | |
17 | 19 | | |
18 | | - | |
| 20 | + | |
19 | 21 | | |
20 | 22 | | |
21 | 23 | | |
| |||
25 | 27 | | |
26 | 28 | | |
27 | 29 | | |
28 | | - | |
| 30 | + | |
29 | 31 | | |
30 | | - | |
| 32 | + | |
31 | 33 | | |
32 | 34 | | |
33 | 35 | | |
| |||
43 | 45 | | |
44 | 46 | | |
45 | 47 | | |
46 | | - | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
47 | 51 | | |
48 | 52 | | |
49 | 53 | | |
| |||
74 | 78 | | |
75 | 79 | | |
76 | 80 | | |
77 | | - | |
78 | | - | |
| 81 | + | |
79 | 82 | | |
80 | 83 | | |
81 | 84 | | |
| |||
131 | 134 | | |
132 | 135 | | |
133 | 136 | | |
134 | | - | |
| 137 | + | |
135 | 138 | | |
136 | 139 | | |
137 | 140 | | |
| |||
181 | 184 | | |
182 | 185 | | |
183 | 186 | | |
184 | | - | |
| 187 | + | |
185 | 188 | | |
186 | 189 | | |
187 | 190 | | |
| |||
0 commit comments