[PATCH perl-rs v3 13/40] pve-rs: resource-scheduling: use generic usage implementation

Daniel Kral
2026-03-30T14:42:35Z

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 <d.kral@proxmox.com>
---
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<T> impl
- use kebab-case for (de)serialization of `PveResource<T>`
- 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> : &Scheduler as "PVE::RS::ResourceScheduling::Static");

- struct StaticNodeInfo {
- name: String,
- maxcpu: usize,
- maxmem: usize,
- services: HashMap<String, StaticServiceUsage>,
- }
-
- struct Usage {
- nodes: HashMap<String, StaticNodeInfo>,
- service_nodes: HashMap<String, HashSet<String>>,
- }
-
- /// A scheduler instance contains the resource usage by node.
+ /// A scheduler instance contains the cluster usage.
pub struct Scheduler {
inner: Mutex<Usage>,
}

+ type StaticResource = PveResource<StaticServiceUsage>;
+
/// Class method: Create a new [`Scheduler`] instance.
+ ///
+ /// See [`proxmox_resource_scheduling::usage::Usage::new`].
#[export(raw_return)]
pub fn new(#[raw] class: Value) -> Result<Value, Error> {
- 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<Vec<(String, f64)>, 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::<Vec<StaticNodeUsage>>();
-
- proxmox_resource_scheduling::pve_static::score_nodes_to_start_service(&nodes, &service)
+ usage
+ .to_scheduler::<StartedResourceAggregator>()
+ .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<T: Into<ResourceStats>> {
+ /// 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<String>,
+}
+
+impl<T: Into<ResourceStats>> From<PveResource<T>> for Resource {
+ fn from(resource: PveResource<T>) -> 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<NodeUsage> {
+ 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