@@ -230,44 +230,43 @@ def _update_metric(engine):
230230 else :
231231 self .assertEqual (handler ._default_iteration_log .call_count , 2 ) # 2 = len([1, 3]) from event_filter
232232
233+ @staticmethod
234+ def _train_func (engine , batch ):
235+ return [batch + 1.0 ]
236+
233237 def test_system_metrics_disabled_by_default (self ):
234238 """
235- Test that a handler left at its default settings does not sample the system metrics.
239+ Test that a handler left at its default settings does not sample the system metrics,
240+ even where mlflow is able to.
236241 """
237242 with tempfile .TemporaryDirectory () as tempdir :
238-
239- def _train_func (engine , batch ):
240- return [batch + 1.0 ]
241-
242- engine = Engine (_train_func )
243+ engine = Engine (self ._train_func )
243244 test_path = os .path .join (tempdir , "mlflow_system_metrics_off" )
244245 handler = MLFlowHandler (iteration_log = False , tracking_uri = path_to_uri (test_path ), close_on_complete = True )
245- handler .attach (engine )
246- engine .run (range (3 ), max_epochs = 1 )
246+ with (
247+ patch ("monai.handlers.mlflow_handler.SystemMetricsMonitor" ) as monitor_class ,
248+ patch ("monai.handlers.mlflow_handler.has_system_metrics" , True ),
249+ ):
250+ handler .attach (engine )
251+ engine .run (range (3 ), max_epochs = 1 )
247252
248- self .assertIsNone (handler .system_metrics_monitor )
249- run = handler .client .get_run (handler .cur_run .info .run_id ) if handler .cur_run else None
250- self .assertIsNone (run )
253+ monitor_class .assert_not_called ()
251254
252255 def test_system_metrics_monitor_life_cycle (self ):
253256 """
254257 Test that the monitor samples the run of the handler with the requested settings,
255258 and stops when the workflow completes.
256259 """
257260 with tempfile .TemporaryDirectory () as tempdir :
258-
259- def _train_func (engine , batch ):
260- return [batch + 1.0 ]
261-
262- engine = Engine (_train_func )
261+ engine = Engine (self ._train_func )
263262 test_path = os .path .join (tempdir , "mlflow_system_metrics" )
264263 handler = MLFlowHandler (
265264 iteration_log = False ,
266265 tracking_uri = path_to_uri (test_path ),
267266 log_system_metrics = True ,
268267 system_metrics_sampling_interval = 1 ,
269268 system_metrics_samples_before_logging = 1 ,
270- close_on_complete = True ,
269+ close_on_complete = False ,
271270 )
272271 monitor = MagicMock ()
273272 with (
@@ -279,24 +278,22 @@ def _train_func(engine, batch):
279278
280279 # the monitor samples the run of the handler, with the requested sampling settings
281280 monitor_class .assert_called_once ()
281+ self .assertEqual (monitor_class .call_args .args [0 ], handler .cur_run .info .run_id )
282282 self .assertEqual (monitor_class .call_args .kwargs ["sampling_interval" ], 1 )
283283 self .assertEqual (monitor_class .call_args .kwargs ["samples_before_logging" ], 1 )
284284 monitor .start .assert_called_once ()
285285 # the sampling is stopped when the workflow completes
286286 monitor .finish .assert_called_once ()
287287 self .assertIsNone (handler .system_metrics_monitor )
288+ handler .close ()
288289
289290 def test_system_metrics_monitor_shared_by_handlers (self ):
290291 """
291292 Test that handlers sharing a run sample it once, and that the run keeps being sampled
292293 until the handler that started the sampling completes.
293294 """
294295 with tempfile .TemporaryDirectory () as tempdir :
295-
296- def _train_func (engine , batch ):
297- return [batch + 1.0 ]
298-
299- engine = Engine (_train_func )
296+ engine = Engine (self ._train_func )
300297 test_path = os .path .join (tempdir , "mlflow_system_metrics_shared" )
301298 # a workflow attaches one handler per engine, all of them sharing a run
302299 handlers = [
@@ -313,8 +310,13 @@ def _train_func(engine, batch):
313310 for handler in handlers :
314311 handler .start (engine )
315312
313+ run_ids = {handler .cur_run .info .run_id for handler in handlers }
314+ self .assertEqual (len (run_ids ), 1 )
315+
316316 # the run is sampled by the first handler only
317317 monitor_class .assert_called_once ()
318+ self .assertEqual (monitor_class .call_args .args [0 ], run_ids .pop ())
319+ monitor .start .assert_called_once ()
318320
319321 # the handlers that do not sample the run leave it running when they complete
320322 for handler in handlers [1 :]:
@@ -328,17 +330,13 @@ def _train_func(engine, batch):
328330 for handler in handlers :
329331 handler .close ()
330332
331- def test_system_metrics_warns_when_mlflow_is_too_old (self ):
333+ def test_system_metrics_warns_when_unavailable (self ):
332334 """
333- Test that a workflow still runs, with a warning, when the installed mlflow cannot
334- record the system metrics.
335+ Test that a workflow still runs, with a warning, when the installed mlflow does not
336+ support recording the system metrics.
335337 """
336338 with tempfile .TemporaryDirectory () as tempdir :
337-
338- def _train_func (engine , batch ):
339- return [batch + 1.0 ]
340-
341- engine = Engine (_train_func )
339+ engine = Engine (self ._train_func )
342340 test_path = os .path .join (tempdir , "mlflow_system_metrics_unavailable" )
343341 handler = MLFlowHandler (
344342 iteration_log = False ,
@@ -347,11 +345,65 @@ def _train_func(engine, batch):
347345 close_on_complete = True ,
348346 )
349347 with patch ("monai.handlers.mlflow_handler.has_system_metrics" , False ):
350- with self .assertWarns (Warning ):
348+ with self .assertWarnsRegex (Warning , "Please install mlflow>=2.8.0 to record the system metrics." ):
349+ handler .attach (engine )
350+ engine .run (range (3 ), max_epochs = 1 )
351+
352+ self .assertIsNone (handler .system_metrics_monitor )
353+
354+ def test_system_metrics_start_failure_does_not_stop_the_workflow (self ):
355+ """
356+ Test that a workflow still runs, with a warning, when the monitor cannot be started.
357+ """
358+ with tempfile .TemporaryDirectory () as tempdir :
359+ engine = Engine (self ._train_func )
360+ test_path = os .path .join (tempdir , "mlflow_system_metrics_start_failure" )
361+ handler = MLFlowHandler (
362+ iteration_log = False ,
363+ tracking_uri = path_to_uri (test_path ),
364+ log_system_metrics = True ,
365+ close_on_complete = True ,
366+ )
367+ with (
368+ patch (
369+ "monai.handlers.mlflow_handler.SystemMetricsMonitor" , side_effect = RuntimeError ("no monitor for you" )
370+ ),
371+ patch ("monai.handlers.mlflow_handler.has_system_metrics" , True ),
372+ ):
373+ with self .assertWarnsRegex (Warning , "Failed to record the system metrics" ):
374+ handler .attach (engine )
375+ engine .run (range (3 ), max_epochs = 1 )
376+
377+ self .assertEqual (engine .state .epoch , 1 )
378+ self .assertIsNone (handler .system_metrics_monitor )
379+ self .assertEqual (len (MLFlowHandler ._monitored_run_ids ), 0 )
380+
381+ def test_system_metrics_stop_failure_is_reported (self ):
382+ """
383+ Test that a monitor which fails to stop is reported and released, so that the run can
384+ be sampled again.
385+ """
386+ with tempfile .TemporaryDirectory () as tempdir :
387+ engine = Engine (self ._train_func )
388+ test_path = os .path .join (tempdir , "mlflow_system_metrics_stop_failure" )
389+ handler = MLFlowHandler (
390+ iteration_log = False ,
391+ tracking_uri = path_to_uri (test_path ),
392+ log_system_metrics = True ,
393+ close_on_complete = True ,
394+ )
395+ monitor = MagicMock ()
396+ monitor .finish .side_effect = RuntimeError ("monitor will not stop" )
397+ with (
398+ patch ("monai.handlers.mlflow_handler.SystemMetricsMonitor" , return_value = monitor ),
399+ patch ("monai.handlers.mlflow_handler.has_system_metrics" , True ),
400+ ):
401+ with self .assertWarnsRegex (Warning , "Failed to stop recording the system metrics" ):
351402 handler .attach (engine )
352403 engine .run (range (3 ), max_epochs = 1 )
353404
354405 self .assertIsNone (handler .system_metrics_monitor )
406+ self .assertEqual (len (MLFlowHandler ._monitored_run_ids ), 0 )
355407
356408 def test_system_metrics_settings_are_validated (self ):
357409 """
0 commit comments