Skip to content

Commit d69246d

Browse files
daniel-nolandclaude
andcommitted
refactor(dataplane): shutdown signaling + kernel driver scoped threads
Replace the last legacy shutdown signaling (ctrlc handler + mpsc<i32> exit code + dedicated controller thread) with the single `lifecycle::Shutdown` path, and migrate the kernel driver to scoped threads with cancellation observation. After this commit there is one signaling path: SIGINT/SIGTERM -> shutdown.root, or any subsystem's report_fatal -> shutdown.root, with the watchdog as the absolute upper bound. - `main`: install `spawn_signal_handler` and `spawn_shutdown_watchdog`, run everything inside `concurrency::thread::scope`, block on `root.cancelled()`, then drain subsystems in canonical order (workers -> router -> metrics -> mgmt). Exit code from `is_fatal()`. - `DriverKernel::start`: takes `&Scope` and `&Subsystem`; workers spawn via `spawn_scoped` with an `ExitGuard` Drop pattern that reports fatal on panic-unwind, early `?`-return, and unexpected normal exit. Reader loops observe cancel between reads. Supervisor joins-and-logs. - Drop `dataplane/src/drivers/tokio_util.rs` and its `run_in_local_tokio_runtime` helper (inlined where needed). - Drop `ctrlc` and `mio` from `dataplane` dependencies; drop `ctrlc` from the workspace. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Daniel Noland <daniel@githedgehog.com>
1 parent d16415d commit d69246d

8 files changed

Lines changed: 211 additions & 236 deletions

File tree

Cargo.lock

Lines changed: 0 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,6 @@ clap = { version = "4.6.1", default-features = true, features = [] }
116116
color-eyre = { version = "0.6.5", default-features = false, features = [] }
117117
colored = { version = "3.1.1", default-features = false, features = [] }
118118
crossbeam-utils = { version = "0.8.21", default-features = false, features = [] }
119-
ctrlc = { version = "3.5.2", default-features = false, features = [] }
120119
dashmap = { version = "6.2.1", default-features = false, features = [] }
121120
derive_builder = { version = "0.20.2", default-features = false, features = [] }
122121
dotenvy = { version = "0.15.7", default-features = false, features = [] }

dataplane/Cargo.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ axum = { workspace = true, features = ["http1", "tokio"] }
1919
axum-server = { workspace = true }
2020
concurrency = { workspace = true }
2121
config = { workspace = true }
22-
ctrlc = { workspace = true, features = ["termination"] }
2322
dyn-iter = { workspace = true }
2423
flow-entry = { workspace = true }
2524
flow-filter = { workspace = true }
@@ -32,7 +31,6 @@ linkme = { workspace = true }
3231
metrics = { workspace = true }
3332
metrics-exporter-prometheus = { workspace = true }
3433
mgmt = { workspace = true }
35-
mio = { workspace = true, features = ["os-ext", "net"] }
3634
nat = { workspace = true }
3735
net = { workspace = true, features = ["test_buffer"] }
3836
nix = { workspace = true, features = ["socket", "hostname"] }

dataplane/src/drivers/kernel/mod.rs

Lines changed: 54 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -18,95 +18,97 @@ mod worker;
1818

1919
use concurrency::sync::Arc;
2020
use concurrency::thread;
21+
#[allow(unused_imports)] // used under loom/shuttle backends
22+
use concurrency::thread::BuilderExt;
23+
use lifecycle::Subsystem;
2124
use net::buffer::test_buffer::TestBuffer;
2225
use pipeline::DynPipeline;
2326
use tracectl::trace_target;
2427
#[allow(unused)]
2528
use tracing::{debug, error, info, trace, warn};
2629

2730
use super::DriverError;
28-
use super::tokio_util::run_in_local_tokio_runtime;
2931
use kif::{Kif, bring_kifs_up};
3032
use worker::Worker;
3133

3234
trace_target!("kernel-driver", LevelFilter::INFO, &["driver"]);
3335

34-
/// Main structure representing the kernel driver.
35-
/// This driver:
36-
/// * receives raw frames via `AF_PACKET`, parses to `Packet<TestBuffer>`
37-
/// * selects a worker by symmetric flow hash
38-
/// * workers run independent pipelines and send processed packets back
39-
/// * dispatcher serializes & transmits on the chosen outgoing interface
36+
/// AF_PACKET-based kernel driver. Spawns N workers with symmetric-hash
37+
/// fanout and per-worker pipelines.
4038
pub struct DriverKernel;
4139

