3636
3737_GRACEFUL_SHUTDOWN_PATH = Path (__file__ ).parent .parent / "graceful_shutdown.py"
3838
39- # Provide a minimal dynamo._core stub so the module can be loaded
40- _dynamo_stub = types .ModuleType ("dynamo" )
39+ # Provide a minimal dynamo._core stub while loading graceful_shutdown.py without
40+ # importing the native extension. The stub is restored immediately after module
41+ # load so pytest package collection can still import the real runtime bindings.
4142_dynamo_core_stub = types .ModuleType ("dynamo._core" )
4243_dynamo_core_stub .DistributedRuntime = object
43- sys .modules .setdefault ("dynamo" , _dynamo_stub )
44- sys .modules .setdefault ("dynamo._core" , _dynamo_core_stub )
44+ _previous_dynamo = sys .modules .get ("dynamo" )
45+ _previous_dynamo_core = sys .modules .get ("dynamo._core" )
46+ _added_dynamo_stub = False
47+ if importlib .util .find_spec ("dynamo" ) is None :
48+ _dynamo_stub = types .ModuleType ("dynamo" )
49+ _dynamo_stub .__path__ = [] # mark as package for dynamo._core imports
50+ sys .modules ["dynamo" ] = _dynamo_stub
51+ _added_dynamo_stub = True
52+ sys .modules ["dynamo._core" ] = _dynamo_core_stub
4553
4654
4755def _load_graceful_shutdown ():
@@ -55,8 +63,20 @@ def _load_graceful_shutdown():
5563
5664
5765_gs = _load_graceful_shutdown ()
66+ if _previous_dynamo_core is None :
67+ sys .modules .pop ("dynamo._core" , None )
68+ else :
69+ sys .modules ["dynamo._core" ] = _previous_dynamo_core
70+ if _added_dynamo_stub :
71+ if _previous_dynamo is None :
72+ sys .modules .pop ("dynamo" , None )
73+ else :
74+ sys .modules ["dynamo" ] = _previous_dynamo
75+
5876graceful_shutdown_with_discovery = _gs .graceful_shutdown_with_discovery
5977install_signal_handlers = _gs .install_signal_handlers
78+ is_shutdown_in_progress = _gs .is_shutdown_in_progress
79+ fast_failover_exit_enabled = _gs .fast_failover_exit_enabled
6080
6181
6282# ---------------------------------------------------------------------------
@@ -309,3 +329,75 @@ async def _run():
309329
310330 asyncio .run (_run ())
311331 mock_runtime .shutdown .assert_called_once ()
332+
333+
334+ def test_shutdown_in_progress_reflects_active_shutdown ():
335+ """The shutdown-in-progress flag is visible before shutdown_event is set."""
336+ assert is_shutdown_in_progress () is False
337+
338+ async def _run ():
339+ mock_runtime = MagicMock ()
340+ mock_endpoint = AsyncMock ()
341+ mock_endpoint .unregister_endpoint_instance = AsyncMock (return_value = None )
342+ await graceful_shutdown_with_discovery (
343+ runtime = mock_runtime ,
344+ endpoints = [mock_endpoint ],
345+ shutdown_event = None ,
346+ grace_period_s = 0 ,
347+ )
348+
349+ asyncio .run (_run ())
350+
351+ assert is_shutdown_in_progress () is True
352+
353+
354+ def test_fast_failover_exit_env_parsing (monkeypatch ):
355+ monkeypatch .delenv ("DYN_GMS_FAILOVER_FAST_EXIT_ON_SIGTERM" , raising = False )
356+ assert fast_failover_exit_enabled () is False
357+
358+ for value in ("1" , "true" , "yes" , "on" , "enabled" ):
359+ monkeypatch .setenv ("DYN_GMS_FAILOVER_FAST_EXIT_ON_SIGTERM" , value )
360+ assert fast_failover_exit_enabled () is True
361+
362+ for value in ("" , "0" , "false" , "no" , "off" ):
363+ monkeypatch .setenv ("DYN_GMS_FAILOVER_FAST_EXIT_ON_SIGTERM" , value )
364+ assert fast_failover_exit_enabled () is False
365+
366+
367+ def test_fast_failover_exit_runs_after_unregister_before_runtime_shutdown (monkeypatch ):
368+ class FastExit (Exception ):
369+ pass
370+
371+ call_order = []
372+ shutdown_event = asyncio .Event ()
373+ mock_runtime = MagicMock ()
374+ mock_runtime .shutdown = MagicMock (side_effect = lambda : call_order .append ("shutdown" ))
375+
376+ async def _run ():
377+ mock_endpoint = AsyncMock ()
378+ mock_endpoint .unregister_endpoint_instance = AsyncMock (
379+ side_effect = lambda : call_order .append ("unregister" )
380+ )
381+
382+ def fake_fast_exit (event ):
383+ assert event is shutdown_event
384+ event .set ()
385+ call_order .append ("fast_exit" )
386+ raise FastExit
387+
388+ monkeypatch .setenv ("DYN_GMS_FAILOVER_FAST_EXIT_ON_SIGTERM" , "1" )
389+ monkeypatch .setattr (_gs , "_fast_exit_after_failover_unregister" , fake_fast_exit )
390+
391+ await graceful_shutdown_with_discovery (
392+ runtime = mock_runtime ,
393+ endpoints = [mock_endpoint ],
394+ shutdown_event = shutdown_event ,
395+ grace_period_s = 0 ,
396+ )
397+
398+ with pytest .raises (FastExit ):
399+ asyncio .run (_run ())
400+
401+ assert call_order == ["unregister" , "fast_exit" ]
402+ assert shutdown_event .is_set ()
403+ mock_runtime .shutdown .assert_not_called ()
0 commit comments