Skip to content

Commit 608905a

Browse files
committed
be less clever about internal disk sorting (explicit ID type)
1 parent 01ebe83 commit 608905a

File tree

3 files changed

+90
-7
lines changed

3 files changed

+90
-7
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sled-agent/config-reconciler/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ omicron-workspace-hack.workspace = true
3636

3737
[dev-dependencies]
3838
omicron-test-utils.workspace = true
39+
proptest.workspace = true
40+
test-strategy.workspace = true
3941

4042
[features]
4143
testing = []

sled-agent/config-reconciler/src/internal_disks.rs

Lines changed: 86 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
//! * During sled-agent startup, before trust quorum has unlocked
1111
1212
use camino::Utf8PathBuf;
13+
use core::cmp;
1314
use id_map::IdMap;
1415
use id_map::IdMappable;
1516
use omicron_common::disk::DiskIdentity;
@@ -252,15 +253,13 @@ struct InternalDiskDetails {
252253
}
253254

254255
impl IdMappable for InternalDiskDetails {
255-
type Id = (bool, Arc<DiskIdentity>);
256+
type Id = InternalDiskDetailsId;
256257

257258
fn id(&self) -> Self::Id {
258-
// Using `!self.is_boot_disk` as the first part of the ID allows us to
259-
// guarantee we always iterate over the internal disks with the current
260-
// boot disk first.
261-
//
262-
// TODO-cleanup add tests for this
263-
(!self.is_boot_disk, Arc::clone(&self.identity))
259+
InternalDiskDetailsId {
260+
identity: Arc::clone(&self.identity),
261+
is_boot_disk: self.is_boot_disk,
262+
}
264263
}
265264
}
266265

@@ -291,6 +290,36 @@ impl From<&'_ Disk> for InternalDiskDetails {
291290
}
292291
}
293292

293+
// Special ID type for `InternalDiskDetails` that lets us guarantee we sort boot
294+
// disks ahead of non-boot disks. There's a whole set of thorny problems here
295+
// about what to do if we think both internal disks should have the same
296+
// contents but they disagree; we'll at least try to have callers prefer the
297+
// boot disk if they're performing a "check the first disk that succeeds" kind
298+
// of operation.
299+
#[derive(Debug, Clone, PartialEq, Eq)]
300+
struct InternalDiskDetailsId {
301+
is_boot_disk: bool,
302+
identity: Arc<DiskIdentity>,
303+
}
304+
305+
impl cmp::Ord for InternalDiskDetailsId {
306+
fn cmp(&self, other: &Self) -> cmp::Ordering {
307+
match self.is_boot_disk.cmp(&other.is_boot_disk) {
308+
cmp::Ordering::Equal => {}
309+
// `false` normally sorts before `true`, so intentionally reverse
310+
// this so that we sort boot disks ahead of non-boot disks.
311+
ord => return ord.reverse(),
312+
}
313+
self.identity.cmp(&other.identity)
314+
}
315+
}
316+
317+
impl cmp::PartialOrd for InternalDiskDetailsId {
318+
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
319+
Some(self.cmp(other))
320+
}
321+
}
322+
294323
struct InternalDisksTask {
295324
disks_tx: watch::Sender<Arc<IdMap<InternalDiskDetails>>>,
296325
raw_disks_rx: watch::Receiver<IdMap<RawDiskWithId>>,
@@ -307,3 +336,53 @@ impl InternalDisksTask {
307336
unimplemented!()
308337
}
309338
}
339+
340+
#[cfg(test)]
341+
mod tests {
342+
use super::*;
343+
use omicron_uuid_kinds::ZpoolUuid;
344+
use proptest::sample::size_range;
345+
use test_strategy::Arbitrary;
346+
use test_strategy::proptest;
347+
348+
#[derive(Debug, Arbitrary)]
349+
struct ArbitraryInternalDiskDetailsId {
350+
is_boot_disk: bool,
351+
vendor: String,
352+
model: String,
353+
serial: String,
354+
}
355+
356+
#[proptest]
357+
fn boot_disks_sort_ahead_of_non_boot_disks_in_id_map(
358+
#[any(size_range(2..4).lift())] values: Vec<
359+
ArbitraryInternalDiskDetailsId,
360+
>,
361+
) {
362+
let disk_map: IdMap<_> = values
363+
.into_iter()
364+
.map(|value| InternalDiskDetails {
365+
identity: Arc::new(DiskIdentity {
366+
vendor: value.vendor,
367+
model: value.model,
368+
serial: value.serial,
369+
}),
370+
zpool_name: ZpoolName::new_internal(ZpoolUuid::new_v4()),
371+
is_boot_disk: value.is_boot_disk,
372+
slot: None,
373+
raw_devfs_path: None,
374+
})
375+
.collect();
376+
377+
// When iterating over the contents, we should never see a boot disk
378+
// after a non-boot disk.
379+
let mut saw_non_boot_disk = false;
380+
for disk in disk_map.iter() {
381+
if disk.is_boot_disk {
382+
assert!(!saw_non_boot_disk);
383+
} else {
384+
saw_non_boot_disk = true;
385+
}
386+
}
387+
}
388+
}

0 commit comments

Comments
 (0)