watch: notify WATCH/MONITOR via nick->watchers reverse indexes (watch_by/monitor_by) instead of scanning every user on each online/offline/nick-change flip — O(watchers) not O(users); maintained through centralized watch_index_*/monitor_index_* helpers + quit cleanup

This commit is contained in:
Jean Chevronnet 2026-08-19 02:34:22 +00:00
parent 16ea4b5a23
commit 16fae5b23c
3 changed files with 158 additions and 31 deletions

View file

@ -58,11 +58,7 @@ fn watch_add(s: &mut Server, uid: Uid, nick: &str) {
);
return;
}
if let Some(u) = s.users.get_mut(&uid) {
if !u.watch.contains(&low) {
u.watch.push(low);
}
}
s.watch_index_add(uid, low);
watch_status(s, uid, nick);
}
@ -94,9 +90,7 @@ impl Command for Watch {
for tok in params.iter().flat_map(|p| p.split_whitespace()) {
match tok {
"C" | "c" => {
if let Some(u) = s.users.get_mut(&uid) {
u.watch.clear();
}
s.watch_index_clear(uid);
s.numeric(uid, RPL_ENDOFWATCHLIST, ":End of WATCH list");
}
"S" | "s" => {
@ -126,9 +120,7 @@ impl Command for Watch {
_ if tok.starts_with('+') => watch_add(s, uid, &tok[1..]),
_ if tok.starts_with('-') => {
let low = tok[1..].to_ascii_lowercase();
if let Some(u) = s.users.get_mut(&uid) {
u.watch.retain(|n| n != &low);
}
s.watch_index_remove(uid, &low);
s.numeric(
uid,
RPL_WATCHOFF,
@ -199,11 +191,7 @@ impl Command for Monitor {
);
continue;
}
if let Some(u) = s.users.get_mut(&uid) {
if !u.monitor.contains(&low) {
u.monitor.push(low);
}
}
s.monitor_index_add(uid, low);
added.push(t);
}
monitor_report(s, uid, &added);
@ -218,14 +206,12 @@ impl Command for Monitor {
.collect()
})
.unwrap_or_default();
if let Some(u) = s.users.get_mut(&uid) {
u.monitor.retain(|n| !targets.contains(n));
for t in &targets {
s.monitor_index_remove(uid, t);
}
}
"C" => {
if let Some(u) = s.users.get_mut(&uid) {
u.monitor.clear();
}
s.monitor_index_clear(uid);
}
"L" => {
let list = s

View file

@ -171,6 +171,11 @@ pub struct Server {
/// the common case (count 0) skips the O(users) scan. Maintained solely through
/// `accept_add`/`accept_remove` and the quit path.
pub accepted_nicks: HashMap<String, u32>,
/// Reverse index (lowercased) nick -> uids WATCHing / MONITORing it, so an
/// online/offline flip notifies only the watchers instead of scanning every user.
/// Maintained solely through the `watch_index_*` / `monitor_index_*` helpers and quit.
pub watch_by: HashMap<String, HashSet<Uid>>,
pub monitor_by: HashMap<String, HashSet<Uid>>,
// labeled-response: while Some((uid, buf)), that client's own responses are
// diverted into `buf` instead of the socket, so `on_line` can wrap them with
// the command's `label` (single tag, BATCH, or ACK). RefCell because the
@ -228,6 +233,8 @@ impl Server {
raw_config: cfg.raw,
config_gen: 0,
accepted_nicks: HashMap::default(),
watch_by: HashMap::default(),
monitor_by: HashMap::default(),
label_capture: RefCell::new(None),
log: RefCell::new(LogState::default()),
event_tx,
@ -726,6 +733,23 @@ impl Server {
}
}
}
// drop this user from the WATCH/MONITOR reverse indexes
for n in &user.watch {
if let Some(set) = self.watch_by.get_mut(n) {
set.remove(&uid);
if set.is_empty() {
self.watch_by.remove(n);
}
}
}
for n in &user.monitor {
if let Some(set) = self.monitor_by.get_mut(n) {
set.remove(&uid);
if set.is_empty() {
self.monitor_by.remove(n);
}
}
}
self.uuid_local.remove(&user.uuid);
if user.registered {
self.push_whowas(

View file

@ -37,15 +37,17 @@ impl Server {
}) else {
return;
};
for (&uid, u) in &self.users {
if u.watch.contains(&low) {
if let Some(set) = self.watch_by.get(&low) {
for &uid in set {
self.numeric(
uid,
RPL_LOGON,
&format!("{dnick} {ident} {host} {ts} :is now online"),
);
}
if u.monitor.contains(&low) {
}
if let Some(set) = self.monitor_by.get(&low) {
for &uid in set {
self.numeric(uid, RPL_MONONLINE, &format!(":{dnick}!{ident}@{host}"));
}
}
@ -56,11 +58,13 @@ impl Server {
pub fn watch_notify_offline(&self, nick: &str) {
let low = nick.to_ascii_lowercase();
let ts = now();
for (&uid, u) in &self.users {
if u.watch.contains(&low) {
if let Some(set) = self.watch_by.get(&low) {
for &uid in set {
self.numeric(uid, RPL_LOGOFF, &format!("{nick} * * {ts} :is now offline"));
}
if u.monitor.contains(&low) {
}
if let Some(set) = self.monitor_by.get(&low) {
for &uid in set {
self.numeric(uid, RPL_MONOFFLINE, &format!(":{nick}"));
}
}
@ -69,10 +73,123 @@ impl Server {
/// How many users currently WATCH `nick` (for `WATCH S` stats).
pub fn watchers_of(&self, nick: &str) -> usize {
let low = nick.to_ascii_lowercase();
self.users
.values()
.filter(|u| u.watch.contains(&low))
.count()
self.watch_by.get(&low).map(|s| s.len()).unwrap_or(0)
}
// ── WATCH/MONITOR list mutators — keep the reverse index in sync ──────────
/// Add `nick_low` to `uid`'s WATCH list (if absent) and index it.
pub fn watch_index_add(&mut self, uid: Uid, nick_low: String) {
let added = self
.users
.get_mut(&uid)
.map(|u| {
if u.watch.contains(&nick_low) {
false
} else {
u.watch.push(nick_low.clone());
true
}
})
.unwrap_or(false);
if added {
self.watch_by.entry(nick_low).or_default().insert(uid);
}
}
/// Remove `nick_low` from `uid`'s WATCH list and de-index it.
pub fn watch_index_remove(&mut self, uid: Uid, nick_low: &str) {
let removed = self
.users
.get_mut(&uid)
.map(|u| {
let before = u.watch.len();
u.watch.retain(|n| n != nick_low);
before != u.watch.len()
})
.unwrap_or(false);
if removed {
if let Some(set) = self.watch_by.get_mut(nick_low) {
set.remove(&uid);
if set.is_empty() {
self.watch_by.remove(nick_low);
}
}
}
}
/// Clear `uid`'s whole WATCH list (WATCH C) and de-index every entry.
pub fn watch_index_clear(&mut self, uid: Uid) {
let nicks = self
.users
.get_mut(&uid)
.map(|u| std::mem::take(&mut u.watch))
.unwrap_or_default();
for n in nicks {
if let Some(set) = self.watch_by.get_mut(&n) {
set.remove(&uid);
if set.is_empty() {
self.watch_by.remove(&n);
}
}
}
}
/// Add `nick_low` to `uid`'s MONITOR list (if absent) and index it.
pub fn monitor_index_add(&mut self, uid: Uid, nick_low: String) {
let added = self
.users
.get_mut(&uid)
.map(|u| {
if u.monitor.contains(&nick_low) {
false
} else {
u.monitor.push(nick_low.clone());
true
}
})
.unwrap_or(false);
if added {
self.monitor_by.entry(nick_low).or_default().insert(uid);
}
}
/// Remove `nick_low` from `uid`'s MONITOR list and de-index it.
pub fn monitor_index_remove(&mut self, uid: Uid, nick_low: &str) {
let removed = self
.users
.get_mut(&uid)
.map(|u| {
let before = u.monitor.len();
u.monitor.retain(|n| n != nick_low);
before != u.monitor.len()
})
.unwrap_or(false);
if removed {
if let Some(set) = self.monitor_by.get_mut(nick_low) {
set.remove(&uid);
if set.is_empty() {
self.monitor_by.remove(nick_low);
}
}
}
}
/// Clear `uid`'s whole MONITOR list (MONITOR C) and de-index every entry.
pub fn monitor_index_clear(&mut self, uid: Uid) {
let nicks = self
.users
.get_mut(&uid)
.map(|u| std::mem::take(&mut u.monitor))
.unwrap_or_default();
for n in nicks {
if let Some(set) = self.monitor_by.get_mut(&n) {
set.remove(&uid);
if set.is_empty() {
self.monitor_by.remove(&n);
}
}
}
}
/// True if `sender_nick` is on `target`'s ACCEPT list (callerid +g).