@@ -10,6 +10,7 @@ use nexus_db_queries::db::model::RendezvousDebugDataset;
1010use  nexus_db_queries:: db:: DataStore ; 
1111use  nexus_types:: deployment:: BlueprintDatasetConfig ; 
1212use  nexus_types:: deployment:: BlueprintDatasetDisposition ; 
13+ use  nexus_types:: internal_api:: background:: DebugDatasetsRendezvousStats ; 
1314use  omicron_common:: api:: internal:: shared:: DatasetKind ; 
1415use  omicron_uuid_kinds:: BlueprintUuid ; 
1516use  omicron_uuid_kinds:: DatasetUuid ; 
@@ -23,7 +24,7 @@ pub(crate) async fn reconcile_debug_datasets(
2324    blueprint_id :  BlueprintUuid , 
2425    blueprint_datasets :  impl  Iterator < Item  = & BlueprintDatasetConfig > , 
2526    inventory_datasets :  & BTreeSet < DatasetUuid > , 
26- )  -> anyhow:: Result < ( ) >  { 
27+ )  -> anyhow:: Result < DebugDatasetsRendezvousStats >  { 
2728    // We expect basically all executions of this task to do nothing: we're 
2829    // activated periodically, and only do work when a dataset has been 
2930    // newly-added or newly-expunged. 
@@ -39,27 +40,42 @@ pub(crate) async fn reconcile_debug_datasets(
3940        . map ( |d| ( d. id ( ) ,  d) ) 
4041        . collect :: < BTreeMap < _ ,  _ > > ( ) ; 
4142
43+     let  mut  stats = DebugDatasetsRendezvousStats :: default ( ) ; 
44+ 
4245    for  dataset in  blueprint_datasets. filter ( |d| d. kind  == DatasetKind :: Debug )  { 
4346        match  dataset. disposition  { 
4447            BlueprintDatasetDisposition :: InService  => { 
4548                // Only attempt to insert this dataset if it has shown up in 
4649                // inventory (required for correctness) and isn't already 
4750                // present in the db (performance optimization only). Inserting 
4851                // an already-present row is a no-op, so it's safe to skip. 
49-                 if  inventory_datasets. contains ( & dataset. id ) 
50-                     && !existing_db_datasets. contains_key ( & dataset. id ) 
51-                 { 
52+                 if  existing_db_datasets. contains_key ( & dataset. id )  { 
53+                     stats. num_already_exist  += 1 ; 
54+                 }  else  if  !inventory_datasets. contains ( & dataset. id )  { 
55+                     stats. num_not_in_inventory  += 1 ; 
56+                 }  else  { 
5257                    let  db_dataset = RendezvousDebugDataset :: new ( 
5358                        dataset. id , 
5459                        dataset. pool . id ( ) , 
5560                        blueprint_id, 
5661                    ) ; 
57-                     datastore
62+                     let  did_insert =  datastore
5863                        . debug_dataset_insert_if_not_exists ( opctx,  db_dataset) 
5964                        . await 
6065                        . with_context ( || { 
6166                            format ! ( "failed to insert dataset {}" ,  dataset. id) 
62-                         } ) ?; 
67+                         } ) ?
68+                         . is_some ( ) ; 
69+ 
70+                     if  did_insert { 
71+                         stats. num_inserted  += 1 ; 
72+                     }  else  { 
73+                         // This means we hit the TOCTOU race mentioned above: 
74+                         // when we queried the DB this row didn't exist, but 
75+                         // another Nexus must have beat us to actually inserting 
76+                         // it. 
77+                         stats. num_already_exist  += 1 ; 
78+                     } 
6379                } 
6480            } 
6581            BlueprintDatasetDisposition :: Expunged  => { 
@@ -81,7 +97,9 @@ pub(crate) async fn reconcile_debug_datasets(
8197                    . get ( & dataset. id ) 
8298                    . map ( |d| d. is_tombstoned ( ) ) 
8399                    . unwrap_or ( false ) ; 
84-                 if  !already_tombstoned { 
100+                 if  already_tombstoned { 
101+                     stats. num_already_tombstoned  += 1 ; 
102+                 }  else  { 
85103                    if  datastore
86104                        . debug_dataset_tombstone ( 
87105                            opctx, 
@@ -96,17 +114,23 @@ pub(crate) async fn reconcile_debug_datasets(
96114                            ) 
97115                        } ) ?
98116                    { 
117+                         stats. num_tombstoned  += 1 ; 
99118                        info ! ( 
100119                            opctx. log,  "tombstoned expunged dataset" ; 
101120                            "dataset_id"  => %dataset. id, 
102121                        ) ; 
122+                     }  else  { 
123+                         // Similar TOCTOU race lost as above; this dataset was 
124+                         // either already tombstoned by another racing Nexus, or 
125+                         // has been hard deleted. 
126+                         stats. num_already_tombstoned  += 1 ; 
103127                    } 
104128                } 
105129            } 
106130        } 
107131    } 
108132
109-     Ok ( ( ) ) 
133+     Ok ( stats ) 
110134} 
111135
112136#[ cfg( test) ]  
@@ -213,15 +237,15 @@ mod tests {
213237        ) ) | { 
214238            let  blueprint_id = BlueprintUuid :: new_v4( ) ; 
215239
216-             let  datastore_datasets = runtime. block_on( async  { 
240+             let  ( result_stats ,   datastore_datasets)  = runtime. block_on( async  { 
217241                let  ( blueprint_datasets,  inventory_datasets)  = proptest_do_prep( 
218242                    opctx, 
219243                    datastore, 
220244                    blueprint_id, 
221245                    & prep, 
222246                ) . await ; 
223247
224-                 reconcile_debug_datasets( 
248+                 let  result_stats =  reconcile_debug_datasets( 
225249                    opctx, 
226250                    datastore, 
227251                    blueprint_id, 
@@ -231,15 +255,19 @@ mod tests {
231255                . await 
232256                . expect( "reconciled debug dataset" ) ; 
233257
234-                  datastore
258+                  let  datastore_datasets =  datastore
235259                    . debug_dataset_list_all_batched( opctx) 
236260                    . await 
237261                    . unwrap( ) 
238262                    . into_iter( ) 
239263                    . map( |d| ( d. id( ) ,  d) ) 
240-                     . collect:: <BTreeMap <_,  _>>( ) 
264+                     . collect:: <BTreeMap <_,  _>>( ) ; 
265+ 
266+                  ( result_stats,  datastore_datasets) 
241267            } ) ; 
242268
269+             let  mut  expected_stats = DebugDatasetsRendezvousStats :: default ( ) ; 
270+ 
243271            for  ( id,  prep)  in prep { 
244272                let  id:  DatasetUuid  = u32_to_id( id) ; 
245273
@@ -252,6 +280,32 @@ mod tests {
252280                    prep. disposition == ArbitraryDisposition :: InService ; 
253281                let  in_inventory = prep. in_inventory; 
254282
283+                 // Validate rendezvous output 
284+                 match  ( in_db_before,  in_service,  in_inventory)  { 
285+                     // "Not in database and expunged" is consistent with "hard 
286+                     // deleted", which we can't separate from "already 
287+                     // tombstoned". 
288+                     ( false ,  false ,  _)  => { 
289+                         expected_stats. num_already_tombstoned += 1 ; 
290+                     } 
291+                     // "In database and expunged" should result in tombstoning. 
292+                     ( true ,  false ,  _)  => { 
293+                         expected_stats. num_tombstoned += 1 ; 
294+                     } 
295+                     // In service but already existed 
296+                     ( true ,  true ,  _)  => { 
297+                         expected_stats. num_already_exist += 1 ; 
298+                     } 
299+                     // In service, not in db yet, but not in inventory 
300+                     ( false ,  true ,  false )  => { 
301+                         expected_stats. num_not_in_inventory += 1 ; 
302+                     } 
303+                     // In service, not in db yet, present in inventory 
304+                     ( false ,  true ,  true )  => { 
305+                         expected_stats. num_inserted += 1 ; 
306+                     } 
307+                 } 
308+ 
255309                // Validate database state 
256310                match  ( in_db_before,  in_service,  in_inventory)  { 
257311                    // Wasn't in DB, isn't in service: should still not be in db 
@@ -298,6 +352,8 @@ mod tests {
298352                    } 
299353                } 
300354            } 
355+ 
356+             assert_eq!( result_stats,  expected_stats) ; 
301357        } ) ; 
302358
303359        runtime. block_on ( db. terminate ( ) ) ; 
0 commit comments