From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [45.144.208.40]) by lore.proxmox.com (Postfix) with ESMTPS id 8969A1FF0C0 for ; Wed, 21 Oct 2026 11:01:46 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id D63472179A; Wed, 21 Oct 2026 11:00:41 +0200 (CEST) From: Dominik Rusovac To: pve-devel@lists.proxmox.com Subject: [PATCH proxmox 03/15] fix #7993: resource-scheduling: lift to weighted load balancing Date: Wed, 21 Oct 2026 10:59:20 +0200 Message-ID: <20261021085932.1196302-4-d.rusovac@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20261021085932.1196302-1-d.rusovac@proxmox.com> References: <20261021085932.1196302-1-d.rusovac@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1792573177141 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.061 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust 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: XGF53NWH5OWXWGR4JMP2Z5RFVEV6RLIB X-Message-ID-Hash: XGF53NWH5OWXWGR4JMP2Z5RFVEV6RLIB X-MailFrom: d.rusovac@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: Blend weights into node load computation, effectively replacing arithmetic mean [0] of the node's CPU load and memory usage with weighted arithmetic mean [1] thereof. Expose customizable weights for bruteforce method. For TOPSIS method expose customizable weights for average cpu and average memory only. The weights of highest cpu and highest memory are not independently configurable; each is derived as twice as much as its average counterpart, that is, the weight of highest cpu/memory is twice as much as the weight of average cpu/memory. This makes it possible to prioritize balancing a specific computational resource while also preserving the ratio between a resource's two criteria, as introduced in [2]. Apart from the achieved imbalance, expose the score of a scored migration. Moreover, primarily order scored migrations by their score (lower is better), falling back to imbalance when scores tie, and finally to the migration itself if both tie -- ensuring a total, deterministic order even when several candidates achieve the exact same score. Add basic unit test for this. Refactor existing tests. [0] https://en.wikipedia.org/wiki/Arithmetic_mean [1] https://en.wikipedia.org/wiki/Weighted_arithmetic_mean [2] https://lore.proxmox.com/all/20221110143800.98047-3-f.ebner@proxmox.com/ Signed-off-by: Dominik Rusovac --- proxmox-resource-scheduling/src/node.rs | 27 +- proxmox-resource-scheduling/src/pve_static.rs | 3 +- proxmox-resource-scheduling/src/scheduler.rs | 248 +++++++++++++----- .../tests/scheduler.rs | 136 ++++++++-- 4 files changed, 327 insertions(+), 87 deletions(-) diff --git a/proxmox-resource-scheduling/src/node.rs b/proxmox-resource-scheduling/src/node.rs index 3c7ed393..f9dc5b16 100644 --- a/proxmox-resource-scheduling/src/node.rs +++ b/proxmox-resource-scheduling/src/node.rs @@ -51,9 +51,30 @@ impl NodeStats { self.mem as f64 / self.maxmem as f64 } - /// Returns a combined node usage as a percentage. - pub fn load(&self) -> f64 { - (self.cpu_load() + self.mem_load()) / 2.0 + /// Returns the node load as the [weighted arithmetic mean] of [`Self::cpu_load`] and + /// [`Self::mem_load`]. + /// + /// `cpu_weight` controls how much [`Self::cpu_load`] contributes to the load, analogously, + /// `mem_weight` controls how much [`Self::mem_load`] contributes to the load; the higher the + /// weight, the more that resource dominates the result. + /// + /// NOTE: + /// - the weights have to be positive + /// - only the weights' ratio matters, not their absolute magnitude + /// - a weight of 0.0 means no contribution to the load + /// - [weighted arithmetic mean] is the same as [arithmetic mean] if the weights are equal + /// + /// [weighted arithmetic mean]: https://en.wikipedia.org/wiki/Weighted_arithmetic_mean + /// [arithmetic mean]: https://en.wikipedia.org/wiki/Arithmetic_mean + pub fn load(&self, cpu_weight: f64, mem_weight: f64) -> f64 { + let weights_sum = cpu_weight + mem_weight; + + if weights_sum == 0.0 { + // both, cpu and mem, should not contribute to the load, so there is no load + return 0.0; + } + + (self.cpu_load() * cpu_weight + self.mem_load() * mem_weight) / weights_sum } } diff --git a/proxmox-resource-scheduling/src/pve_static.rs b/proxmox-resource-scheduling/src/pve_static.rs index 40b76cbc..98d64907 100644 --- a/proxmox-resource-scheduling/src/pve_static.rs +++ b/proxmox-resource-scheduling/src/pve_static.rs @@ -96,5 +96,6 @@ pub fn score_nodes_to_start_service>( let scheduler = Scheduler::from_nodes(nodes); - scheduler.score_nodes_to_start_resource(*service) + // uses original empirically determined TOPSIS weights + scheduler.score_nodes_to_start_resource(*service, 1, 5) } diff --git a/proxmox-resource-scheduling/src/scheduler.rs b/proxmox-resource-scheduling/src/scheduler.rs index a12cd519..0690385d 100644 --- a/proxmox-resource-scheduling/src/scheduler.rs +++ b/proxmox-resource-scheduling/src/scheduler.rs @@ -8,6 +8,9 @@ use std::{ collections::BinaryHeap, }; +/// Constant to control the ratio of the weights of a resource's average and highest criteria. +pub const TOPSIS_AVERAGE_VS_HIGHEST_FACTOR: f64 = 2.0; + /// The scheduler view of a node. #[derive(Clone, Debug)] pub struct NodeUsage { @@ -69,23 +72,6 @@ fn node_stats_after_migration(candidate: &MigrationCandidate, node: &NodeUsage) new_stats } -criteria_struct! { - /// A given alternative. - struct PveTopsisAlternative { - #[criterion("average CPU", -1.0)] - average_cpu: f64, - #[criterion("highest CPU", -2.0)] - highest_cpu: f64, - #[criterion("average memory", -5.0)] - average_memory: f64, - #[criterion("highest memory", -10.0)] - highest_memory: f64, - } - - const N_CRITERIA; - static PVE_HA_TOPSIS_CRITERIA; -} - #[derive(Clone, Debug)] pub struct Scheduler { nodes: Vec, @@ -111,12 +97,20 @@ pub struct ScoredMigration { pub migration: Migration, /// The expected node imbalance after the migration. pub imbalance: f64, + /// The score. + /// + /// - corresponds to imbalance of weighted loads for + /// [`Scheduler::score_best_balancing_migration_candidates`] + /// - corresponds to distance to ideal best alternatives for + /// [`Scheduler::score_best_balancing_migration_candidates_topsis`] + pub score: f64, } impl Ord for ScoredMigration { fn cmp(&self, other: &Self) -> Ordering { - self.imbalance - .total_cmp(&other.imbalance) + self.score + .total_cmp(&other.score) + .then(self.imbalance.total_cmp(&other.imbalance)) .then(self.migration.cmp(&other.migration)) } } @@ -136,19 +130,21 @@ impl PartialEq for ScoredMigration { impl Eq for ScoredMigration {} impl ScoredMigration { - pub fn new>(migration: T, imbalance: f64) -> Self { - // Depending on how the imbalance is calculated, it can contain minor approximation errors. - // As this struct implements the Ord trait, users of the struct's cmp() can run into cases, - // where the imbalance is the same up to the significant digits in base 10, but treated as - // different values. + pub fn new>(migration: T, imbalance: f64, score: f64) -> Self { + // Depending on how the imbalance or score is calculated, it can contain minor approximation + // errors. As this struct implements the Ord trait, users of the struct's cmp() can run into + // cases, where the imbalance or score is the same up to the significant digits in base 10, + // but treated as different values. // // Therefore, truncate any non-significant digits to prevent these cases. let factor = 10_f64.powi(f64::DIGITS as i32); let truncated_imbalance = f64::trunc(factor * imbalance) / factor; + let truncated_score = f64::trunc(factor * score) / factor; Self { migration: migration.into(), imbalance: truncated_imbalance, + score: truncated_score, } } } @@ -179,15 +175,15 @@ impl Scheduler { } } - /// Maps the current node usages to a [`PveTopsisAlternative`]. + /// Maps the current node usages to a TOPSIS alternative. /// - /// The [`PveTopsisAlternative`] is derived by calculating a modified version of the root mean - /// square (RMS) and maximum value of each stat in the node usages. + /// The alternative is derived by calculating a modified version of the root mean square (RMS) + /// and maximum value of each stat in the node usages. fn topsis_alternative_with( &self, map_node_stats: impl Fn(&NodeUsage) -> NodeStats, - ) -> PveTopsisAlternative { - let len = self.nodes.len(); + ) -> [f64; 4] { + let len = self.nodes.len() as f64; // Base values on percentages to allow comparing nodes with different stats. let mut highest_cpu = 0.0; @@ -209,55 +205,79 @@ impl Scheduler { // Add 1.0 to avoid boosting tiny differences: e.g. 0.004 is twice as much as 0.002, but // 1.004 is only slightly more than 1.002. - PveTopsisAlternative { - average_cpu: 1.0 + (squares_cpu / len as f64).sqrt(), - highest_cpu: 1.0 + highest_cpu, - average_memory: 1.0 + (squares_mem / len as f64).sqrt(), - highest_memory: 1.0 + highest_mem, - } + [ + 1.0 + (squares_cpu / len).sqrt(), + 1.0 + highest_cpu, + 1.0 + (squares_mem / len).sqrt(), + 1.0 + highest_mem, + ] } /// Returns the load imbalance among the nodes. /// + /// `cpu_weight`, respectively, `mem_weight`, control how much [`NodeStats::cpu_load`], + /// respectively, [`NodeStats::mem_load`], contributes to the load imbalance via + /// [`NodeStats::load`]. + /// /// See [`calculate_node_imbalance`] for more information. - pub fn node_imbalance(&self) -> f64 { - calculate_node_imbalance(&self.nodes, |node| node.stats.load()) + pub fn node_imbalance(&self, cpu_weight: f64, mem_weight: f64) -> f64 { + calculate_node_imbalance(&self.nodes, |node| node.stats.load(cpu_weight, mem_weight)) } /// Returns the load imbalance among the nodes as if a specific resource was moved. /// + /// `cpu_weight`, respectively, `mem_weight`, control how much [`NodeStats::cpu_load`], + /// respectively, [`NodeStats::mem_load`], contributes to the load imbalance via + /// [`NodeStats::load`]. + /// /// See [`calculate_node_imbalance`] for more information. - fn node_imbalance_with_migration_candidate(&self, candidate: &MigrationCandidate) -> f64 { + fn node_imbalance_with_migration_candidate( + &self, + candidate: &MigrationCandidate, + cpu_weight: f64, + mem_weight: f64, + ) -> f64 { calculate_node_imbalance(&self.nodes, |node| { - node_stats_after_migration(candidate, node).load() + node_stats_after_migration(candidate, node).load(cpu_weight, mem_weight) }) } - /// Scores the given migration `candidates` by the best node imbalance improvement with - /// exhaustive search. + /// Uses brute-force search to score how much either of the given migration `candidates` + /// improves node imbalance and ranks them accordingly. /// /// The `candidates` are assumed to be consistent with the scheduler. No further validation is /// done whether the given nodenames actually exist in the scheduler. /// /// The scoring is done as if each resource migration has already been done. This assumes that /// the already migrated resource consumes the same amount of each stat as on the previous node - /// according to its `stats`. + /// according to its `stats`. The lower the score, the better. + /// + /// `cpu_weight` and `mem_weight` are blended into [`NodeStats::load`]. The higher the weight of + /// a computational resource, the more the search will tend to prioritize balancing out that + /// resource, e.g., a high `mem_weight` will steer migrations toward evening out memory usage, + /// even at the cost of leaving CPU load less balanced. This is a general tendency, not a + /// guarantee. /// /// Returns up to `limit` of the best scored migrations. pub fn score_best_balancing_migration_candidates( &self, candidates: I, limit: usize, + cpu_weight: usize, + mem_weight: usize, ) -> Vec where I: IntoIterator, { + let (w1, w2) = (cpu_weight as f64, mem_weight as f64); + let mut scored_migrations = candidates .into_iter() .map(|candidate| { - let imbalance = self.node_imbalance_with_migration_candidate(&candidate); + let imbalance = self.node_imbalance_with_migration_candidate(&candidate, 1.0, 1.0); + let score = self.node_imbalance_with_migration_candidate(&candidate, w1, w2); - Reverse(ScoredMigration::new(candidate, imbalance)) + Reverse(ScoredMigration::new(candidate, imbalance, score)) }) .collect::>(); @@ -274,56 +294,106 @@ impl Scheduler { best_migrations } - /// Scores the given migration `candidates` by the best node imbalance improvement with the - /// TOPSIS method. + /// Scores the given migration `candidates` using the TOPSIS method. /// /// The `candidates` are assumed to be consistent with the scheduler. No further validation is /// done whether the given nodenames actually exist in the scheduler. /// /// The scoring is done as if each resource migration has already been done. This assumes that /// the already migrated resource consumes the same amount of each stat as on the previous node - /// according to its `stats`. + /// according to its `stats`. The lower the score, the better. + /// + /// The weights are assigned to the respective criteria of the TOPSIS method. /// /// Returns up to `limit` of the best scored migrations. pub fn score_best_balancing_migration_candidates_topsis( &self, candidates: &[MigrationCandidate], limit: usize, + cpu_weight: usize, + mem_weight: usize, ) -> Result, Error> { + let (w1, w2) = (cpu_weight as f64, mem_weight as f64); + + let average_cpu = topsis::Criterion::new("average_cpu".to_owned(), -w1); + let highest_cpu = topsis::Criterion::new( + "highest_cpu".to_owned(), + -(TOPSIS_AVERAGE_VS_HIGHEST_FACTOR * w1), + ); + let average_mem = topsis::Criterion::new("average_mem".to_owned(), -w2); + let highest_mem = topsis::Criterion::new( + "highest_mem".to_owned(), + -(TOPSIS_AVERAGE_VS_HIGHEST_FACTOR * w2), + ); + let matrix = candidates .iter() .map(|candidate| { self.topsis_alternative_with(|node| node_stats_after_migration(candidate, node)) - .into() }) .collect::>(); - let best_alternatives = - topsis::rank_alternatives(&topsis::Matrix::new(matrix)?, &PVE_HA_TOPSIS_CRITERIA)?; + let mut scored_migrations = topsis::score_alternatives( + &topsis::Matrix::new(matrix)?, + &topsis::Criteria::new([average_cpu, highest_cpu, average_mem, highest_mem])?, + )? + .iter() + .zip(candidates.iter()) + .map(|(topsis_score, candidate)| { + let imbalance = self.node_imbalance_with_migration_candidate(candidate, w1, w2); + + Reverse(ScoredMigration::new( + candidate.clone(), + imbalance, + // invert score for consistent sorting + 1.0 - topsis_score, + )) + }) + .collect::>(); - Ok(best_alternatives - .into_iter() - .take(limit) - .map(|i| { - let imbalance = self.node_imbalance_with_migration_candidate(&candidates[i]); + let mut best_migrations = Vec::with_capacity(limit); - ScoredMigration::new(candidates[i].clone(), imbalance) - }) - .collect()) + // BinaryHeap::into_iter_sorted() is still in nightly unfortunately + while best_migrations.len() < limit { + match scored_migrations.pop() { + Some(Reverse(alternative)) => best_migrations.push(alternative), + None => break, + } + } + + Ok(best_migrations) } - /// Scores nodes to start a resource with the usage statistics `resource_stats` on. + /// Scores nodes to start a resource with the usage statistics `resource_stats` on, using the + /// TOPSIS method. /// /// The scoring is done as if the resource is already started on each node. This assumes that /// the already started resource consumes the maximum amount of each stat according to its /// `resource_stats`. /// + /// The weights are assigned to the respective criteria of the TOPSIS method. + /// /// Returns a vector of (nodename, score) pairs. Scores are between 0.0 and 1.0 and a higher /// score is better. pub fn score_nodes_to_start_resource>( &self, resource_stats: T, + cpu_weight: usize, + mem_weight: usize, ) -> Result, Error> { + let (w1, w2) = (cpu_weight as f64, mem_weight as f64); + + let average_cpu = topsis::Criterion::new("average_cpu".to_owned(), -w1); + let highest_cpu = topsis::Criterion::new( + "highest_cpu".to_owned(), + -(TOPSIS_AVERAGE_VS_HIGHEST_FACTOR * w1), + ); + let average_mem = topsis::Criterion::new("average_mem".to_owned(), -w2); + let highest_mem = topsis::Criterion::new( + "highest_mem".to_owned(), + -(TOPSIS_AVERAGE_VS_HIGHEST_FACTOR * w2), + ); + let resource_stats = resource_stats.into(); let matrix = self @@ -339,12 +409,13 @@ impl Scheduler { new_stats }) - .into() }) .collect::>(); - let scores = - topsis::score_alternatives(&topsis::Matrix::new(matrix)?, &PVE_HA_TOPSIS_CRITERIA)?; + let scores = topsis::score_alternatives( + &topsis::Matrix::new(matrix)?, + &topsis::Criteria::new([average_cpu, highest_cpu, average_mem, highest_mem])?, + )?; Ok(scores .into_iter() @@ -367,6 +438,7 @@ mod tests { target_node: String::from("node2"), }, 0.7231749488916931, + 0.7231749488916931, ); let migration2 = ScoredMigration::new( Migration { @@ -375,6 +447,7 @@ mod tests { target_node: String::from("node3"), }, 0.723174948891693, + 0.723174948891693, ); let migration3 = ScoredMigration::new( Migration { @@ -383,6 +456,57 @@ mod tests { target_node: String::from("node2"), }, 0.723174948891693 + 1e-15, + 0.723174948891693 + 1e-15, + ); + + let mut migrations = vec![migration2.clone(), migration3.clone(), migration1.clone()]; + + migrations.sort(); + + assert_eq!( + vec![migration1.clone(), migration2.clone(), migration3.clone()], + migrations + ); + + let mut heap = BinaryHeap::from(vec![ + Reverse(migration2.clone()), + Reverse(migration3.clone()), + Reverse(migration1.clone()), + ]); + + assert_eq!(heap.pop(), Some(Reverse(migration1))); + assert_eq!(heap.pop(), Some(Reverse(migration2))); + assert_eq!(heap.pop(), Some(Reverse(migration3))); + } + + #[test] + fn test_score_tie_breaks_on_imbalance() { + let migration1 = ScoredMigration::new( + Migration { + sid: String::from("vm:102"), + source_node: String::from("node1"), + target_node: String::from("node2"), + }, + 0.0, + 0.1, + ); + let migration2 = ScoredMigration::new( + Migration { + sid: String::from("vm:102"), + source_node: String::from("node1"), + target_node: String::from("node3"), + }, + 0.0, + 0.2, + ); + let migration3 = ScoredMigration::new( + Migration { + sid: String::from("vm:101"), + source_node: String::from("node1"), + target_node: String::from("node2"), + }, + 0.3, + 0.3, ); let mut migrations = vec![migration2.clone(), migration3.clone(), migration1.clone()]; diff --git a/proxmox-resource-scheduling/tests/scheduler.rs b/proxmox-resource-scheduling/tests/scheduler.rs index 61fdd840..19e2c353 100644 --- a/proxmox-resource-scheduling/tests/scheduler.rs +++ b/proxmox-resource-scheduling/tests/scheduler.rs @@ -5,6 +5,11 @@ use proxmox_resource_scheduling::{ scheduler::{Migration, MigrationCandidate, NodeUsage, Scheduler}, }; +const DEFAULT_CPU_WEIGHT: usize = 1; +const DEFAULT_MEM_WEIGHT: usize = 1; + +const TOPSIS_CPU_VS_MEM_FACTOR: usize = 5; + fn new_empty_cluster_scheduler() -> Scheduler { Scheduler::from_nodes(Vec::::new()) } @@ -83,7 +88,10 @@ fn new_heterogeneous_cluster_scheduler() -> Scheduler { fn test_node_imbalance_with_empty_cluster() { let scheduler = new_empty_cluster_scheduler(); - assert_eq!(scheduler.node_imbalance(), 0.0); + assert_eq!( + scheduler.node_imbalance(DEFAULT_CPU_WEIGHT as f64, DEFAULT_MEM_WEIGHT as f64), + 0.0 + ); } #[test] @@ -100,11 +108,17 @@ fn test_node_imbalance_with_perfectly_balanced_cluster() { let scheduler = Scheduler::from_nodes(vec![node.clone()]); - assert_eq!(scheduler.node_imbalance(), 0.0); + assert_eq!( + scheduler.node_imbalance(DEFAULT_CPU_WEIGHT as f64, DEFAULT_MEM_WEIGHT as f64), + 0.0 + ); let scheduler = Scheduler::from_nodes(vec![node.clone(), node.clone(), node]); - assert_eq!(scheduler.node_imbalance(), 0.0); + assert_eq!( + scheduler.node_imbalance(DEFAULT_CPU_WEIGHT as f64, DEFAULT_MEM_WEIGHT as f64), + 0.0 + ); } fn new_simple_migration_candidates() -> (Vec, Migration, Migration) { @@ -150,9 +164,11 @@ fn rank_best_balancing_migration_candidates( scheduler: &Scheduler, candidates: Vec, limit: usize, + cpu_weight: usize, + mem_weight: usize, ) -> Vec { scheduler - .score_best_balancing_migration_candidates(candidates, limit) + .score_best_balancing_migration_candidates(candidates, limit, cpu_weight, mem_weight) .into_iter() .map(|entry| entry.migration) .collect() @@ -163,7 +179,13 @@ fn test_score_best_balancing_migration_candidates_with_no_candidates() { let scheduler = new_homogeneous_cluster_scheduler(); assert_eq!( - rank_best_balancing_migration_candidates(&scheduler, vec![], 2), + rank_best_balancing_migration_candidates( + &scheduler, + vec![], + 2, + DEFAULT_CPU_WEIGHT, + DEFAULT_MEM_WEIGHT + ), vec![] ); } @@ -172,12 +194,21 @@ fn test_score_best_balancing_migration_candidates_with_no_candidates() { fn test_score_best_balancing_migration_candidates_in_homogeneous_cluster() { let scheduler = new_homogeneous_cluster_scheduler(); - assert_imbalance(scheduler.node_imbalance(), 0.3460548572604576); + assert_imbalance( + scheduler.node_imbalance(DEFAULT_CPU_WEIGHT as f64, DEFAULT_MEM_WEIGHT as f64), + 0.3460548572604576, + ); let (candidates, migration1, migration2) = new_simple_migration_candidates(); assert_eq!( - rank_best_balancing_migration_candidates(&scheduler, candidates, 2), + rank_best_balancing_migration_candidates( + &scheduler, + candidates, + 2, + DEFAULT_CPU_WEIGHT, + DEFAULT_MEM_WEIGHT + ), vec![migration2, migration1] ); } @@ -186,12 +217,21 @@ fn test_score_best_balancing_migration_candidates_in_homogeneous_cluster() { fn test_score_best_balancing_migration_candidates_in_heterogeneous_cluster() { let scheduler = new_heterogeneous_cluster_scheduler(); - assert_imbalance(scheduler.node_imbalance(), 0.23352917788066363); + assert_imbalance( + scheduler.node_imbalance(DEFAULT_CPU_WEIGHT as f64, DEFAULT_MEM_WEIGHT as f64), + 0.23352917788066363, + ); let (candidates, migration1, migration2) = new_simple_migration_candidates(); assert_eq!( - rank_best_balancing_migration_candidates(&scheduler, candidates, 2), + rank_best_balancing_migration_candidates( + &scheduler, + candidates, + 2, + DEFAULT_CPU_WEIGHT, + DEFAULT_MEM_WEIGHT + ), vec![migration2, migration1] ); } @@ -202,7 +242,12 @@ fn rank_best_balancing_migration_candidates_topsis( limit: usize, ) -> Result, Error> { Ok(scheduler - .score_best_balancing_migration_candidates_topsis(candidates, limit)? + .score_best_balancing_migration_candidates_topsis( + candidates, + limit, + DEFAULT_CPU_WEIGHT, + 2 * DEFAULT_MEM_WEIGHT, + )? .into_iter() .map(|entry| entry.migration) .collect()) @@ -225,7 +270,10 @@ fn test_score_best_balancing_migration_candidates_topsis_in_homogeneous_cluster( -> Result<(), Error> { let scheduler = new_homogeneous_cluster_scheduler(); - assert_imbalance(scheduler.node_imbalance(), 0.3460548572604576); + assert_imbalance( + scheduler.node_imbalance(DEFAULT_CPU_WEIGHT as f64, DEFAULT_MEM_WEIGHT as f64), + 0.3460548572604576, + ); let (candidates, migration1, migration2) = new_simple_migration_candidates(); @@ -242,7 +290,10 @@ fn test_score_best_balancing_migration_candidates_topsis_in_heterogeneous_cluste -> Result<(), Error> { let scheduler = new_heterogeneous_cluster_scheduler(); - assert_imbalance(scheduler.node_imbalance(), 0.23352917788066363); + assert_imbalance( + scheduler.node_imbalance(DEFAULT_CPU_WEIGHT as f64, DEFAULT_MEM_WEIGHT as f64), + 0.23352917788066363, + ); let (candidates, migration1, migration2) = new_simple_migration_candidates(); @@ -257,8 +308,11 @@ fn test_score_best_balancing_migration_candidates_topsis_in_heterogeneous_cluste fn rank_nodes_to_start_resource( scheduler: &Scheduler, resource_stats: ResourceStats, + cpu_weight: usize, + mem_weight: usize, ) -> Result, Error> { - let mut alternatives = scheduler.score_nodes_to_start_resource(resource_stats)?; + let mut alternatives = + scheduler.score_nodes_to_start_resource(resource_stats, cpu_weight, mem_weight)?; alternatives.sort_by(|a, b| b.1.total_cmp(&a.1)); @@ -280,7 +334,12 @@ fn test_score_homogeneous_nodes_to_start_resource() -> Result<(), Error> { }; assert_eq!( - rank_nodes_to_start_resource(&scheduler, heavy_memory_resource_stats)?, + rank_nodes_to_start_resource( + &scheduler, + heavy_memory_resource_stats, + DEFAULT_CPU_WEIGHT, + TOPSIS_CPU_VS_MEM_FACTOR * DEFAULT_MEM_WEIGHT + )?, vec!["node2", "node3", "node1"] ); @@ -292,7 +351,12 @@ fn test_score_homogeneous_nodes_to_start_resource() -> Result<(), Error> { }; assert_eq!( - rank_nodes_to_start_resource(&scheduler, heavy_cpu_resource_stats)?, + rank_nodes_to_start_resource( + &scheduler, + heavy_cpu_resource_stats, + DEFAULT_CPU_WEIGHT, + TOPSIS_CPU_VS_MEM_FACTOR * DEFAULT_MEM_WEIGHT + )?, vec!["node1", "node3", "node2"] ); @@ -304,7 +368,12 @@ fn test_score_homogeneous_nodes_to_start_resource() -> Result<(), Error> { }; assert_eq!( - rank_nodes_to_start_resource(&scheduler, unlimited_cpu_resource_stats)?, + rank_nodes_to_start_resource( + &scheduler, + unlimited_cpu_resource_stats, + DEFAULT_CPU_WEIGHT, + TOPSIS_CPU_VS_MEM_FACTOR * DEFAULT_MEM_WEIGHT + )?, vec!["node1", "node3", "node2"] ); @@ -316,7 +385,12 @@ fn test_score_homogeneous_nodes_to_start_resource() -> Result<(), Error> { }; assert_eq!( - rank_nodes_to_start_resource(&scheduler, combined_resource_stats)?, + rank_nodes_to_start_resource( + &scheduler, + combined_resource_stats, + DEFAULT_CPU_WEIGHT, + TOPSIS_CPU_VS_MEM_FACTOR * DEFAULT_MEM_WEIGHT + )?, vec!["node2", "node3", "node1"] ); @@ -335,7 +409,12 @@ fn test_score_heterogeneous_nodes_to_start_resource() -> Result<(), Error> { }; assert_eq!( - rank_nodes_to_start_resource(&scheduler, heavy_memory_resource_stats)?, + rank_nodes_to_start_resource( + &scheduler, + heavy_memory_resource_stats, + DEFAULT_CPU_WEIGHT, + TOPSIS_CPU_VS_MEM_FACTOR * DEFAULT_MEM_WEIGHT + )?, vec!["node2", "node1", "node3"] ); @@ -347,7 +426,12 @@ fn test_score_heterogeneous_nodes_to_start_resource() -> Result<(), Error> { }; assert_eq!( - rank_nodes_to_start_resource(&scheduler, heavy_cpu_resource_stats)?, + rank_nodes_to_start_resource( + &scheduler, + heavy_cpu_resource_stats, + DEFAULT_CPU_WEIGHT, + TOPSIS_CPU_VS_MEM_FACTOR * DEFAULT_MEM_WEIGHT + )?, vec!["node3", "node2", "node1"] ); @@ -359,7 +443,12 @@ fn test_score_heterogeneous_nodes_to_start_resource() -> Result<(), Error> { }; assert_eq!( - rank_nodes_to_start_resource(&scheduler, unlimited_cpu_resource_stats)?, + rank_nodes_to_start_resource( + &scheduler, + unlimited_cpu_resource_stats, + DEFAULT_CPU_WEIGHT, + TOPSIS_CPU_VS_MEM_FACTOR * DEFAULT_MEM_WEIGHT + )?, vec!["node1", "node3", "node2"] ); @@ -371,7 +460,12 @@ fn test_score_heterogeneous_nodes_to_start_resource() -> Result<(), Error> { }; assert_eq!( - rank_nodes_to_start_resource(&scheduler, combined_resource_stats)?, + rank_nodes_to_start_resource( + &scheduler, + combined_resource_stats, + DEFAULT_CPU_WEIGHT, + TOPSIS_CPU_VS_MEM_FACTOR * DEFAULT_MEM_WEIGHT + )?, vec!["node2", "node1", "node3"] ); -- 2.47.3