From: Daniel Kral <d.kral@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH ha-manager v2 10/12] rules: restrict inter-plugin resource references to simple cases
Date: Fri, 1 Aug 2025 18:22:25 +0200 [thread overview]
Message-ID: <20250801162230.296214-11-d.kral@proxmox.com> (raw)
In-Reply-To: <20250801162230.296214-1-d.kral@proxmox.com>
Add inter-plugin checks and helpers, which allow resources to be used in
node affinity rules and resource affinity rules at the same time, if the
following conditions are met:
- the resources of a resource affinity rule are not part of any node
affinity rule, which has multiple priority groups. This is because of
the dynamic nature of priority groups.
- the resources of a positive resource affinity rule are part of at most
one node affinity rule, but no more. Otherwise, it is not easily
decidable (yet) what the common node restrictions are.
- the positive resource affinity rules, which have at least one resource
which is part of one node affinity rule, make all the resources part
of the node affinity rule.
- the resources of a negative resource affinity rule are not restricted
by their node affinity rules in such a way that these do not have
enough nodes to be separated on.
Additionally, resources of a positive resource affinity rule, which are
also part of at most a single node affinity rule, are also added to the
node affinity rule.
Signed-off-by: Daniel Kral <d.kral@proxmox.com>
---
src/PVE/HA/Rules.pm | 281 ++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 281 insertions(+)
diff --git a/src/PVE/HA/Rules.pm b/src/PVE/HA/Rules.pm
index 64dae1e4..323ad038 100644
--- a/src/PVE/HA/Rules.pm
+++ b/src/PVE/HA/Rules.pm
@@ -410,6 +410,8 @@ sub canonicalize : prototype($$$) {
next if $@; # plugin doesn't implement plugin_canonicalize(...)
}
+ $class->global_canonicalize($rules);
+
return $messages;
}
@@ -475,4 +477,283 @@ sub get_next_ordinal : prototype($) {
return $current_order + 1;
}
+=head1 INTER-PLUGIN RULE CHECKERS
+
+=cut
+
+my $has_multiple_priorities = sub {
+ my ($node_affinity_rule) = @_;
+
+ my $priority;
+ for my $node (values $node_affinity_rule->{nodes}->%*) {
+ $priority = $node->{priority} if !defined($priority);
+
+ return 1 if $priority != $node->{priority};
+ }
+};
+
+=head3 check_single_priority_node_affinity_in_resource_affinity_rules(...)
+
+Returns all rules in C<$resource_affinity_rules> and C<$node_affinity_rules> as
+a list of lists, each consisting of the rule type and resource id, where at
+least one resource in a resource affinity rule are in node affinity rules,
+which have multiple priority groups defined.
+
+That is, the resource affinity rule cannot be statically checked to be feasible
+as the selection of the priority group is dependent on the currently online
+nodes.
+
+If there are none, the returned list is empty.
+
+=cut
+
+sub check_single_priority_node_affinity_in_resource_affinity_rules {
+ my ($resource_affinity_rules, $node_affinity_rules) = @_;
+
+ my @conflicts = ();
+
+ while (my ($resource_affinity_id, $resource_affinity_rule) = each %$resource_affinity_rules) {
+ my $has_conflicts;
+ my $resources = $resource_affinity_rule->{resources};
+ my @paired_node_affinity_rules = ();
+
+ for my $node_affinity_id (keys %$node_affinity_rules) {
+ my $node_affinity_rule = $node_affinity_rules->{$node_affinity_id};
+
+ next if sets_are_disjoint($resources, $node_affinity_rule->{resources});
+
+ $has_conflicts = $has_multiple_priorities->($node_affinity_rule)
+ if !$has_conflicts;
+
+ push @paired_node_affinity_rules, $node_affinity_id;
+ }
+ if ($has_conflicts) {
+ push @conflicts, ['resource-affinity', $resource_affinity_id];
+ push @conflicts, ['node-affinity', $_] for @paired_node_affinity_rules;
+ }
+ }
+
+ @conflicts = sort { $a->[0] cmp $b->[0] || $a->[1] cmp $b->[1] } @conflicts;
+ return \@conflicts;
+}
+
+__PACKAGE__->register_check(
+ sub {
+ my ($args) = @_;
+
+ return check_single_priority_node_affinity_in_resource_affinity_rules(
+ $args->{resource_affinity_rules},
+ $args->{node_affinity_rules},
+ );
+ },
+ sub {
+ my ($conflicts, $errors) = @_;
+
+ for my $conflict (@$conflicts) {
+ my ($type, $ruleid) = @$conflict;
+
+ if ($type eq 'node-affinity') {
+ push $errors->{$ruleid}->{resources}->@*,
+ "resources are in a resource affinity rule and cannot be in"
+ . " a node affinity rule with multiple priorities";
+ } elsif ($type eq 'resource-affinity') {
+ push $errors->{$ruleid}->{resources}->@*,
+ "resources are in node affinity rules with multiple priorities";
+ }
+ }
+ },
+);
+
+=head3 check_single_node_affinity_per_positive_resource_affinity_rule(...)
+
+Returns all rules in C<$positive_rules> and C<$node_affinity_rules> as a list of
+lists, each consisting of the rule type and resource id, where one of the
+resources is used in a positive resource affinity rule and more than one node
+affinity rule.
+
+If there are none, the returned list is empty.
+
+=cut
+
+sub check_single_node_affinity_per_positive_resource_affinity_rule {
+ my ($positive_rules, $node_affinity_rules) = @_;
+
+ my @conflicts = ();
+
+ while (my ($positiveid, $positive_rule) = each %$positive_rules) {
+ my $positive_resources = $positive_rule->{resources};
+ my @paired_node_affinity_rules = ();
+
+ while (my ($node_affinity_id, $node_affinity_rule) = each %$node_affinity_rules) {
+ next if sets_are_disjoint($positive_resources, $node_affinity_rule->{resources});
+
+ push @paired_node_affinity_rules, $node_affinity_id;
+ }
+ if (@paired_node_affinity_rules > 1) {
+ push @conflicts, ['positive', $positiveid];
+ push @conflicts, ['node-affinity', $_] for @paired_node_affinity_rules;
+ }
+ }
+
+ @conflicts = sort { $a->[0] cmp $b->[0] || $a->[1] cmp $b->[1] } @conflicts;
+ return \@conflicts;
+}
+
+__PACKAGE__->register_check(
+ sub {
+ my ($args) = @_;
+
+ return check_single_node_affinity_per_positive_resource_affinity_rule(
+ $args->{positive_rules},
+ $args->{node_affinity_rules},
+ );
+ },
+ sub {
+ my ($conflicts, $errors) = @_;
+
+ for my $conflict (@$conflicts) {
+ my ($type, $ruleid) = @$conflict;
+
+ if ($type eq 'positive') {
+ push $errors->{$ruleid}->{resources}->@*,
+ "resources are in multiple node affinity rules";
+ } elsif ($type eq 'node-affinity') {
+ push $errors->{$ruleid}->{resources}->@*,
+ "at least one resource is in a positive resource affinity"
+ . " rule and there are other resources in at least one"
+ . " other node affinity rule already";
+ }
+ }
+ },
+);
+
+=head3 check_negative_resource_affinity_node_affinity_consistency(...)
+
+Returns all rules in C<$negative_rules> and C<$node_affinity_rules> as a list
+of lists, each consisting of the rule type and resource id, where the resources
+in the negative resource affinity rule are restricted to less nodes than needed
+to keep them separate by their node affinity rules.
+
+That is, the negative resource affinity rule cannot be fullfilled as there are
+not enough nodes to spread the resources on.
+
+If there are none, the returned list is empty.
+
+=cut
+
+sub check_negative_resource_affinity_node_affinity_consistency {
+ my ($negative_rules, $node_affinity_rules) = @_;
+
+ my @conflicts = ();
+
+ while (my ($negativeid, $negative_rule) = each %$negative_rules) {
+ my $allowed_nodes = {};
+ my $located_resources;
+ my $resources = $negative_rule->{resources};
+ my @paired_node_affinity_rules = ();
+
+ for my $node_affinity_id (keys %$node_affinity_rules) {
+ my ($node_affinity_resources, $node_affinity_nodes) =
+ $node_affinity_rules->{$node_affinity_id}->@{qw(resources nodes)};
+ my $common_resources = set_intersect($resources, $node_affinity_resources);
+
+ next if keys %$common_resources < 1;
+
+ $located_resources = set_union($located_resources, $common_resources);
+ $allowed_nodes = set_union($allowed_nodes, $node_affinity_nodes);
+
+ push @paired_node_affinity_rules, $node_affinity_id;
+ }
+ if (keys %$allowed_nodes < keys %$located_resources) {
+ push @conflicts, ['negative', $negativeid];
+ push @conflicts, ['node-affinity', $_] for @paired_node_affinity_rules;
+ }
+ }
+
+ @conflicts = sort { $a->[0] cmp $b->[0] || $a->[1] cmp $b->[1] } @conflicts;
+ return \@conflicts;
+}
+
+__PACKAGE__->register_check(
+ sub {
+ my ($args) = @_;
+
+ return check_negative_resource_affinity_node_affinity_consistency(
+ $args->{negative_rules},
+ $args->{node_affinity_rules},
+ );
+ },
+ sub {
+ my ($conflicts, $errors) = @_;
+
+ for my $conflict (@$conflicts) {
+ my ($type, $ruleid) = @$conflict;
+
+ if ($type eq 'negative') {
+ push $errors->{$ruleid}->{resources}->@*,
+ "two or more resources are restricted to less nodes than"
+ . " available to the resources";
+ } elsif ($type eq 'node-affinity') {
+ push $errors->{$ruleid}->{resources}->@*,
+ "at least one resource is in a negative resource affinity"
+ . " rule and this rule would restrict these to less nodes"
+ . " than available to the resources";
+ }
+ }
+ },
+);
+
+=head1 INTER-PLUGIN RULE CANONICALIZATION HELPERS
+
+=cut
+
+=head3 create_implicit_positive_resource_affinity_node_affinity_rules(...)
+
+Modifies C<$rules> such that all resources of a positive resource affinity rule,
+defined in C<$positive_rules>, where at least one of their resources is also in
+a node affinity rule, defined in C<$node_affinity_rules>, makes all the other
+positive resource affinity rule's resources also part of the node affinity rule.
+
+This helper assumes that there can only be a single node affinity rule per
+positive resource affinity rule as there is no heuristic yet what should be
+done in the case of multiple node affinity rules.
+
+This also makes it cheaper to infer these implicit constraints later instead of
+propagating that information in each scheduler invocation.
+
+=cut
+
+sub create_implicit_positive_resource_affinity_node_affinity_rules {
+ my ($rules, $positive_rules, $node_affinity_rules) = @_;
+
+ my @conflicts = ();
+
+ while (my ($positiveid, $positive_rule) = each %$positive_rules) {
+ my $found_node_affinity_id;
+ my $positive_resources = $positive_rule->{resources};
+
+ for my $node_affinity_id (keys %$node_affinity_rules) {
+ my $node_affinity_rule = $rules->{ids}->{$node_affinity_id};
+ next if sets_are_disjoint($positive_resources, $node_affinity_rule->{resources});
+
+ # assuming that all $resources have at most one node affinity rule,
+ # take the first found node affinity rule.
+ $node_affinity_rule->{resources}->{$_} = 1 for keys %$positive_resources;
+ last;
+ }
+ }
+}
+
+sub global_canonicalize {
+ my ($class, $rules) = @_;
+
+ my $args = $class->get_check_arguments($rules);
+
+ create_implicit_positive_resource_affinity_node_affinity_rules(
+ $rules,
+ $args->{positive_rules},
+ $args->{node_affinity_rules},
+ );
+}
+
1;
--
2.47.2
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
next prev parent reply other threads:[~2025-08-01 16:22 UTC|newest]
Thread overview: 14+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-08-01 16:22 [pve-devel] [PATCH ha-manager v2 00/12] HA rules follow up (part 1) Daniel Kral
2025-08-01 16:22 ` [pve-devel] [PATCH ha-manager v2 01/12] manager: fix ~revision version check for ha groups migration Daniel Kral
2025-08-01 16:22 ` [pve-devel] [PATCH ha-manager v2 02/12] test: ha tester: add ha groups migration tests with runtime upgrades Daniel Kral
2025-08-01 16:22 ` [pve-devel] [PATCH ha-manager v2 03/12] tree-wide: pass optional parameters as hash values for for_each_rule helper Daniel Kral
2025-08-01 16:22 ` [pve-devel] [PATCH ha-manager v2 04/12] api: rules: add missing return schema for the read_rule api endpoint Daniel Kral
2025-08-01 16:22 ` [pve-devel] [PATCH ha-manager v2 05/12] api: rules: ignore disable parameter if it is set to a falsy value Daniel Kral
2025-08-01 16:22 ` [pve-devel] [PATCH ha-manager v2 06/12] rules: resource affinity: make message in inter-consistency check clearer Daniel Kral
2025-08-01 16:22 ` [pve-devel] [PATCH ha-manager v2 07/12] config, manager: do not check ignored resources with affinity when migrating Daniel Kral
2025-08-01 16:22 ` [pve-devel] [PATCH ha-manager v2 08/12] rules: make positive affinity resources migrate on single resource fail Daniel Kral
2025-08-01 16:22 ` [pve-devel] [PATCH ha-manager v2 09/12] rules: allow same resources in node and resource affinity rules Daniel Kral
2025-08-01 16:22 ` Daniel Kral [this message]
2025-08-01 16:22 ` [pve-devel] [PATCH ha-manager v2 11/12] test: rules: add test cases for inter-plugin checks allowing simple use cases Daniel Kral
2025-08-01 16:22 ` [pve-devel] [PATCH ha-manager v2 12/12] test: ha tester: add resource affinity test cases mixed with node affinity rules Daniel Kral
2025-08-01 17:36 ` [pve-devel] applied: [PATCH ha-manager v2 00/12] HA rules follow up (part 1) Thomas Lamprecht
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20250801162230.296214-11-d.kral@proxmox.com \
--to=d.kral@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox