@@ -175,3 +175,173 @@ impl SitrepGc {
175175 status
176176 }
177177}
178+
179+ #[ cfg( test) ]
180+ mod tests {
181+ use super :: * ;
182+ use chrono:: Utc ;
183+ use nexus_db_queries:: db:: datastore:: fm:: InsertSitrepError ;
184+ use nexus_db_queries:: db:: pub_test_utils:: TestDatabase ;
185+ use nexus_types:: fm;
186+ use omicron_common:: api:: external:: Error ;
187+ use omicron_test_utils:: dev;
188+ use omicron_uuid_kinds:: CollectionUuid ;
189+ use omicron_uuid_kinds:: OmicronZoneUuid ;
190+ use omicron_uuid_kinds:: SitrepUuid ;
191+ use std:: collections:: BTreeSet ;
192+
193+ #[ tokio:: test]
194+ async fn test_orphaned_sitrep_gc ( ) {
195+ let logctx = dev:: test_setup_log ( "test_orphaned_sitrep_gc" ) ;
196+ let db = TestDatabase :: new_with_datastore ( & logctx. log ) . await ;
197+ let ( opctx, datastore) = ( db. opctx ( ) , db. datastore ( ) ) ;
198+
199+ let mut task = SitrepGc :: new ( datastore. clone ( ) ) ;
200+
201+ // First, insert an initial sitrep. This should succeed.
202+ let sitrep1 = fm:: Sitrep {
203+ metadata : fm:: SitrepMetadata {
204+ id : SitrepUuid :: new_v4 ( ) ,
205+ inv_collection_id : CollectionUuid :: new_v4 ( ) ,
206+ creator_id : OmicronZoneUuid :: new_v4 ( ) ,
207+ comment : "test sitrep v1" . to_string ( ) ,
208+ time_created : Utc :: now ( ) ,
209+ parent_sitrep_id : None ,
210+ } ,
211+ } ;
212+ datastore
213+ . fm_sitrep_insert ( & opctx, & sitrep1)
214+ . await
215+ . expect ( "inserting initial sitrep should succeed" ) ;
216+
217+ // Now, create some orphaned sitreps which also have no parent.
218+ let mut orphans = BTreeSet :: new ( ) ;
219+ for i in 1 ..5 {
220+ insert_orphan ( & datastore, & opctx, & mut orphans, None , 1 , i) . await ;
221+ }
222+
223+ // Next, create a new sitrep which descends from sitrep 1.
224+ let sitrep2 = fm:: Sitrep {
225+ metadata : fm:: SitrepMetadata {
226+ id : SitrepUuid :: new_v4 ( ) ,
227+ inv_collection_id : CollectionUuid :: new_v4 ( ) ,
228+ creator_id : OmicronZoneUuid :: new_v4 ( ) ,
229+ comment : "test sitrep v2" . to_string ( ) ,
230+ time_created : Utc :: now ( ) ,
231+ parent_sitrep_id : Some ( sitrep1. metadata . id ) ,
232+ } ,
233+ } ;
234+ datastore
235+ . fm_sitrep_insert ( & opctx, & sitrep2)
236+ . await
237+ . expect ( "inserting child sitrep should succeed" ) ;
238+
239+ // Now, create some orphaned sitreps which also descend from sitrep 1.
240+ for i in 1 ..4 {
241+ insert_orphan (
242+ & datastore,
243+ & opctx,
244+ & mut orphans,
245+ Some ( sitrep1. metadata . id ) ,
246+ 2 ,
247+ i,
248+ )
249+ . await ;
250+ }
251+
252+ // Make sure the orphans exist.
253+ for & id in & orphans {
254+ match datastore. fm_sitrep_metadata_read ( & opctx, id) . await {
255+ Ok ( _) => { }
256+ Err ( Error :: NotFound { .. } ) => {
257+ panic ! ( "orphaned sitrep {id} should exist" ) ;
258+ }
259+ Err ( e) => {
260+ panic ! (
261+ "unexpected error reading orphaned sitrep {id}: {e}"
262+ ) ;
263+ }
264+ }
265+ }
266+
267+ // Activate the background task.
268+ let status = dbg ! ( task. actually_activate( opctx) . await ) ;
269+
270+ // Now, the orphans should all be gone.
271+ for & id in & orphans {
272+ match datastore. fm_sitrep_metadata_read ( & opctx, id) . await {
273+ Ok ( _) => {
274+ panic ! (
275+ "orphaned sitrep {id} should have been deleted, \
276+ but it appears to still exist!"
277+ )
278+ }
279+ Err ( Error :: NotFound { .. } ) => {
280+ // Okay, it's gone.
281+ }
282+ Err ( e) => {
283+ panic ! (
284+ "unexpected error reading orphaned sitrep {id}: {e}"
285+ ) ;
286+ }
287+ }
288+ }
289+ // But the non-orphaned sitreps should still be there!
290+ datastore
291+ . fm_sitrep_metadata_read ( & opctx, sitrep1. id ( ) )
292+ . await
293+ . expect ( "sitrep 1 should still exist" ) ;
294+ datastore
295+ . fm_sitrep_metadata_read ( & opctx, sitrep2. id ( ) )
296+ . await
297+ . expect ( "sitrep 2 should still exist" ) ;
298+
299+ assert_eq ! ( status. errors, Vec :: <String >:: new( ) ) ;
300+ assert_eq ! ( status. versions_scanned, 2 ) ;
301+ assert_eq ! (
302+ status. orphaned_sitreps. get( & 1 ) ,
303+ Some ( & OrphanedSitreps { found: 4 , deleted: 4 } )
304+ ) ;
305+ assert_eq ! (
306+ status. orphaned_sitreps. get( & 2 ) ,
307+ Some ( & OrphanedSitreps { found: 3 , deleted: 3 } )
308+ ) ;
309+
310+ db. terminate ( ) . await ;
311+ logctx. cleanup_successful ( ) ;
312+ }
313+
314+ async fn insert_orphan (
315+ datastore : & DataStore ,
316+ opctx : & OpContext ,
317+ orphans : & mut BTreeSet < SitrepUuid > ,
318+ parent_sitrep_id : Option < SitrepUuid > ,
319+ v : usize ,
320+ i : usize ,
321+ ) {
322+ let sitrep = fm:: Sitrep {
323+ metadata : fm:: SitrepMetadata {
324+ id : SitrepUuid :: new_v4 ( ) ,
325+ inv_collection_id : CollectionUuid :: new_v4 ( ) ,
326+ creator_id : OmicronZoneUuid :: new_v4 ( ) ,
327+ comment : format ! ( "test sitrep v{i}; orphan {i}" ) ,
328+ time_created : Utc :: now ( ) ,
329+ parent_sitrep_id,
330+ } ,
331+ } ;
332+ match datastore. fm_sitrep_insert ( & opctx, & sitrep) . await {
333+ Ok ( _) => {
334+ panic ! ( "inserting sitrep v{v} orphan {i} should not succeed" )
335+ }
336+ Err ( InsertSitrepError :: ParentNotCurrent ( id) ) => {
337+ orphans. insert ( id) ;
338+ }
339+ Err ( InsertSitrepError :: Other ( e) ) => {
340+ panic ! (
341+ "expected inserting sitrep v{v} orphan {i} to fail because \
342+ its parent is out of date, but saw an unexpected error: {e}"
343+ ) ;
344+ }
345+ }
346+ }
347+ }
0 commit comments