irc: client-side ping timeout and connection reliability
- Send PING after 120s of silence, disconnect after 30s with no PONG - Reset backoff to base when a working connection drops (was registered) - Validate channel membership before sending to channels The ping timeout catches silent disconnects where the TCP connection stays open but OFTC has dropped us. Previously we'd sit "connected" indefinitely receiving nothing.
This commit is contained in:
parent
a77609c025
commit
7ed6d8622c
1 changed files with 46 additions and 1 deletions
|
|
@ -23,6 +23,8 @@ use tracing::{error, info, warn};
|
|||
const MAX_LOG_LINES: usize = 200;
|
||||
const RECONNECT_BASE_SECS: u64 = 5;
|
||||
const RECONNECT_MAX_SECS: u64 = 300;
|
||||
const PING_INTERVAL_SECS: u64 = 120;
|
||||
const PING_TIMEOUT_SECS: u64 = 30;
|
||||
|
||||
/// Parsed IRC message.
|
||||
struct IrcMessage {
|
||||
|
|
@ -208,8 +210,14 @@ async fn connection_loop(
|
|||
}
|
||||
}
|
||||
|
||||
// Reset backoff if we had a working connection (registered
|
||||
// successfully before disconnecting)
|
||||
let was_connected = state.borrow().connected;
|
||||
state.borrow_mut().connected = false;
|
||||
state.borrow_mut().writer = None;
|
||||
if was_connected {
|
||||
backoff = RECONNECT_BASE_SECS;
|
||||
}
|
||||
|
||||
// Persist current channel list to config
|
||||
{
|
||||
|
|
@ -274,11 +282,39 @@ async fn register_and_read<R: tokio::io::AsyncRead + Unpin>(
|
|||
}
|
||||
|
||||
let mut buf = Vec::new();
|
||||
let mut ping_sent = false;
|
||||
let mut deadline = tokio::time::Instant::now()
|
||||
+ std::time::Duration::from_secs(PING_INTERVAL_SECS);
|
||||
|
||||
loop {
|
||||
buf.clear();
|
||||
let n = reader.read_until(b'\n', &mut buf).await?;
|
||||
|
||||
let read_result = tokio::select! {
|
||||
result = reader.read_until(b'\n', &mut buf) => result,
|
||||
_ = tokio::time::sleep_until(deadline) => {
|
||||
if ping_sent {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::TimedOut,
|
||||
"ping timeout — no response from server",
|
||||
));
|
||||
}
|
||||
info!("irc: no data for {}s, sending PING", PING_INTERVAL_SECS);
|
||||
state.borrow_mut().send_raw("PING :keepalive").await?;
|
||||
ping_sent = true;
|
||||
deadline = tokio::time::Instant::now()
|
||||
+ std::time::Duration::from_secs(PING_TIMEOUT_SECS);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let n = read_result?;
|
||||
if n == 0 { break; }
|
||||
|
||||
// Any data from server resets the ping timer
|
||||
ping_sent = false;
|
||||
deadline = tokio::time::Instant::now()
|
||||
+ std::time::Duration::from_secs(PING_INTERVAL_SECS);
|
||||
|
||||
// IRC is not guaranteed UTF-8 — lossy conversion handles Latin-1 etc.
|
||||
let line = String::from_utf8_lossy(&buf).trim_end().to_string();
|
||||
if line.is_empty() { continue; }
|
||||
|
|
@ -471,6 +507,15 @@ pub async fn handle_command(
|
|||
return Err("usage: irc send <target> <message>".into());
|
||||
}
|
||||
let target = &args[0];
|
||||
if target.starts_with('#') {
|
||||
let s = state.borrow();
|
||||
if !s.channels.iter().any(|c| c == target) {
|
||||
return Err(format!(
|
||||
"not in channel {target} (joined: {})",
|
||||
s.channels.join(", ")
|
||||
));
|
||||
}
|
||||
}
|
||||
let msg = args[1..].join(" ");
|
||||
let nick = state.borrow().config.nick.clone();
|
||||
state
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue