-
Notifications
You must be signed in to change notification settings - Fork 42
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
(9/5) [nexus] Allow anti-affinity members to be affinity groups #7572
Conversation
|
||
// If this affinity group is a member in other anti-affinity |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is tested by affinity_group_delete_group_deletes_membership_in_anti_affinity_groups
below
})?; | ||
|
||
|
||
// Check that the affinity group's members are not reserved. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is tested within anti_affinity_group_membership_add_remove_group_with_vmm
})?; | ||
} | ||
{ | ||
use db::schema::anti_affinity_group_affinity_membership::dsl as member_dsl; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I expanded anti_affinity_group_delete_group_deletes_members
to test this
err.bail_retryable_or_else(e, |e| { | ||
public_error_from_diesel( | ||
e, | ||
ErrorHandler::NotFoundByResource( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I expanded anti_affinity_group_membership_for_deleted_objects
to check for these NotFound
errors
err.bail_retryable_or_else(e, |e| { | ||
public_error_from_diesel( | ||
e, | ||
ErrorHandler::NotFoundByResource( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As mentioned above, I'm testing these NotFound
errors in anti_affinity_group_membership_for_deleted_objects
pagparams: &PaginatedBy<'_>, | ||
) -> ListResultVec<AntiAffinityGroupInstanceMembership> { | ||
pagparams: &DataPageParams<'_, Uuid>, | ||
) -> ListResultVec<external::AntiAffinityGroupMember> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is tested by anti_affinity_group_membership_list_extended
dropshot::PaginationOrder::Descending => false, | ||
}; | ||
|
||
let mut query = QueryBuilder::new() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't use the paginated
macro because it doesn't really support reading from the UNION of two tables, and IMO, it's easier to write the SQL manually that it is to mess with those trait bounds.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm also experimenting with replacing the paginated
macro with some utilities, closer to this implementation, in #7717
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file, as usual, contains the actual changes that we need to consider during instance placement.
I'll try to comment below to explain some of the rationale. For what it's worth, I also consider this to be the most "worth scrutinizing" file.
@@ -57,18 +57,70 @@ pub fn sled_find_targets_query( | |||
COALESCE(SUM(CAST(sled_resource_vmm.reservoir_ram AS INT8)), 0) + " | |||
).param().sql(" <= sled.reservoir_size | |||
), | |||
our_aa_groups AS ( | |||
our_a_groups AS ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
our_a_groups
and our_a_instances
are pulled up from below, because we're now using them when evaluating what the "anti-affinity instances" should be.
ON affinity_group_instance_membership.group_id = our_a_groups.group_id | ||
WHERE instance_id != ").param().sql(" | ||
), | ||
our_direct_aa_groups AS ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I use the term "direct" to mean "this thing belongs as a member within the anti-affinity group we're observing".
I use the term "indirect" below for "this instance is being considered, but only because it's part of an affinity group that is a member within our anti-affinity group"
SELECT group_id | ||
FROM anti_affinity_group_instance_membership | ||
WHERE instance_id = ").param().sql(" | ||
), | ||
other_aa_instances AS ( | ||
other_direct_aa_instances AS ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are instances we're "anti-affine" with, due to their membership in an anti-affinity group our instance also belongs to.
Our Instance | Other Instances |
---|---|
Member in anti-affinity group | Member in anti-affinity group |
WHERE instance_id != ").param().sql(" | ||
), | ||
our_indirect_aa_groups AS ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The input to this whole query is "our instance ID".
We looked up the affinity groups this instance belongs to earlier - that's saved as our_a_groups
.
So this subquery looks up "memberships in any anti-affinity groups, based on this instance's affinity groups".
This is "indirect" because our instance may not directly belong to this anti-affinity group, but we're looking it up because our instance needs to consider it through the affinity-group member relationship.
(The exactly_one_affinity_group
value we return here is used below...)
FROM anti_affinity_group_affinity_membership | ||
WHERE affinity_group_id IN (SELECT group_id FROM our_a_groups) | ||
), | ||
other_indirect_aa_instances_via_instances AS ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are instances we're "anti-affine" with, due to their instance membership in the anti-affinity group and also due to our membership in an affinity group (see: our_indirect_aa_groups
).
Our Instance | Other Instances |
---|---|
Member in affinity group belonging to anti-affinity group | Member in anti-affinity group |
ELSE TRUE | ||
END | ||
), | ||
other_aa_instances AS ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
... and this sums up all the cases of anti-affinity:
Our Instance | Other Instances |
---|---|
Member in anti-affinity group | Member in anti-affinity group |
Member in affinity group belonging to anti-affinity group | Member in anti-affinity group |
Member in affinity group belonging to anti-affinity group | Member in affinity group belonging to anti-affinity group |
JOIN our_indirect_aa_groups | ||
ON anti_affinity_group_instance_membership.group_id = our_indirect_aa_groups.anti_affinity_group_id | ||
), | ||
other_indirect_aa_instances_via_groups AS ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are instances we're "anti-affine" with, due to their group membership in an anti-affinity group and due to our membership in an affinity group that also belongs to that anti-affinity group.
This is basically the "indirect in both directions case". Imagine:
- Affinity group "sheep": inst1, inst2
- Affinity group "wolves": inst3, inst4
- Anti-affinity group "do-not-eat-sheep": "sheep", "wolves"
If we're trying to place "inst1", we are:
- Indirectly anti-affine with "wolves" (see:
our_indirect_aa_groups
) - So we want to look up all the members of "wolves", because we're anti-affine with all of them
Our Instance | Other Instances |
---|---|
Member in affinity group belonging to anti-affinity group | Member in affinity group belonging to anti-affinity group |
We care about exactly_one_affinity_group
here because if we have an instance which belongs to multiple affinity groups that are anti-affine from each other, we need to consider all those indirect instance members.
In the example above, the sheep "inst1" and "inst2" are not anti-affine from each other, because they both only belong to "exactly one affinity group" in the anti-affinity group. This hits the THEN
clause of the CASE
statement below, because we don't consider our instance anti-affine from sheep
.
If we instead imagine the case of:
- Affinity group "sweet": inst1, inst2
- Affinity group "sour": inst1, inst3
- Affinity group "spicy": inst3, inst4
- Anti-affinity group "dont-mix-flavors": "sweet", "sour", "spicy"
If we now try to place "inst1", it belongs to both group "sweet" and group "sour". Here, our_indirect_aa_groups.exactly_one_affinity_group
is false - so, "inst1" is considered anti-affine with all other groups, including ones it belongs to. This hits the ELSE TRUE
clause below, because "inst1" is anti-affine to all indirect groups members here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there's a lot more of this PR i still need to get through, but here are a few notes from the first couple files...i haven't actually gotten to the meat yet.
#[derive(Queryable, Insertable, Clone, Debug, Selectable)] | ||
#[diesel(table_name = anti_affinity_group_affinity_membership)] | ||
pub struct AntiAffinityGroupAffinityMembership { | ||
pub anti_affinity_group_id: DbTypedUuid<AntiAffinityGroupKind>, | ||
pub affinity_group_id: DbTypedUuid<AffinityGroupKind>, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Definitely a fan of making this a separate table, rather than changing the existing table to have nullable instance and anti-affinity group ID fields.
impl AntiAffinityGroupAffinityMembership { | ||
pub fn new( | ||
anti_affinity_group_id: AntiAffinityGroupUuid, | ||
affinity_group_id: AffinityGroupUuid, | ||
) -> Self { | ||
Self { | ||
anti_affinity_group_id: anti_affinity_group_id.into(), | ||
affinity_group_id: affinity_group_id.into(), | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is maybe a bit goofy of me, but for stuff like this where there's a clear directional relationship (the anti-affinity group is a member of the affinity group),i kinda wonder whether we ought to have a method on AntiAffinityGroupUuid
be the constructor, so you can write
let membership = anti_affinity_group_id.is_member_of(affinity_group_id);
and have that relationship be clearly stated at the call site? On the other hand, this might just be annoying and deviate from the convention for constructors for no real reason. It's the type of thing I would have reached for in my misspent youth[^1] which makes me kind of leery of this suggestion...
[^1] i.e. back when i wrote Scala
dropshot::PaginationOrder::Descending => false, | ||
}; | ||
|
||
let mut query = QueryBuilder::new() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpicky: is it worth, perhaps, factoring the raw query builder out from the code that actually runs it, so we can have an expectorate test for it?
.sql( | ||
"SELECT id,label FROM ( | ||
SELECT instance_id as id, 'instance' as label | ||
FROM anti_affinity_group_instance_membership | ||
WHERE group_id = ", | ||
) | ||
.param() | ||
.bind::<diesel::sql_types::Uuid, _>(authz_anti_affinity_group.id()) | ||
.sql( | ||
" | ||
UNION | ||
SELECT affinity_group_id as id, 'affinity_group' as label | ||
FROM anti_affinity_group_affinity_membership | ||
WHERE anti_affinity_group_id = ", | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this use of union
plus a label to zip together two tables is quite cute, it kinda makes me want to rewrite some code in my webhooks branch...
use external::AntiAffinityGroupMember as Member; | ||
match label.as_str() { | ||
"affinity_group" => Ok(Member::AffinityGroup( | ||
AffinityGroupUuid::from_untyped_uuid(id), | ||
)), | ||
"instance" => Ok(Member::Instance( | ||
InstanceUuid::from_untyped_uuid(id), | ||
)), | ||
other => Err(external::Error::internal_error(&format!( | ||
"Unexpected label from database query: {other}" | ||
))), | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is kind of a silly idea, but: could we avoid the "other" case if we defined a SQL enum for these labels? not sure if it's actually worth the effort to do that, and having a type that doesn't actually appear in any tables is kinda weird, so take it or leave it.
let err = err.clone(); | ||
use db::schema::anti_affinity_group::dsl as anti_affinity_group_dsl; | ||
use db::schema::affinity_group::dsl as affinity_group_dsl; | ||
use db::schema::affinity_group_instance_membership::dsl as a_instance_membership_dsl; | ||
use db::schema::anti_affinity_group_affinity_membership::dsl as aa_affinity_membership_dsl; | ||
use db::schema::sled_resource_vmm::dsl as resource_dsl; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
turbo nit, sorry: i usually expect all value-level code in a block to be below all the imports in that block, so i found the let err = err.clone();
above the imports a bit easy to miss. i might prefer:
let err = err.clone(); | |
use db::schema::anti_affinity_group::dsl as anti_affinity_group_dsl; | |
use db::schema::affinity_group::dsl as affinity_group_dsl; | |
use db::schema::affinity_group_instance_membership::dsl as a_instance_membership_dsl; | |
use db::schema::anti_affinity_group_affinity_membership::dsl as aa_affinity_membership_dsl; | |
use db::schema::sled_resource_vmm::dsl as resource_dsl; | |
use db::schema::anti_affinity_group::dsl as anti_affinity_group_dsl; | |
use db::schema::affinity_group::dsl as affinity_group_dsl; | |
use db::schema::affinity_group_instance_membership::dsl as a_instance_membership_dsl; | |
use db::schema::anti_affinity_group_affinity_membership::dsl as aa_affinity_membership_dsl; | |
use db::schema::sled_resource_vmm::dsl as resource_dsl; | |
let err = err.clone(); |
on the other hand, i will be the first to admit that i'm probably being obnoxious about this, so feel free to disregard me.
// Check that the anti-affinity group exists | ||
anti_affinity_group_dsl::anti_affinity_group | ||
.filter(anti_affinity_group_dsl::time_deleted.is_null()) | ||
.filter(anti_affinity_group_dsl::id.eq(authz_anti_affinity_group.id())) | ||
.select(anti_affinity_group_dsl::id) | ||
.first_async::<uuid::Uuid>(&conn) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this instead be implemented using the existing DatastoreCollection
machinery, which IIUC is mostly for dealing with this sort of thing?
SELECT anti_affinity_group_id AS group_id,instance_id | ||
FROM affinity_group_instance_membership | ||
JOIN our_indirect_aa_groups | ||
ON affinity_group_instance_membership.group_id = our_indirect_aa_groups.affinity_group_id |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm probably completely missing something here, but I'm not quite following how this subquery works. If I may propose a slightly different example, suppose we have
- affinity group Outcasts contains instances Janis and Damian
- affinity group Plastics contains instances Regina, Gretchen, and Karen
- anti-affinity group Dislikes contains affinity groups Outcasts and Plastics (but no instances)
Suppose I'm starting instance Janis. Then (IIUC):
our_indirect_aa_groups
will contain one row:(Dislikes, Outcasts, TRUE)
; the other membership,(Dislikes, Plastics)
, is excluded by this subquery's WHERE clauseother_indirect_aa_instances_via_instances
will be empty
Next, the query for other_indirect_aa_instances_via_groups
will end up SELECTing from
affinity group instance membership
JOIN our_indirect_aa_groups
ON affinity_group_instance_membership.group_id = our_indirect_aa_groups.affinity_group_id
The rows of affinity_group_instance_membership
are
Outcasts, Janis
Outcasts, Damian
Plastics, Regina
Plastics, Gretchen
Plastics, Karen
And the rows of our_indirect_aa_groups
are
Dislikes, Outcasts, TRUE
So the SELECT on the joined table will produce two rows:
Outcasts, Janis
Outcasts, Damian
This seems like the inverse of what we wanted, since this is the subquery that needs to add Regina, Gretchen, and Karen to the anti-affine instance set. I think my brain is expecting the right side of the join to refer to a list of the other cliques affinity groups that are members of the anti-affinity groups in which our instance is transitively a member.
What am I overlooking here?
So, in my attempt to follow-up on this, I:
So:
|
Expands group membership for anti-affinity groups to allow both:
... to be anti-affine from each other.
Without this feature, putting instances in an anti-affinity group would force them all to be separate from
one another. However, this can be unsatisfactory when multiple instances in that anti-affinity group do not care
if they are co-located.
As an example: Suppose you have a collection of several VMMs each representing a vertical deployment of a stack, and you want redundancy between these groups at a "collection of VMMs" layer. It's fine - and sometimes desirable - for everything in that vertical deployment to be co-located. However, it's the "groups of VMMs" that need to be isolated from each other. With this PR, one could put each of these "VMM collections" into distinct affinity groups, and put each of those affinity groups within an anti-affinity group.
Fixes #7567