Trace terminal recovery and isolate neighbor resolver latency

This commit is contained in:
lda committed 2026-10-10 04:11:41 +07:00 Verified
1 parent 74c9b9c33d
commit a2f39bd3c2
7 files changed
+84 -3

No files matched your search

Generated
+1
View File
@@ -3692,6 +3692,7 @@ dependencies = [
"serde_json", "serde_json",
"serial_test", "serial_test",
"tokio", "tokio",
"tracing",
"wakey-core", "wakey-core",
] ]
+17
View File
@@ -103,6 +103,23 @@ sum by (job, instance) (increase(wakey_terminal_backpressure_total[15m]))
This counts encounters, not their duration and not dropped chunks. A burst of This counts encounters, not their duration and not dropped chunks. A burst of
image output can raise it while output still arrives intact through backpressure. image output can raise it while output still arrives intact through backpressure.
Average duration of completed backpressure episodes, in seconds:
```promql
sum by (job, instance, outcome) (
rate(wakey_operation_duration_seconds_sum{operation="terminal_backpressure_wait"}[5m])
)
/
sum by (job, instance, outcome) (
rate(wakey_operation_duration_seconds_count{operation="terminal_backpressure_wait"}[5m])
)
```
An episode starts when the output channel is full and ends when the pending
backlog drains or the relay disconnects. It is not per-frame network latency;
unfinished episodes do not appear until they end. Keep the outcome grouping so
disconnected episodes are not confused with successfully drained ones.
## Snapshots ## Snapshots
Device entries in successfully sent snapshots over the last hour: Device entries in successfully sent snapshots over the last hour:
+6
View File
@@ -100,6 +100,12 @@ existing debug events too; inspect them before enabling it broadly.
with backoff and CC disconnect duration. Sending credentials is not an auth ACK; with backoff and CC disconnect duration. Sending credentials is not an auth ACK;
CC's authentication event is the authoritative result. DNS diagnostics are a CC's authentication event is the authoritative result. DNS diagnostics are a
separate lookup, not a measurement of the resolver inside WebSocket connect. separate lookup, not a measurement of the resolver inside WebSocket connect.
- Terminal recovery uses `terminal_snapshot_generate` (encoded byte count),
transport-attachment spans, and `terminal_snapshot_request`. These measure
local work, not browser restoration or end-to-end image replay. Resume operation
success means credentials were queued for the worker. `terminal_backpressure_wait`
operation durations cover one full-queue backlog episode until it drains (`ok`)
or disconnects (`disconnected`); cancellation can retain the default `error` outcome.
Instrument operation boundaries rather than individual rows or terminal chunks. Instrument operation boundaries rather than individual rows or terminal chunks.
Use `skip_all`, explicitly allow count fields, and instrument async futures rather Use `skip_all`, explicitly allow count fields, and instrument async futures rather
+35 -2
View File
@@ -35,6 +35,7 @@ struct RelayOutputBuffer {
sender: Option<mpsc::Sender<Message>>, sender: Option<mpsc::Sender<Message>>,
pending: VecDeque<Message>, pending: VecDeque<Message>,
snapshot_pending: bool, snapshot_pending: bool,
backpressure_wait: Option<wakey_core::telemetry::Operation>,
} }
impl RelayOutputBuffer { impl RelayOutputBuffer {
@@ -44,6 +45,9 @@ impl RelayOutputBuffer {
} }
fn disconnect(&mut self) { fn disconnect(&mut self) {
if let Some(mut wait) = self.backpressure_wait.take() {
wait.outcome("disconnected");
}
self.sender = None; self.sender = None;
self.pending.clear(); self.pending.clear();
self.snapshot_pending = false; self.snapshot_pending = false;
@@ -80,6 +84,10 @@ impl RelayOutputBuffer {
match sender.try_send(frame) { match sender.try_send(frame) {
Ok(()) => {} Ok(()) => {}
Err(mpsc::error::TrySendError::Full(frame)) => { Err(mpsc::error::TrySendError::Full(frame)) => {
// One measurement per backlog episode, not per select poll.
self.backpressure_wait = Some(wakey_core::telemetry::Operation::new(
"terminal_backpressure_wait",
));
wakey_core::telemetry::count( wakey_core::telemetry::count(
wakey_core::telemetry::Counter::TerminalBackpressure, wakey_core::telemetry::Counter::TerminalBackpressure,
1, 1,
@@ -111,6 +119,9 @@ impl RelayOutputBuffer {
permit.send(frame); permit.send(frame);
if self.pending.is_empty() { if self.pending.is_empty() {
self.snapshot_pending = false; self.snapshot_pending = false;
if let Some(mut wait) = self.backpressure_wait.take() {
wait.outcome("ok");
}
} }
} }
@@ -134,10 +145,14 @@ impl TerminalState {
self.parser.screen_mut().set_size(rows, cols); self.parser.screen_mut().set_size(rows, cols);
} }
#[tracing::instrument(name = "terminal_snapshot_generate", skip_all, fields(bytes = tracing::field::Empty))]
fn snapshot(&self) -> Vec<u8> { fn snapshot(&self) -> Vec<u8> {
self.parser let snapshot = self
.parser
.screen() .screen()
.snapshot_formatted(TERMINAL_SCROLLBACK_ROWS) .snapshot_formatted(TERMINAL_SCROLLBACK_ROWS);
tracing::Span::current().record("bytes", snapshot.len());
snapshot
} }
} }
@@ -262,6 +277,7 @@ impl TerminalManager {
#[tracing::instrument(name = "terminal_resume", skip_all, fields(terminal_id = %terminal_id))] #[tracing::instrument(name = "terminal_resume", skip_all, fields(terminal_id = %terminal_id))]
pub fn resume(&self, terminal_id: &TerminalId, relay_token: String) -> Result<()> { pub fn resume(&self, terminal_id: &TerminalId, relay_token: String) -> Result<()> {
let mut operation = wakey_core::telemetry::Operation::new("terminal_resume");
let active = self.active.lock().expect("terminal manager poisoned"); let active = self.active.lock().expect("terminal manager poisoned");
let session = active let session = active
.get(terminal_id.as_str()) .get(terminal_id.as_str())
@@ -269,6 +285,8 @@ impl TerminalManager {
if session.relay_credentials.send(relay_token).is_err() { if session.relay_credentials.send(relay_token).is_err() {
anyhow::bail!("terminal session {terminal_id} has stopped"); anyhow::bail!("terminal session {terminal_id} has stopped");
} }
operation.outcome("ok");
info!("terminal resume credentials queued for worker");
Ok(()) Ok(())
} }
@@ -1190,6 +1208,7 @@ mod tests {
assert!(relay.is_connected()); assert!(relay.is_connected());
assert!(relay.has_pending()); assert!(relay.has_pending());
assert!(relay.backpressure_wait.is_some());
assert_eq!( assert_eq!(
rx.recv().await.expect("original queued frame").into_data(), rx.recv().await.expect("original queued frame").into_data(),
vec![1] vec![1]
@@ -1203,6 +1222,20 @@ mod tests {
vec![2] vec![2]
); );
assert!(!relay.has_pending()); assert!(!relay.has_pending());
assert!(relay.backpressure_wait.is_none());
}
#[test]
fn disconnect_finishes_backpressure_episode() {
let (tx, _rx) = mpsc::channel(1);
let mut relay = RelayOutputBuffer::default();
relay.connect(tx);
relay.queue_live(Message::Binary(vec![1].into()));
relay.queue_live(Message::Binary(vec![2].into()));
assert!(relay.backpressure_wait.is_some());
relay.disconnect();
assert!(relay.backpressure_wait.is_none());
assert!(!relay.has_pending());
} }
#[tokio::test] #[tokio::test]
+16 -1
View File
@@ -7,7 +7,7 @@ use futures_util::{SinkExt, StreamExt};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::time::Duration; use std::time::Duration;
use tokio::sync::broadcast; use tokio::sync::broadcast;
use tracing::{info, warn}; use tracing::{Instrument, info, warn};
use wakey_agent::protocol::{ use wakey_agent::protocol::{
AgentCapability, DEFAULT_TERMINAL_MAX_SESSIONS, DEFAULT_TERMINAL_SESSION_TTL_SECONDS, AgentCapability, DEFAULT_TERMINAL_MAX_SESSIONS, DEFAULT_TERMINAL_SESSION_TTL_SECONDS,
ServerMessage, TerminalAgentHandshake, TerminalControl, TerminalId, TerminalOperatorHandshake, ServerMessage, TerminalAgentHandshake, TerminalControl, TerminalId, TerminalOperatorHandshake,
@@ -307,10 +307,17 @@ async fn handle_agent_terminal_socket(state: AppState, terminal_id: String, mut
let (mut outbound, pending) = match state let (mut outbound, pending) = match state
.terminals .terminals
.attach_agent(&terminal_id, &auth.0, &auth.1) .attach_agent(&terminal_id, &auth.0, &auth.1)
.instrument(
tracing::info_span!("terminal_agent_transport_attach", terminal_id = %terminal_id),
)
.await .await
{ {
Ok(outbound) => outbound, Ok(outbound) => outbound,
Err(code) => { Err(code) => {
warn!(
terminal_id,
code, "agent terminal transport attachment rejected"
);
close_socket(&mut socket, code).await; close_socket(&mut socket, code).await;
return; return;
} }
@@ -393,10 +400,17 @@ async fn handle_operator_terminal_socket(
let mut outbound = match state let mut outbound = match state
.terminals .terminals
.attach_operator(&terminal_id, &auth.0, &auth.1) .attach_operator(&terminal_id, &auth.0, &auth.1)
.instrument(
tracing::info_span!("terminal_operator_transport_attach", terminal_id = %terminal_id),
)
.await .await
{ {
Ok(attached) => attached, Ok(attached) => attached,
Err(code) => { Err(code) => {
warn!(
terminal_id,
code, "operator terminal transport attachment rejected"
);
close_socket(&mut socket, code).await; close_socket(&mut socket, code).await;
return; return;
} }
@@ -407,6 +421,7 @@ async fn handle_operator_terminal_socket(
if let Err(code) = state if let Err(code) = state
.terminals .terminals
.relay_to_agent(&terminal_id, TerminalRelayFrame::Text(snapshot)) .relay_to_agent(&terminal_id, TerminalRelayFrame::Text(snapshot))
.instrument(tracing::info_span!("terminal_snapshot_request", terminal_id = %terminal_id))
.await .await
{ {
warn!(terminal_id, code, "failed to request terminal snapshot"); warn!(terminal_id, code, "failed to request terminal snapshot");
+1
View File
@@ -11,6 +11,7 @@ macaddr = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
tokio = { workspace = true, features = ["net", "sync"] } tokio = { workspace = true, features = ["net", "sync"] }
tracing = "0.1"
wakey-core = { path = "../wakey-core", registry = "gitea", version = "0"} wakey-core = { path = "../wakey-core", registry = "gitea", version = "0"}
[dev-dependencies] [dev-dependencies]
+8
View File
@@ -3,9 +3,11 @@ use futures::future::try_join_all;
use lda_ipjs::subcommands::neighbor; use lda_ipjs::subcommands::neighbor;
use std::collections::HashSet; use std::collections::HashSet;
use std::net::IpAddr; use std::net::IpAddr;
use tracing::Instrument;
use wakey_core::{InventoryQuery, NeighborEntry, NeighborState, Query}; use wakey_core::{InventoryQuery, NeighborEntry, NeighborState, Query};
/// Resolve a hostname through the local resolver and return all reported IPs. /// Resolve a hostname through the local resolver and return all reported IPs.
#[tracing::instrument(name = "neighbor_hostname_resolve", skip_all)]
pub async fn get_ips(machine_name: &str) -> Result<impl Iterator<Item = IpAddr>> { pub async fn get_ips(machine_name: &str) -> Result<impl Iterator<Item = IpAddr>> {
Ok(tokio::net::lookup_host((machine_name, 0)) Ok(tokio::net::lookup_host((machine_name, 0))
.await .await
@@ -54,6 +56,10 @@ pub async fn get_neighbors(
#[cfg(unix)] #[cfg(unix)]
{ {
let results = neighbor::nl::get(&ip_filter, &dev_strs, &nud_filter, macs) let results = neighbor::nl::get(&ip_filter, &dev_strs, &nud_filter, macs)
.instrument(tracing::info_span!(
"neighbor_table_read",
backend = "netlink"
))
.await .await
.context("rtnetlink failed")? .context("rtnetlink failed")?
.into_iter() .into_iter()
@@ -68,6 +74,7 @@ pub async fn get_neighbors(
if ip_filter.is_empty() { if ip_filter.is_empty() {
let dev = dev_strs.first().copied(); let dev = dev_strs.first().copied();
results = neighbor::get_with_backend(neighbor::Backend::Json, None, dev, &nud_filter) results = neighbor::get_with_backend(neighbor::Backend::Json, None, dev, &nud_filter)
.instrument(tracing::info_span!("neighbor_table_read", backend = "json"))
.await .await
.context("ip -j neigh failed")? .context("ip -j neigh failed")?
.into_iter() .into_iter()
@@ -82,6 +89,7 @@ pub async fn get_neighbors(
dev, dev,
&nud_filter, &nud_filter,
) )
.instrument(tracing::info_span!("neighbor_table_read", backend = "json"))
.await .await
.with_context(|| format!("ip -j neigh failed for {ip}"))? .with_context(|| format!("ip -j neigh failed for {ip}"))?
.into_iter() .into_iter()