Skip to content

Commit cdd3451

Browse files
committed
unnumbered: replace monitor condvar signaling with wake channel
The condvar/Mutex<()>/dropped-flag scheme had lost-wakeup races (no predicate under the mutex), which could delay reconciliation or shutdown by a full poll interval, and split shutdown across two Drop impls. Use a sync_channel(1) instead: try_send wakes the monitor (a full buffer coalesces rapid config changes), and the loop exits when the channel disconnects. Drop for UnnumberedManager clears the Option<SyncSender> to disconnect before ManagedThread joins the thread; Drop bodies run before field drops, so the ordering is structural. Disconnected at the wake site can only mean the monitor panicked, so log it at crit. None means a test manager with no monitor thread. An async conversion of this subsystem is planned as a follow-up PR in the style of the BFD conversion (#795/#796). Signed-off-by: Trey Aspelund <trey@oxidecomputer.com>
1 parent d49fb7a commit cdd3451

1 file changed

Lines changed: 61 additions & 46 deletions

File tree

unnumbered/src/manager.rs

Lines changed: 61 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,16 @@ use mg_common::lock;
1010
use mg_common::thread::ManagedThread;
1111
use ndp::Ipv6NetworkInterface;
1212
use network_interface::{NetworkInterface, NetworkInterfaceConfig};
13-
use slog::{Logger, info, o, warn};
13+
use slog::{Logger, crit, info, o, warn};
1414
use std::{
1515
collections::HashMap,
1616
net::{IpAddr, Ipv6Addr},
1717
num::NonZeroU32,
1818
sync::{
19-
Arc, Condvar, Mutex,
20-
atomic::{AtomicBool, Ordering},
19+
Arc, Mutex,
20+
mpsc::{
21+
Receiver, RecvTimeoutError, SyncSender, TrySendError, sync_channel,
22+
},
2123
},
2224
thread,
2325
time::{Duration, Instant},
@@ -49,14 +51,20 @@ pub struct UnnumberedManager {
4951
/// currently have active router-discovery runtime.
5052
configured_interfaces: Arc<Mutex<HashMap<String, u16>>>,
5153

54+
/// Wakes the monitor thread when configuration changes. Buffer size 1:
55+
/// if a wake token is already pending the monitor has yet to run and
56+
/// will observe this change too, so a full buffer needs no new token.
57+
///
58+
/// None means no monitor thread exists by construction (tests), where
59+
/// reconciliation is driven manually. The manager's Drop impl takes the
60+
/// sender to disconnect the channel, ending the monitor loop before
61+
/// ManagedThread's drop joins the thread.
62+
monitor_tx: Option<SyncSender<()>>,
63+
5264
/// Managed thread for interface monitoring.
5365
/// Stored to ensure automatic cleanup on drop - the ManagedThread's Drop
54-
/// impl will signal shutdown and join the thread.
66+
/// impl will join the thread.
5567
monitor_thread: Arc<ManagedThread>,
56-
57-
/// Condvar to wake the monitor thread when work is added
58-
monitor_condvar: Arc<Condvar>,
59-
monitor_mutex: Arc<Mutex<()>>,
6068
}
6169

6270
impl UnnumberedManager {
@@ -65,37 +73,30 @@ impl UnnumberedManager {
6573
"module" => MOD_UNNUMBERED_MANAGER,
6674
));
6775

68-
// Create the managed thread and get its dropped flag before constructing
69-
// the manager, so we can pass the flag to the thread function.
7076
let monitor_thread = Arc::new(ManagedThread::new());
71-
let dropped = monitor_thread.dropped_flag();
7277
let active_interfaces = Arc::new(Mutex::new(InterfaceMap::new()));
7378
let configured_interfaces = Arc::new(Mutex::new(HashMap::new()));
74-
let monitor_condvar = Arc::new(Condvar::new());
75-
let monitor_mutex = Arc::new(Mutex::new(()));
79+
let (monitor_tx, monitor_rx) = sync_channel(1);
7680

7781
let manager = Arc::new(Self {
7882
log: log.clone(),
7983
active_interfaces: Arc::clone(&active_interfaces),
8084
configured_interfaces: Arc::clone(&configured_interfaces),
85+
monitor_tx: Some(monitor_tx),
8186
monitor_thread: Arc::clone(&monitor_thread),
82-
monitor_condvar: Arc::clone(&monitor_condvar),
83-
monitor_mutex: Arc::clone(&monitor_mutex),
8487
});
8588

86-
// Spawn and register the interface monitor thread.
87-
// The ManagedThread will automatically signal shutdown and join the
88-
// thread when the manager is dropped.
89+
// Spawn and register the interface monitor thread. Dropping the
90+
// manager disconnects the wake channel, which ends the monitor
91+
// loop; the ManagedThread then joins the thread.
8992
let handle = thread::Builder::new()
9093
.name("unnumbered-interface-monitor".to_string())
9194
.spawn(move || {
9295
Self::run_interface_monitor(
9396
log,
9497
active_interfaces,
9598
configured_interfaces,
96-
monitor_condvar,
97-
monitor_mutex,
98-
dropped,
99+
monitor_rx,
99100
);
100101
})
101102
.expect("failed to spawn interface monitor thread");
@@ -110,9 +111,9 @@ impl UnnumberedManager {
110111
log: Logger::root(slog::Discard, o!()),
111112
active_interfaces: Arc::new(Mutex::new(InterfaceMap::new())),
112113
configured_interfaces: Arc::new(Mutex::new(HashMap::new())),
114+
// No monitor thread; tests drive reconciliation manually.
115+
monitor_tx: None,
113116
monitor_thread: Arc::new(ManagedThread::new()),
114-
monitor_condvar: Arc::new(Condvar::new()),
115-
monitor_mutex: Arc::new(Mutex::new(())),
116117
})
117118
}
118119

