From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 158891FF141 for ; Mon, 30 Mar 2026 16:42:35 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id B394136950; Mon, 30 Mar 2026 16:41:46 +0200 (CEST) From: Daniel Kral To: pve-devel@lists.proxmox.com Subject: [PATCH perl-rs v3 13/40] pve-rs: resource-scheduling: use generic usage implementation Date: Mon, 30 Mar 2026 16:30:22 +0200 Message-ID: <20260330144101.668747-14-d.kral@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260330144101.668747-1-d.kral@proxmox.com> References: <20260330144101.668747-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: 1774881613714 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.064 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: IDOQL4VO6S4W3TWGC6MKVSRBWLBV25G2 X-Message-ID-Hash: IDOQL4VO6S4W3TWGC6MKVSRBWLBV25G2 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 v2 -> v3: - require callers to handle the `current_node` not set invariant themselves, as this is pve-ha-manager-specific behavior and simplifies the logic a bit - s/FromInto/From/ for PveResource impl - use kebab-case for (de)serialization of `PveResource` - make node_stats closure variable mutable instead of shadowing it in the closure body again in StartedResourceAggregator::aggregate() .../src/bindings/resource_scheduling/mod.rs | 3 + .../resource_scheduling/pve_static.rs | 154 ++++++------------ .../bindings/resource_scheduling/resource.rs | 41 +++++ .../src/bindings/resource_scheduling/usage.rs | 33 ++++ 4 files changed, 130 insertions(+), 101 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..5353db9 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,9 +78,8 @@ pub mod pve_rs_resource_scheduling_static { let usage = this.inner.lock().unwrap(); usage - .nodes - .keys() - .map(|nodename| nodename.to_string()) + .nodenames_iter() + .map(|nodename| nodename.to_owned()) .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.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..532e868 --- /dev/null +++ b/pve-rs/src/bindings/resource_scheduling/resource.rs @@ -0,0 +1,41 @@ +use proxmox_resource_scheduling::resource::{ + Resource, ResourcePlacement, ResourceState, ResourceStats, +}; + +use serde::{Deserialize, Serialize}; + +/// A PVE resource. +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct PveResource> { + /// The resource's usage statistics. + stats: T, + /// Whether the resource is running. + running: bool, + /// The resource's current node. + current_node: String, + /// The resource's optional migration target node. + target_node: Option, +} + +impl> From> for Resource { + fn from(resource: PveResource) -> Self { + let state = if resource.running { + ResourceState::Started + } else { + ResourceState::Starting + }; + + let current_node = resource.current_node; + let placement = if let Some(target_node) = resource.target_node { + ResourcePlacement::Moving { + current_node, + target_node, + } + } else { + ResourcePlacement::Stationary { current_node } + }; + + 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..17a8d4d --- /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(), |mut node_stats, sid| { + if let Some(resource) = usage.get_resource(sid) { + node_stats.add_started_resource(&resource.stats()); + } + + node_stats + }); + + NodeUsage { + name: nodename.to_owned(), + stats, + } + }) + .collect() + } +} -- 2.47.3