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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.