@@ -301,32 +301,71 @@ async fn test_system_timeseries_schema_list(
301301 . expect ( "Failed to find HTTP request latency histogram schema" ) ;
302302}
303303
304- pub async fn timeseries_query (
304+ pub async fn timeseries_query_until_success (
305305 cptestctx : & ControlPlaneTestContext < omicron_nexus:: Server > ,
306306 query : impl ToString ,
307307) -> Vec < oxql_types:: Table > {
308- execute_timeseries_query ( cptestctx, "/v1/system/timeseries/query" , query)
309- . await
308+ timeseries_query_until_success_ (
309+ cptestctx,
310+ "/v1/system/timeseries/query" ,
311+ query,
312+ )
313+ . await
310314}
311315
312- pub async fn project_timeseries_query (
316+ pub async fn project_timeseries_query_until_success (
313317 cptestctx : & ControlPlaneTestContext < omicron_nexus:: Server > ,
314318 project : & str ,
315319 query : impl ToString ,
316320) -> Vec < oxql_types:: Table > {
317- execute_timeseries_query (
321+ timeseries_query_until_success_ (
318322 cptestctx,
319323 & format ! ( "/v1/timeseries/query?project={}" , project) ,
320324 query,
321325 )
322326 . await
323327}
324328
325- async fn execute_timeseries_query (
329+ /// Run an OxQL query until it succeeds or panics.
330+ async fn timeseries_query_until_success_ (
326331 cptestctx : & ControlPlaneTestContext < omicron_nexus:: Server > ,
327332 endpoint : & str ,
328333 query : impl ToString ,
329334) -> Vec < oxql_types:: Table > {
335+ const POLL_INTERVAL : Duration = Duration :: from_secs ( 1 ) ;
336+ const POLL_MAX : Duration = Duration :: from_secs ( 30 ) ;
337+ let query_ = query. to_string ( ) ;
338+ wait_for_condition (
339+ || async {
340+ match execute_timeseries_query ( cptestctx, endpoint, & query_) . await {
341+ Some ( r) => Ok ( r) ,
342+ None => Err ( CondCheckError :: < ( ) > :: NotYet ) ,
343+ }
344+ } ,
345+ & POLL_INTERVAL ,
346+ & POLL_MAX ,
347+ )
348+ . await
349+ . unwrap_or_else ( |_| {
350+ panic ! (
351+ "Timeseries named in query are not available \
352+ after {:?}, query: '{}'",
353+ POLL_MAX ,
354+ query. to_string( ) ,
355+ )
356+ } )
357+ }
358+
359+ /// Run an OxQL query.
360+ ///
361+ /// This returns `None` if the query resulted in client error and the body
362+ /// indicates that a timeseries named in the query could not be found. In all
363+ /// other cases, it either succeeds or panics.
364+ pub async fn execute_timeseries_query (
365+ cptestctx : & ControlPlaneTestContext < omicron_nexus:: Server > ,
366+ endpoint : & str ,
367+ query : impl ToString ,
368+ ) -> Option < Vec < oxql_types:: Table > > {
330369 // first, make sure the latest timeseries have been collected.
331370 cptestctx
332371 . oximeter
@@ -353,14 +392,29 @@ async fn execute_timeseries_query(
353392 . unwrap_or_else ( |e| {
354393 panic ! ( "timeseries query failed: {e:?}\n query: {query}" )
355394 } ) ;
356- rsp. parsed_body :: < OxqlQueryResult > ( )
357- . unwrap_or_else ( |e| {
358- panic ! (
359- "could not parse timeseries query response: {e:?}\n \
360- query: {query}\n response: {rsp:#?}"
361- ) ;
362- } )
363- . tables
395+
396+ // Check for a timeseries-not-found error specifically.
397+ if rsp. status . is_client_error ( ) {
398+ let text = std:: str:: from_utf8 ( & rsp. body )
399+ . expect ( "Timeseries query response body should be UTF-8" ) ;
400+ if text. contains ( "Schema for timeseries" ) && text. contains ( "not found" )
401+ {
402+ return None ;
403+ }
404+ }
405+
406+ // Try to parse the query as usual, which will fail on other kinds of
407+ // errors.
408+ Some (
409+ rsp. parsed_body :: < OxqlQueryResult > ( )
410+ . unwrap_or_else ( |e| {
411+ panic ! (
412+ "could not parse timeseries query response: {e:?}\n \
413+ query: {query}\n response: {rsp:#?}"
414+ ) ;
415+ } )
416+ . tables ,
417+ )
364418}
365419
366420#[ nexus_test]
@@ -467,7 +521,7 @@ async fn test_instance_watcher_metrics(
467521 // activate the instance watcher background task.
468522 activate_instance_watcher ( ) . await ;
469523
470- let metrics = timeseries_query ( & cptestctx, OXQL_QUERY ) . await ;
524+ let metrics = timeseries_query_until_success ( & cptestctx, OXQL_QUERY ) . await ;
471525 let checks = metrics
472526 . iter ( )
473527 . find ( |t| t. name ( ) == "virtual_machine:check" )
@@ -483,7 +537,7 @@ async fn test_instance_watcher_metrics(
483537 // activate the instance watcher background task.
484538 activate_instance_watcher ( ) . await ;
485539
486- let metrics = timeseries_query ( & cptestctx, OXQL_QUERY ) . await ;
540+ let metrics = timeseries_query_until_success ( & cptestctx, OXQL_QUERY ) . await ;
487541 let checks = metrics
488542 . iter ( )
489543 . find ( |t| t. name ( ) == "virtual_machine:check" )
@@ -500,7 +554,7 @@ async fn test_instance_watcher_metrics(
500554 // activate the instance watcher background task.
501555 activate_instance_watcher ( ) . await ;
502556
503- let metrics = timeseries_query ( & cptestctx, OXQL_QUERY ) . await ;
557+ let metrics = timeseries_query_until_success ( & cptestctx, OXQL_QUERY ) . await ;
504558 let checks = metrics
505559 . iter ( )
506560 . find ( |t| t. name ( ) == "virtual_machine:check" )
@@ -525,7 +579,7 @@ async fn test_instance_watcher_metrics(
525579 // activate the instance watcher background task.
526580 activate_instance_watcher ( ) . await ;
527581
528- let metrics = timeseries_query ( & cptestctx, OXQL_QUERY ) . await ;
582+ let metrics = timeseries_query_until_success ( & cptestctx, OXQL_QUERY ) . await ;
529583 let checks = metrics
530584 . iter ( )
531585 . find ( |t| t. name ( ) == "virtual_machine:check" )
@@ -554,7 +608,7 @@ async fn test_instance_watcher_metrics(
554608 // activate the instance watcher background task.
555609 activate_instance_watcher ( ) . await ;
556610
557- let metrics = timeseries_query ( & cptestctx, OXQL_QUERY ) . await ;
611+ let metrics = timeseries_query_until_success ( & cptestctx, OXQL_QUERY ) . await ;
558612 let checks = metrics
559613 . iter ( )
560614 . find ( |t| t. name ( ) == "virtual_machine:check" )
@@ -599,42 +653,57 @@ async fn test_project_timeseries_query(
599653 // Query with no project specified
600654 let q1 = "get virtual_machine:check" ;
601655
602- let result = project_timeseries_query ( & cptestctx, "project1" , q1) . await ;
656+ let result =
657+ project_timeseries_query_until_success ( & cptestctx, "project1" , q1)
658+ . await ;
603659 assert_eq ! ( result. len( ) , 1 ) ;
604660 assert ! ( result[ 0 ] . timeseries( ) . len( ) > 0 ) ;
605661
606662 // also works with project ID
607- let result =
608- project_timeseries_query ( & cptestctx, & p1. identity . id . to_string ( ) , q1)
609- . await ;
663+ let result = project_timeseries_query_until_success (
664+ & cptestctx,
665+ & p1. identity . id . to_string ( ) ,
666+ q1,
667+ )
668+ . await ;
610669 assert_eq ! ( result. len( ) , 1 ) ;
611670 assert ! ( result[ 0 ] . timeseries( ) . len( ) > 0 ) ;
612671
613- let result = project_timeseries_query ( & cptestctx, "project2" , q1) . await ;
672+ let result =
673+ project_timeseries_query_until_success ( & cptestctx, "project2" , q1)
674+ . await ;
614675 assert_eq ! ( result. len( ) , 1 ) ;
615676 assert ! ( result[ 0 ] . timeseries( ) . len( ) > 0 ) ;
616677
617678 // with project specified
618679 let q2 = & format ! ( "{} | filter project_id == \" {}\" " , q1, p1. identity. id) ;
619680
620- let result = project_timeseries_query ( & cptestctx, "project1" , q2) . await ;
681+ let result =
682+ project_timeseries_query_until_success ( & cptestctx, "project1" , q2)
683+ . await ;
621684 assert_eq ! ( result. len( ) , 1 ) ;
622685 assert ! ( result[ 0 ] . timeseries( ) . len( ) > 0 ) ;
623686
624- let result = project_timeseries_query ( & cptestctx, "project2" , q2) . await ;
687+ let result =
688+ project_timeseries_query_until_success ( & cptestctx, "project2" , q2)
689+ . await ;
625690 assert_eq ! ( result. len( ) , 1 ) ;
626691 assert_eq ! ( result[ 0 ] . timeseries( ) . len( ) , 0 ) ;
627692
628693 // with instance specified
629694 let q3 = & format ! ( "{} | filter instance_id == \" {}\" " , q1, i1. identity. id) ;
630695
631696 // project containing instance gives me something
632- let result = project_timeseries_query ( & cptestctx, "project1" , q3) . await ;
697+ let result =
698+ project_timeseries_query_until_success ( & cptestctx, "project1" , q3)
699+ . await ;
633700 assert_eq ! ( result. len( ) , 1 ) ;
634701 assert_eq ! ( result[ 0 ] . timeseries( ) . len( ) , 1 ) ;
635702
636703 // should be empty or error
637- let result = project_timeseries_query ( & cptestctx, "project2" , q3) . await ;
704+ let result =
705+ project_timeseries_query_until_success ( & cptestctx, "project2" , q3)
706+ . await ;
638707 assert_eq ! ( result. len( ) , 1 ) ;
639708 assert_eq ! ( result[ 0 ] . timeseries( ) . len( ) , 0 ) ;
640709
@@ -868,7 +937,7 @@ async fn test_mgs_metrics(
868937 . try_force_collect ( )
869938 . await
870939 . expect ( "Could not force oximeter collection" ) ;
871- let table = timeseries_query ( & cptestctx, & query)
940+ let table = timeseries_query_until_success ( & cptestctx, & query)
872941 . await
873942 . into_iter ( )
874943 . find ( |t| t. name ( ) == name)
0 commit comments