4240
#[allow(clippy::cast_possible_truncation)]
4341
impl DriverKernel {
44-
/// Spawn `workers` processing threads, each with its own pipeline instance.
45-
///
46-
/// Returns:
47-
/// - `Arc<Vec<Sender<Packet<TestBuffer>>>>` one sender per worker (dispatcher -> worker)
48-
/// - `Receiver<Packet<TestBuffer>>` a single queue for processed packets (worker -> dispatcher)
49-
fn spawn_workers(
42+
/// Spawn `num_workers` worker threads into `scope`, each with its own
43+
/// pipeline. Bails on the first spawn failure; workers that did spawn
44+
/// drain via the scope join.
45+
fn spawn_workers_scoped<'scope>(
46+
scope: &'scope thread::Scope<'scope, '_>,
47+
workers_subsystem: &Subsystem,
5048
num_workers: usize,
5149
setup_pipeline: &Arc<dyn Send + Sync + Fn() -> DynPipeline<TestBuffer>>,
5250
interfaces: &[Kif],
53-
) -> Vec<thread::JoinHandle<Result<(), std::io::Error>>> {
51+
) -> Result<Vec<thread::ScopedJoinHandle<'scope, Result<(), std::io::Error>>>, std::io::Error>
52+
{
5453
info!("Spawning {num_workers} workers");
55-
let mut workers = Vec::new();
56-
for wid in 0..num_workers {
57-
let builder = thread::Builder::new().name(format!("dp-worker-{wid}"));
58-
let mut worker = Worker::new(wid, num_workers, setup_pipeline);
59-
match worker.start(builder, interfaces) {
60-
Ok(handle) => workers.push(handle),
61-
Err(e) => {
62-
error!("Failed to start worker {wid}: {e}");
63-
}
64-
}
65-
}
66-
workers
54+
(0..num_workers)
55+
.map(|wid| {
56+
let builder = thread::Builder::new().name(format!("dp-worker-{wid}"));
57+
Worker::new(wid, num_workers, setup_pipeline, workers_subsystem.clone())
58+
.start(scope, builder, interfaces)
59+
})
60+
.collect()
6761
}
6862

69-
/// Starts the kernel driver, spawns worker threads, and runs the dispatcher loop.
70-
///
71-
/// - `args`: kernel driver CLI parameters (e.g., `--interface` list)
72-
/// - `workers`: number of worker threads / pipelines
73-
/// - `setup_pipeline`: factory returning a **fresh** `DynPipeline<TestBuffer>` per worker
63+
/// Spawn worker threads + supervisor into `scope`. The scope joins
64+
/// all driver threads on closure return.
7465
///
7566
/// # Errors
76-
/// Returns [`DriverError`] in case the driver fails to start successfully.
77-
pub fn start(
78-
stop_tx: std::sync::mpsc::Sender<i32>,
67+
/// Returns [`DriverError`] on interface setup or thread spawn failure.
68+
pub fn start<'scope>(
69+
scope: &'scope thread::Scope<'scope, '_>,
70+
workers_subsystem: &Subsystem,
7971
args: impl IntoIterator<Item = impl AsRef<str> + Clone>,
8072
num_workers: usize,
8173
setup_pipeline: &Arc<dyn Send + Sync + Fn() -> DynPipeline<TestBuffer>>,
8274
) -> Result<(), DriverError> {
75+
// A current_thread runtime built inside another tokio runtime
76+
// panics; catch nesting in debug.
77+
debug_assert!(
78+
tokio::runtime::Handle::try_current().is_err(),
79+
"DriverKernel::start must not be invoked from within a tokio runtime context"
80+
);
81+
8382
info!("Collecting interfaces from config");
8483
let interfaces = kif::get_interfaces(args)?;
8584

86-
// ensure that the kernel interfaces for rx/tx are up
87-
run_in_local_tokio_runtime(async || bring_kifs_up(interfaces.as_slice()).await)?;
85+
tokio::runtime::Builder::new_current_thread()
86+
.enable_all()
87+
.build()?
88+
.block_on(bring_kifs_up(interfaces.as_slice()))?;
8889

89-
// Spawn workers
90-
let worker_handles =
91-
Self::spawn_workers(num_workers, setup_pipeline, interfaces.as_slice());
90+
let worker_handles = Self::spawn_workers_scoped(
91+
scope,
92+
workers_subsystem,
93+
num_workers,
94+
setup_pipeline,
95+
interfaces.as_slice(),
96+
)?;
9297

93-
let control_builder = thread::Builder::new().name("kernel-driver-controller".to_string());
94-
control_builder.spawn(move || {
98+
// The supervisor just joins-and-logs; worker fatal reporting is
99+
// handled by the `ExitGuard` inside each worker thread.
100+
let supervisor_builder =
101+
thread::Builder::new().name("kernel-driver-supervisor".to_string());
102+
supervisor_builder.spawn_scoped(scope, move || {
95103
for (id, handle) in worker_handles.into_iter().enumerate() {
96-
info!("Waiting for workers to finish");
104+
info!("Waiting for worker {id} to finish");
97105
match handle.join() {
98-
Ok(result) => match result {
99-
Ok(()) => info!("Worker {id} exited successfully"),
100-
Err(e) => error!("Worker {id} exited with error: {e}"),
101-
},
102-
Err(e) => error!("Unable to spawn worker {id} error: {e:?}"),
106+
Ok(Ok(())) => info!("Worker {id} exited successfully"),
107+
Ok(Err(e)) => error!("Worker {id} exited with error: {e}"),
108+
Err(panic_payload) => error!("Worker {id} panicked: {panic_payload:?}"),
103109
}
104110
}
105-
106-
// Exiting with error as it's not expected for all workers to finish
107-
error!("All workers finished unexpectedly");
108-
#[allow(clippy::expect_used)]
109-
stop_tx.send(1).expect("Failed to send stop signal");
111+
info!("All workers joined");
110112
})?;
111113

112114
Ok(())

dataplane/src/drivers/kernel/worker.rs

Lines changed: 76 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,16 @@ use tokio::sync::Mutex;
1515

1616
use concurrency::sync::Arc;
1717
use concurrency::thread;
18+
#[allow(unused_imports)] // used under loom/shuttle backends
19+
use concurrency::thread::BuilderExt;
20+
use lifecycle::Subsystem;
1821
use net::buffer::test_buffer::TestBuffer;
1922
use net::interface::InterfaceIndex;
2023
use net::packet::{DoneReason, Packet};
2124
use pipeline::{DynPipeline, NetworkFunction};
2225

2326
use crate::drivers::kernel::fanout::{PacketFanoutType, set_packet_fanout};
2427
use crate::drivers::kernel::kif::Kif;
25-
use crate::drivers::tokio_util::run_in_local_tokio_runtime;
2628

2729
use tracing::{debug, error, info, trace, warn};
2830

@@ -126,35 +128,77 @@ pub struct Worker {
126128
id: WorkerId,
127129
total_workers: usize,
128130
setup_pipeline: Arc<dyn Send + Sync + Fn() -> DynPipeline<TestBuffer>>,
131+
subsystem: Subsystem,
129132
}
130133

131134
impl Worker {
132135
pub fn new(
133136
id: WorkerId,
134137
total_workers: usize,
135138
setup_pipeline: &Arc<dyn Send + Sync + Fn() -> DynPipeline<TestBuffer>>,
139+
subsystem: Subsystem,
136140
) -> Self {
137141
Worker {
138142
id,
139143
total_workers,
140144
setup_pipeline: setup_pipeline.clone(),
145+
subsystem,
141146
}
142147
}
143148

144-
pub fn start(
149+
#[allow(clippy::too_many_lines)]
150+
pub fn start<'scope>(
145151
&mut self,
152+
scope: &'scope thread::Scope<'scope, '_>,
146153
thread_builder: thread::Builder,
147154
interfaces: &[Kif],
148-
) -> Result<thread::JoinHandle<Result<(), io::Error>>, io::Error> {
155+
) -> Result<thread::ScopedJoinHandle<'scope, Result<(), io::Error>>, io::Error> {
149156
let id = self.id;
150157
let total_workers = self.total_workers;
151158
let setup = self.setup_pipeline.clone();
152-
let interfaces = interfaces.iter().map(Kif::clone).collect::<Vec<_>>();
159+
let subsystem = self.subsystem.clone();
160+
let cancel = subsystem.cancel_token();
161+
let interfaces = interfaces.to_vec();
162+
163+
let handle_res = thread_builder.spawn_scoped(scope, move || {
164+
// Drop-guard so panic-unwind, early-`?`, and unexpected normal
165+
// return all reach report_fatal. Disarmed on the graceful path.
166+
struct ExitGuard {
167+
subsystem: Subsystem,
168+
id: WorkerId,
169+
armed: bool,
170+
}
171+
impl ExitGuard {
172+
fn disarm(&mut self) {
173+
self.armed = false;
174+
}
175+
}
176+
impl Drop for ExitGuard {
177+
fn drop(&mut self) {
178+
if !self.armed || self.subsystem.is_cancelled() {
179+
return;
180+
}
181+
let reason = if std::thread::panicking() {
182+
format!("worker {} panicked", self.id)
183+
} else {
184+
format!("worker {} exited unexpectedly", self.id)
185+
};
186+
self.subsystem.report_fatal(&reason);
187+
}
188+
}
153189

154-
let handle_res = thread_builder.spawn(move || {
155190
info!(worker = id, "Worker started");
191+
let mut guard = ExitGuard {
192+
subsystem: subsystem.clone(),
193+
id,
194+
armed: true,
195+
};
196+
197+
let rt = tokio::runtime::Builder::new_current_thread()
198+
.enable_all()
199+
.build_local(tokio::runtime::LocalOptions::default())?;
156200

157-
run_in_local_tokio_runtime(async || {
201+
let result = rt.block_on(async {
158202
let (readers, if_table) =
159203
match build_interface_table(id, total_workers, interfaces.as_slice()) {
160204
Ok(table) => table,
@@ -166,27 +210,39 @@ impl Worker {
166210

167211
let setup = setup.clone();
168212
let if_table = if_table.clone();
213+
let cancel = cancel.clone();
169214

170215
let mut reader_handles = tokio::task::JoinSet::new();
171216

172217
for intf in readers {
173218
let setup = setup.clone();
174219
let if_table = if_table.clone();
220+
let cancel = cancel.clone();
175221
reader_handles.spawn_local(async move {
176222
let intf = intf;
177223
let mut pipeline = setup();
178224
loop {
179-
tracing::debug!(worker = id, "awaiting packets");
225+
debug!(worker = id, "awaiting packets");
180226

181-
let packets_vec = match read_packets_from_interface(id, &intf).await {
182-
Ok(packets) => packets,
183-
Err(e) => {
184-
error!(
227+
let packets_vec = tokio::select! {
228+
() = cancel.cancelled() => {
229+
info!(
185230
worker = id,
186231
rx_intf_name = intf.if_name,
187-
"Error reading packets from interface: {e}"
232+
"cancellation observed; exiting reader"
188233
);
189-
vec![]
234+
break;
235+
}
236+
result = read_packets_from_interface(id, &intf) => match result {
237+
Ok(packets) => packets,
238+
Err(e) => {
239+
error!(
240+
worker = id,
241+
rx_intf_name = intf.if_name,
242+
"Error reading packets from interface: {e}"
243+
);
244+
vec![]
245+
}
190246
}
191247
};
192248

@@ -198,7 +254,6 @@ impl Worker {
198254
intf.if_name
199255
);
200256

201-
// Try to receive everything else that is in the buffer
202257
let packets = packets_vec.into_iter();
203258

204259
let mut count = 0;
@@ -238,8 +293,13 @@ impl Worker {
238293
}
239294

240295
Ok::<(), io::Error>(())
241-
})?;
242-
info!(worker = id, "Worker exited");
296+
});
297+
298+
if subsystem.is_cancelled() {
299+
guard.disarm();
300+
}
301+
info!(worker = id, "worker exited");
302+
result?;
243303
Ok::<(), io::Error>(())
244304
})?;
245305
Ok(handle_res)

dataplane/src/drivers/mod.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
use thiserror::Error;
55

66
pub mod kernel;
7-
mod tokio_util;
87

98
#[derive(Error, Debug)]
109
pub enum DriverError {

0 commit comments

Comments
 (0)