@@ -197,6 +197,17 @@ impl DataStore {
197197 . map ( |_| ( ) )
198198 }
199199
200+ /// Lists all orphaned alternative sitreps for the provided
201+ /// [`fm::SitrepVersion`].
202+ ///
203+ /// Orphaned sitreps at a given version are those which descend from the
204+ /// same parent sitrep as the current sitrep at that version, but which were
205+ /// not committed successfully to the sitrep history.
206+ ///
207+ /// Note that this operation is only performed relative to a committed
208+ /// sitrep version. This is in order to prevent the returned list of sitreps
209+ /// from including sitreps which are in the process of being prepared, by
210+ /// only performing it on versions earlier than the current version.
200211 pub async fn fm_sitrep_list_orphaned (
201212 & self ,
202213 opctx : & OpContext ,
@@ -566,9 +577,11 @@ mod tests {
566577 use crate :: db:: explain:: ExplainableAsync ;
567578 use crate :: db:: pub_test_utils:: TestDatabase ;
568579 use chrono:: Utc ;
580+ use nexus_types:: fm;
569581 use omicron_test_utils:: dev;
570582 use omicron_uuid_kinds:: CollectionUuid ;
571583 use omicron_uuid_kinds:: OmicronZoneUuid ;
584+ use std:: collections:: BTreeSet ;
572585
573586 #[ tokio:: test]
574587 async fn explain_insert_sitrep_version_query ( ) {
@@ -834,4 +847,143 @@ mod tests {
834847 db. terminate ( ) . await ;
835848 logctx. cleanup_successful ( ) ;
836849 }
850+
851+ #[ tokio:: test]
852+ async fn test_sitrep_list_orphaned ( ) {
853+ let logctx = dev:: test_setup_log ( "test_sitrep_list_orphaned" ) ;
854+ let db = TestDatabase :: new_with_datastore ( & logctx. log ) . await ;
855+ let ( opctx, datastore) = ( db. opctx ( ) , db. datastore ( ) ) ;
856+
857+ // First, insert an initial sitrep. This should succeed.
858+ let sitrep1 = fm:: Sitrep {
859+ metadata : fm:: SitrepMetadata {
860+ id : SitrepUuid :: new_v4 ( ) ,
861+ inv_collection_id : CollectionUuid :: new_v4 ( ) ,
862+ creator_id : OmicronZoneUuid :: new_v4 ( ) ,
863+ comment : "test sitrep v1" . to_string ( ) ,
864+ time_created : Utc :: now ( ) ,
865+ parent_sitrep_id : None ,
866+ } ,
867+ } ;
868+ datastore
869+ . fm_sitrep_insert ( & opctx, & sitrep1)
870+ . await
871+ . expect ( "inserting initial sitrep should succeed" ) ;
872+
873+ // Now, create some orphaned sitreps which also have no parent.
874+ let mut orphans_v1 = BTreeSet :: new ( ) ;
875+ for i in 1 ..5 {
876+ insert_orphan ( & datastore, & opctx, & mut orphans_v1, None , 1 , i)
877+ . await ;
878+ }
879+
880+ // List orphans at the current version.
881+ let v1 = datastore
882+ . fm_current_sitrep_version ( & opctx)
883+ . await
884+ . unwrap ( )
885+ . expect ( "should have a version" ) ;
886+ assert_eq ! ( dbg!( & v1) . id, sitrep1. metadata. id) ;
887+ let listed_orphans = datastore
888+ . fm_sitrep_list_orphaned ( & opctx, & v1)
889+ . await
890+ . expect ( "listing orphans should succeed" )
891+ . iter ( )
892+ . map ( |sitrep| sitrep. id )
893+ . collect :: < BTreeSet < _ > > ( ) ;
894+ assert_eq ! ( dbg!( & listed_orphans) , & orphans_v1) ;
895+
896+ // Next, create a new sitrep which descends from sitrep 0.
897+ let sitrep2 = fm:: Sitrep {
898+ metadata : fm:: SitrepMetadata {
899+ id : SitrepUuid :: new_v4 ( ) ,
900+ inv_collection_id : CollectionUuid :: new_v4 ( ) ,
901+ creator_id : OmicronZoneUuid :: new_v4 ( ) ,
902+ comment : "test sitrep v2" . to_string ( ) ,
903+ time_created : Utc :: now ( ) ,
904+ parent_sitrep_id : Some ( sitrep1. metadata . id ) ,
905+ } ,
906+ } ;
907+ datastore
908+ . fm_sitrep_insert ( & opctx, & sitrep2)
909+ . await
910+ . expect ( "inserting child sitrep should succeed" ) ;
911+
912+ // Now, create some orphaned sitreps which also descend from sitreo 0.
913+ let mut orphans_v2 = BTreeSet :: new ( ) ;
914+ for i in 1 ..5 {
915+ insert_orphan (
916+ & datastore,
917+ & opctx,
918+ & mut orphans_v2,
919+ Some ( sitrep1. metadata . id ) ,
920+ 2 ,
921+ i,
922+ )
923+ . await ;
924+ }
925+
926+ // List orphans at the current version.
927+ let v2 = datastore
928+ . fm_current_sitrep_version ( & opctx)
929+ . await
930+ . unwrap ( )
931+ . expect ( "should have a version" ) ;
932+ assert_eq ! ( dbg!( & v2) . id, sitrep2. metadata. id) ;
933+ let listed_orphans_v2 = datastore
934+ . fm_sitrep_list_orphaned ( & opctx, & v2)
935+ . await
936+ . expect ( "listing orphans at v2 should succeed" )
937+ . iter ( )
938+ . map ( |sitrep| sitrep. id )
939+ . collect :: < BTreeSet < _ > > ( ) ;
940+ assert_eq ! ( dbg!( & listed_orphans_v2) , & orphans_v2) ;
941+ // Orphans at the prior version should also be listable, and it should
942+ // not include the committed sitrep at that version.
943+ let listed_orphans_v1 = datastore
944+ . fm_sitrep_list_orphaned ( & opctx, & v1)
945+ . await
946+ . expect ( "listing orphans at v1 should succeed" )
947+ . iter ( )
948+ . map ( |sitrep| sitrep. id )
949+ . collect :: < BTreeSet < _ > > ( ) ;
950+ assert_eq ! ( dbg!( & listed_orphans_v1) , & orphans_v1) ;
951+
952+ db. terminate ( ) . await ;
953+ logctx. cleanup_successful ( ) ;
954+ }
955+
956+ async fn insert_orphan (
957+ datastore : & DataStore ,
958+ opctx : & OpContext ,
959+ orphans : & mut BTreeSet < SitrepUuid > ,
960+ parent_sitrep_id : Option < SitrepUuid > ,
961+ v : usize ,
962+ i : usize ,
963+ ) {
964+ let sitrep = fm:: Sitrep {
965+ metadata : fm:: SitrepMetadata {
966+ id : SitrepUuid :: new_v4 ( ) ,
967+ inv_collection_id : CollectionUuid :: new_v4 ( ) ,
968+ creator_id : OmicronZoneUuid :: new_v4 ( ) ,
969+ comment : format ! ( "test sitrep v{i}; orphan {i}" ) ,
970+ time_created : Utc :: now ( ) ,
971+ parent_sitrep_id,
972+ } ,
973+ } ;
974+ match datastore. fm_sitrep_insert ( & opctx, & sitrep) . await {
975+ Ok ( _) => {
976+ panic ! ( "inserting sitrep v{v} orphan {i} should not succeed" )
977+ }
978+ Err ( InsertSitrepError :: ParentNotCurrent ( id) ) => {
979+ orphans. insert ( id) ;
980+ }
981+ Err ( InsertSitrepError :: Other ( e) ) => {
982+ panic ! (
983+ "expected inserting sitrep v{v} orphan {i} to fail because \
984+ its parent is out of date, but saw an unexpected error: {e}"
985+ ) ;
986+ }
987+ }
988+ }
837989}
0 commit comments