@@ -125,25 +126,17 @@ impl UnnumberedManager {
125126
/// Uses batched interface checking: a single call to `NetworkInterface::show()`
126127
/// per poll cycle, regardless of how many interfaces are being monitored.
127128
///
128-
/// The `dropped` flag is set by the ManagedThread when the manager is dropped,
129-
/// signaling this thread to exit.
129+
/// Exits when `monitor_rx` disconnects, which happens when the manager
130+
/// (holding the sender) is dropped.
130131
fn run_interface_monitor(
131132
log: Logger,
132133
active_interfaces: Arc<Mutex<InterfaceMap>>,
133134
configured_interfaces: Arc<Mutex<HashMap<String, u16>>>,
134-
monitor_condvar: Arc<Condvar>,
135-
monitor_mutex: Arc<Mutex<()>>,
136-
dropped: Arc<AtomicBool>,
135+
monitor_rx: Receiver<()>,
137136
) {
138137
info!(log, "interface monitor started");
139138

140139
loop {
141-
// Check for shutdown (set by ManagedThread::drop)
142-
if dropped.load(Ordering::Relaxed) {
143-
info!(log, "interface monitor shutting down");
144-
break;
145-
}
146-
147140
// Check if there's any work to do
148141
let has_configured = !lock!(configured_interfaces).is_empty();
149142
let has_active = !lock!(active_interfaces).is_empty();
@@ -169,11 +162,17 @@ impl UnnumberedManager {
169162
}
170163
}
171164

172-
// Wait until notified about new work/shutdown, or poll again
173-
// after the interval.
174-
let guard = lock!(monitor_mutex);
175-
let _result =
176-
monitor_condvar.wait_timeout(guard, INTERFACE_POLL_INTERVAL);
165+
// Sleep until woken by a config change, the poll interval
166+
// elapsing, or the manager being dropped (disconnecting the
167+
// channel). A token sent while we were reconciling above is
168+
// buffered and returns immediately, so wakes cannot be lost.
169+
match monitor_rx.recv_timeout(INTERFACE_POLL_INTERVAL) {
170+
Ok(()) | Err(RecvTimeoutError::Timeout) => (),
171+
Err(RecvTimeoutError::Disconnected) => {
172+
info!(log, "interface monitor shutting down");
173+
break;
174+
}
175+
}
177176
}
178177

179178
info!(log, "interface monitor exited");
@@ -385,8 +384,24 @@ impl UnnumberedManager {
385384
"interface" => interface_str,
386385
);
387386

388-
// Wake up the monitor thread since there's new or updated work.
389-
self.monitor_condvar.notify_one();
387+
// Wake the monitor thread since there's new or updated work. A full
388+
// buffer means a wake is already pending and this change will be
389+
// observed by it. A disconnected channel means the monitor thread
390+
// exited while the manager is still alive — the loop only exits on
391+
// sender disconnect, so this indicates the monitor panicked and the
392+
// configuration just recorded will never be reconciled. A None
393+
// sender means no monitor exists by construction (tests) and the
394+
// caller drives reconciliation itself.
395+
if let Some(tx) = &self.monitor_tx
396+
&& let Err(TrySendError::Disconnected(())) = tx.try_send(())
397+
{
398+
crit!(
399+
self.log,
400+
"interface monitor thread is gone; \
401+
unnumbered interface reconciliation is not running";
402+
"interface" => interface_str,
403+
);
404+
}
390405

391406
Ok(())
392407
}
@@ -466,11 +481,11 @@ impl UnnumberedManager {
466481

467482
impl Drop for UnnumberedManager {
468483
fn drop(&mut self) {
469-
let _guard = lock!(self.monitor_mutex);
470-
self.monitor_thread
471-
.dropped_flag()
472-
.store(true, Ordering::Relaxed);
473-
self.monitor_condvar.notify_one();
484+
// Disconnect the wake channel so the monitor loop exits before
485+
// `monitor_thread`'s field drop joins the thread. Drop bodies run
486+
// before automatic field drops, so this ordering is guaranteed
487+
// regardless of field declaration order.
488+
self.monitor_tx = None;
474489
}
475490
}
476491

0 commit comments

Comments
 (0)