From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id 2151A1FF144 for ; Tue, 24 Mar 2026 19:34:54 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 7B0921C17B; Tue, 24 Mar 2026 19:32:02 +0100 (CET) From: Daniel Kral To: pve-devel@lists.proxmox.com Subject: [PATCH perl-rs v2 13/40] pve-rs: resource-scheduling: use generic usage implementation Date: Tue, 24 Mar 2026 19:29:57 +0100 Message-ID: <20260324183029.1274972-14-d.kral@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260324183029.1274972-1-d.kral@proxmox.com> References: <20260324183029.1274972-1-d.kral@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1774376988381 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.057 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: 3FOMFCF3VTGQPH4ELWZB2HJBKVAA6WVB X-Message-ID-Hash: 3FOMFCF3VTGQPH4ELWZB2HJBKVAA6WVB X-MailFrom: d.kral@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: The proxmox_resource_scheduling crate provides a generic usage implementation, which is backwards compatible with the pve_static bindings. This reduces the static resource scheduling bindings to a slightly thinner wrapper. This also exposes the new `add_resource(...)` binding, which allows callers to add services with additional state other than the usage stats. It is exposed as `add_service(...)` to be consistent with the naming of the rest of the existing methods. Where it is sensible for the bindings, the documentation is extended with a link to the documentation of the underlying methods. Signed-off-by: Daniel Kral --- changes v1 -> v2: - add patch message for context - change from only creating the proxmox_resource_scheduling::scheduler::ClusterUsage (now, proxmox_resource_scheduling::scheduler::Scheduler), to using the new but backwards-compatible `Usage` implementation instead - this essentially also squashes the 'store services stats independently of node' patch in here as this is also tracked by the generic `Usage` impl - add `usage` and `resource` crate for shared code .../src/bindings/resource_scheduling/mod.rs | 3 + .../resource_scheduling/pve_static.rs | 152 ++++++------------ .../bindings/resource_scheduling/resource.rs | 44 +++++ .../src/bindings/resource_scheduling/usage.rs | 33 ++++ 4 files changed, 132 insertions(+), 100 deletions(-) create mode 100644 pve-rs/src/bindings/resource_scheduling/resource.rs create mode 100644 pve-rs/src/bindings/resource_scheduling/usage.rs diff --git a/pve-rs/src/bindings/resource_scheduling/mod.rs b/pve-rs/src/bindings/resource_scheduling/mod.rs index af1fb6b..9ce631c 100644 --- a/pve-rs/src/bindings/resource_scheduling/mod.rs +++ b/pve-rs/src/bindings/resource_scheduling/mod.rs @@ -1,4 +1,7 @@ //! Resource scheduling related bindings. +mod resource; +mod usage; + mod pve_static; pub use pve_static::pve_rs_resource_scheduling_static; diff --git a/pve-rs/src/bindings/resource_scheduling/pve_static.rs b/pve-rs/src/bindings/resource_scheduling/pve_static.rs index a83a9ab..3d9f142 100644 --- a/pve-rs/src/bindings/resource_scheduling/pve_static.rs +++ b/pve-rs/src/bindings/resource_scheduling/pve_static.rs @@ -6,40 +6,34 @@ pub mod pve_rs_resource_scheduling_static { //! //! See [`proxmox_resource_scheduling`]. - use std::collections::{HashMap, HashSet}; use std::sync::Mutex; - use anyhow::{Error, bail}; + use anyhow::Error; use perlmod::Value; - use proxmox_resource_scheduling::pve_static::{StaticNodeUsage, StaticServiceUsage}; + use proxmox_resource_scheduling::node::NodeStats; + use proxmox_resource_scheduling::pve_static::StaticServiceUsage; + use proxmox_resource_scheduling::usage::Usage; + + use crate::bindings::resource_scheduling::{ + resource::PveResource, usage::StartedResourceAggregator, + }; perlmod::declare_magic!(Box : &Scheduler as "PVE::RS::ResourceScheduling::Static"); - struct StaticNodeInfo { - name: String, - maxcpu: usize, - maxmem: usize, - services: HashMap, - } - - struct Usage { - nodes: HashMap, - service_nodes: HashMap>, - } - - /// A scheduler instance contains the resource usage by node. + /// A scheduler instance contains the cluster usage. pub struct Scheduler { inner: Mutex, } + type StaticResource = PveResource; + /// Class method: Create a new [`Scheduler`] instance. + /// + /// See [`proxmox_resource_scheduling::usage::Usage::new`]. #[export(raw_return)] pub fn new(#[raw] class: Value) -> Result { - let inner = Usage { - nodes: HashMap::new(), - service_nodes: HashMap::new(), - }; + let inner = Usage::new(); Ok(perlmod::instantiate_magic!( &class, MAGIC => Box::new(Scheduler { inner: Mutex::new(inner) }) @@ -48,7 +42,7 @@ pub mod pve_rs_resource_scheduling_static { /// Method: Add a node with its basic CPU and memory info. /// - /// This inserts a [`StaticNodeInfo`] entry for the node into the scheduler instance. + /// See [`proxmox_resource_scheduling::usage::Usage::add_node`]. #[export] pub fn add_node( #[try_from_ref] this: &Scheduler, @@ -58,33 +52,24 @@ pub mod pve_rs_resource_scheduling_static { ) -> Result<(), Error> { let mut usage = this.inner.lock().unwrap(); - if usage.nodes.contains_key(&nodename) { - bail!("node {} already added", nodename); - } - - let node = StaticNodeInfo { - name: nodename.clone(), + let stats = NodeStats { + cpu: 0.0, maxcpu, + mem: 0, maxmem, - services: HashMap::new(), }; - usage.nodes.insert(nodename, node); - Ok(()) + usage.add_node(nodename, stats) } /// Method: Remove a node from the scheduler. + /// + /// See [`proxmox_resource_scheduling::usage::Usage::remove_node`]. #[export] pub fn remove_node(#[try_from_ref] this: &Scheduler, nodename: &str) { let mut usage = this.inner.lock().unwrap(); - if let Some(node) = usage.nodes.remove(nodename) { - for (sid, _) in node.services.iter() { - if let Some(service_nodes) = usage.service_nodes.get_mut(sid) { - service_nodes.remove(nodename); - } - } - } + usage.remove_node(nodename); } /// Method: Get a list of all the nodes in the scheduler. @@ -93,8 +78,7 @@ pub mod pve_rs_resource_scheduling_static { let usage = this.inner.lock().unwrap(); usage - .nodes - .keys() + .nodenames_iter() .map(|nodename| nodename.to_string()) .collect() } @@ -104,10 +88,26 @@ pub mod pve_rs_resource_scheduling_static { pub fn contains_node(#[try_from_ref] this: &Scheduler, nodename: &str) -> bool { let usage = this.inner.lock().unwrap(); - usage.nodes.contains_key(nodename) + usage.contains_node(nodename) + } + + /// Method: Add `service` with identifier `sid` to the scheduler. + /// + /// See [`proxmox_resource_scheduling::usage::Usage::add_resource`]. + #[export] + pub fn add_service( + #[try_from_ref] this: &Scheduler, + sid: String, + service: StaticResource, + ) -> Result<(), Error> { + let mut usage = this.inner.lock().unwrap(); + + usage.add_resource(sid, service.try_into()?) } /// Method: Add service `sid` and its `service_usage` to the node. + /// + /// See [`proxmox_resource_scheduling::usage::Usage::add_resource_usage_to_node`]. #[export] pub fn add_service_usage_to_node( #[try_from_ref] this: &Scheduler, @@ -117,81 +117,33 @@ pub mod pve_rs_resource_scheduling_static { ) -> Result<(), Error> { let mut usage = this.inner.lock().unwrap(); - match usage.nodes.get_mut(nodename) { - Some(node) => { - if node.services.contains_key(sid) { - bail!("service '{}' already added to node '{}'", sid, nodename); - } - - node.services.insert(sid.to_string(), service_usage); - } - None => bail!("node '{}' not present in usage hashmap", nodename), - } - - if let Some(service_nodes) = usage.service_nodes.get_mut(sid) { - if service_nodes.contains(nodename) { - bail!("node '{}' already added to service '{}'", nodename, sid); - } - - service_nodes.insert(nodename.to_string()); - } else { - let mut service_nodes = HashSet::new(); - service_nodes.insert(nodename.to_string()); - usage.service_nodes.insert(sid.to_string(), service_nodes); - } - - Ok(()) + // TODO Only for backwards compatibility, can be removed with a proper version bump + #[allow(deprecated)] + usage.add_resource_usage_to_node(nodename, sid, service_usage.into()) } /// Method: Remove service `sid` and its usage from all assigned nodes. + /// + /// See [`proxmox_resource_scheduling::usage::Usage::remove_resource`]. #[export] fn remove_service_usage(#[try_from_ref] this: &Scheduler, sid: &str) { let mut usage = this.inner.lock().unwrap(); - if let Some(nodes) = usage.service_nodes.remove(sid) { - for nodename in &nodes { - if let Some(node) = usage.nodes.get_mut(nodename) { - node.services.remove(sid); - } - } - } + usage.remove_resource(sid); } - /// Scores all previously added nodes for starting a `service` on. + /// Method: Scores nodes to start a service with the usage statistics `service_stats` on. /// - /// Scoring is done according to the static memory and CPU usages of the nodes as if the - /// service would already be running on each. - /// - /// Returns a vector of (nodename, score) pairs. Scores are between 0.0 and 1.0 and a higher - /// score is better. - /// - /// See [`proxmox_resource_scheduling::pve_static::score_nodes_to_start_service`]. + /// See [`proxmox_resource_scheduling::scheduler::Scheduler::score_nodes_to_start_resource`]. #[export] pub fn score_nodes_to_start_service( #[try_from_ref] this: &Scheduler, - service: StaticServiceUsage, + service_stats: StaticServiceUsage, ) -> Result, Error> { let usage = this.inner.lock().unwrap(); - let nodes = usage - .nodes - .values() - .map(|node| { - let mut node_usage = StaticNodeUsage { - name: node.name.to_string(), - cpu: 0.0, - maxcpu: node.maxcpu, - mem: 0, - maxmem: node.maxmem, - }; - for service in node.services.values() { - node_usage.add_service_usage(service); - } - - node_usage - }) - .collect::>(); - - proxmox_resource_scheduling::pve_static::score_nodes_to_start_service(&nodes, &service) + usage + .to_scheduler::() + .score_nodes_to_start_resource(service_stats) } } diff --git a/pve-rs/src/bindings/resource_scheduling/resource.rs b/pve-rs/src/bindings/resource_scheduling/resource.rs new file mode 100644 index 0000000..91d56b9 --- /dev/null +++ b/pve-rs/src/bindings/resource_scheduling/resource.rs @@ -0,0 +1,44 @@ +use anyhow::{Error, bail}; +use proxmox_resource_scheduling::resource::{ + Resource, ResourcePlacement, ResourceState, ResourceStats, +}; + +use serde::{Deserialize, Serialize}; + +/// A PVE resource. +#[derive(Serialize, Deserialize)] +pub struct PveResource> { + /// The resource's usage statistics. + stats: T, + /// Whether the resource is running. + running: bool, + /// The resource's current node. + current_node: Option, + /// The resource's optional migration target node. + target_node: Option, +} + +impl> TryFrom> for Resource { + type Error = Error; + + fn try_from(resource: PveResource) -> Result { + let state = if resource.running { + ResourceState::Started + } else { + ResourceState::Starting + }; + + let placement = match (resource.current_node, resource.target_node) { + (Some(current_node), Some(target_node)) => ResourcePlacement::Moving { + current_node, + target_node, + }, + (Some(current_node), None) | (None, Some(current_node)) => { + ResourcePlacement::Stationary { current_node } + } + _ => bail!("neither current_node nor target_node are set"), + }; + + Ok(Resource::new(resource.stats.into(), state, placement)) + } +} diff --git a/pve-rs/src/bindings/resource_scheduling/usage.rs b/pve-rs/src/bindings/resource_scheduling/usage.rs new file mode 100644 index 0000000..fc8b872 --- /dev/null +++ b/pve-rs/src/bindings/resource_scheduling/usage.rs @@ -0,0 +1,33 @@ +use proxmox_resource_scheduling::{ + scheduler::NodeUsage, + usage::{Usage, UsageAggregator}, +}; + +/// An aggregator, which adds any resource as a started resource. +/// +/// This aggregator is useful if the node base stats do not have any current usage. +pub(crate) struct StartedResourceAggregator; + +impl UsageAggregator for StartedResourceAggregator { + fn aggregate(usage: &Usage) -> Vec { + usage + .nodes_iter() + .map(|(nodename, node)| { + let stats = node.resources_iter().fold(node.stats(), |node_stats, sid| { + let mut node_stats = node_stats; + + if let Some(resource) = usage.get_resource(sid) { + node_stats.add_started_resource(&resource.stats()); + } + + node_stats + }); + + NodeUsage { + name: nodename.to_string(), + stats, + } + }) + .collect() + } +} -- 2.47.3