From 69d4064c877cd9ae80de8cb6bc65b89b3c99aeae Mon Sep 17 00:00:00 2001 From: Alex Lyn Date: Fri, 3 Jul 2026 10:48:40 +0800 Subject: [PATCH] runtime-rs: Introduce queues for DanConfig to make it configurable As there's no field to map the configuration's network_queues item in the `DanConfig`, this commit introduces a network_queues to do this. And accordingly, we also make it passed down from sandbox layer to Dan network configurations. To make it work well, it make it more robust for queues and queue_size settings with checking logics. And related UT is added. Signed-off-by: Alex Lyn --- .../crates/resource/src/network/dan.rs | 105 ++++++++++++------ .../src/network/endpoint/endpoints_test.rs | 52 +++++++++ .../runtimes/virt_container/src/sandbox.rs | 6 + 3 files changed, 127 insertions(+), 36 deletions(-) diff --git a/src/runtime-rs/crates/resource/src/network/dan.rs b/src/runtime-rs/crates/resource/src/network/dan.rs index e17a557886..f739fb7ff8 100644 --- a/src/runtime-rs/crates/resource/src/network/dan.rs +++ b/src/runtime-rs/crates/resource/src/network/dan.rs @@ -68,8 +68,8 @@ impl DanInner { let json_str = fs::read_to_string(&config.dan_conf_path) .await .context("Read DAN config from file")?; - let config: DanConfig = serde_json::from_str(&json_str).context("Invalid DAN config")?; - info!(sl!(), "Dan config is loaded = {:?}", config); + let dan_config: DanConfig = serde_json::from_str(&json_str).context("Invalid DAN config")?; + info!(sl!(), "Dan config is loaded = {:?}", dan_config); let (connection, handle, _) = rtnetlink::new_connection().context("New connection")?; let thread_handler = tokio::spawn(connection); @@ -77,43 +77,45 @@ impl DanInner { thread_handler.abort(); }); - let mut entity_list = Vec::with_capacity(config.devices.len()); - for (idx, device) in config.devices.iter().enumerate() { + let mut entity_list = Vec::with_capacity(dan_config.devices.len()); + for (idx, device) in dan_config.devices.iter().enumerate() { let name = format!("eth{idx}"); + // The `network_queues` is a queue *pair* count. + // Keep `queue_num` as a pair count and the hypervisor backend converts pairs into the actual virtqueue count. + // A JSON-provided non-zero `queue_num` (also a pair count) with a higher priority always wins. + let (qnum, qsize) = device + .device + .get_effective_queues(config.network_queues); let endpoint: Arc = match &device.device { - Device::VhostUser { - path, - queue_num, - queue_size, - } => Arc::new( - VhostUserEndpoint::new( - dev_mgr, - &name, - &device.guest_mac, - path, - *queue_num, - *queue_size, + Device::VhostUser { path, .. } => { + Arc::new( + VhostUserEndpoint::new( + dev_mgr, + &name, + &device.guest_mac, + path, + qnum, + qsize, + ) + .await + .with_context(|| format!("create a vhost user endpoint, path: {path}"))?, ) - .await - .with_context(|| format!("create a vhost user endpoint, path: {path}"))?, - ), - Device::HostTap { - tap_name, - queue_num, - queue_size, - } => Arc::new( - TapEndpoint::new( - &handle, - &name, - tap_name, - &device.guest_mac, - *queue_num, - *queue_size, - dev_mgr, + } + Device::HostTap { tap_name, .. } => { + Arc::new( + TapEndpoint::new( + &handle, + &name, + tap_name, + &device.guest_mac, + qnum, + qsize, + dev_mgr, + ) + .await + .with_context(|| format!("create a {tap_name} tap endpoint"))?, ) - .await - .with_context(|| format!("create a {tap_name} tap endpoint"))?, - ), + } }; let network_info = Arc::new( @@ -129,7 +131,7 @@ impl DanInner { } Ok(Self { - netns: config.netns, + netns: dan_config.netns, entity_list, }) } @@ -211,6 +213,9 @@ impl Network for Dan { #[derive(Debug)] pub struct DanNetworkConfig { pub dan_conf_path: PathBuf, + /// Number of virtio queue pairs (each pair = 1 RX + 1 TX). + /// Derived from `network_queues` in the hypervisor TOML config. + pub network_queues: usize, } /// Directly attachable network config written by CNI plugins @@ -259,6 +264,34 @@ pub(crate) enum Device { }, } +impl Device { + /// get the effective queue-pair count and queue size. + pub(crate) fn get_effective_queues(&self, network_queues: usize) -> (usize, usize) { + // The `network_queues` comes from hypervisor configurations, and we need to ensure that it is at least 1, + // otherwise the network device will not work. + let network_queues = network_queues.max(1); + let (queue_num, queue_size) = match self { + Device::VhostUser { + queue_num, + queue_size, + .. + } + | Device::HostTap { + queue_num, + queue_size, + .. + } => (queue_num, queue_size), + }; + let qnum = if *queue_num == 0 { + network_queues + } else { + *queue_num + }; + let qsize = if *queue_size == 0 { 256 } else { *queue_size }; + (qnum, qsize) + } +} + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub(crate) struct NetworkInfo { pub(crate) interface: Interface, diff --git a/src/runtime-rs/crates/resource/src/network/endpoint/endpoints_test.rs b/src/runtime-rs/crates/resource/src/network/endpoint/endpoints_test.rs index 753b88e70d..df3b356654 100644 --- a/src/runtime-rs/crates/resource/src/network/endpoint/endpoints_test.rs +++ b/src/runtime-rs/crates/resource/src/network/endpoint/endpoints_test.rs @@ -17,6 +17,7 @@ mod tests { use tokio::sync::RwLock; use crate::network::{ + dan::Device, endpoint::{IPVlanEndpoint, MacVlanEndpoint, VlanEndpoint}, network_model::{ self, @@ -410,4 +411,55 @@ mod tests { } } } + + // DAN regression test for the minimum-of-1 requirement. + #[test] + fn test_dan_device_get_effective_queues_min_one() { + // Test `network_queues` of 0 to the minimum of 1. + let default_network_queues = 0_usize; + + // VhostUser effective pair count falls back to default (1), + // queue size falls back to 256. + let vhost = Device::VhostUser { + path: "/tmp/test".to_owned(), + queue_num: 0, + queue_size: 0, + }; + assert_eq!( + vhost.get_effective_queues(default_network_queues), + (1, 256) + ); + + // HostTap fallback default behaviour. + let tap = Device::HostTap { + tap_name: "tap0".to_owned(), + queue_num: 0, + queue_size: 0, + }; + assert_eq!(tap.get_effective_queues(default_network_queues), (1, 256)); + + // This catches a regression that would always return 1. + let vhost_default4 = Device::VhostUser { + path: "/tmp/test".to_owned(), + queue_num: 0, + queue_size: 0, + }; + assert_eq!(vhost_default4.get_effective_queues(4), (4, 256)); + + // A non-zero `queue_num` from the JSON always wins over the default, + // even when it equals the minimum — and is never reported as 0. + let vhost_nonzero = Device::VhostUser { + path: "/tmp/test".to_owned(), + queue_num: 1, + queue_size: 0, + }; + assert_eq!(vhost_nonzero.get_effective_queues(default_network_queues), (1, 256)); + + let tap_explicit = Device::HostTap { + tap_name: "tap0".to_owned(), + queue_num: 7, + queue_size: 512, + }; + assert_eq!(tap_explicit.get_effective_queues(default_network_queues), (7, 512)); + } } diff --git a/src/runtime-rs/crates/runtimes/virt_container/src/sandbox.rs b/src/runtime-rs/crates/runtimes/virt_container/src/sandbox.rs index 853cfd4b85..fb9b3dd710 100644 --- a/src/runtime-rs/crates/runtimes/virt_container/src/sandbox.rs +++ b/src/runtime-rs/crates/runtimes/virt_container/src/sandbox.rs @@ -549,6 +549,12 @@ impl VirtSandbox { Some(ResourceConfig::Network(NetworkConfig::Dan( DanNetworkConfig { dan_conf_path: dan_path, + network_queues: self + .hypervisor + .hypervisor_config() + .await + .network_info + .network_queues as usize, }, ))) } else if let Some(netns_path) = network_env.netns.as_ref() {