[Git][ghc/ghc][wip/sjakobi/T27547-testsuite-robustness-reword] 3 commits: testsuite: Don't let a straggling test process hang the whole run
Simon Jakobi pushed to branch wip/sjakobi/T27547-testsuite-robustness-reword at Glasgow Haskell Compiler / GHC Commits: 9c373e02 by Simon Jakobi at 2026-08-16T12:27:56+02:00 testsuite: Don't let a straggling test process hang the whole run A test process that outlives the timeout program's direct child -- for instance a forkProcess child that deadlocks during RTS shutdown (#27547) -- keeps the inherited stdout/stderr pipes open. The driver then blocks forever waiting for EOF, with no timeout left to break it, so a single bad test hangs the entire testsuite run. timeout.py now kills the test's whole process group once its direct child has been reaped. The child already puts itself in a fresh group via setpgrp(). T12903 in the threaded nonmoving ways is the concrete case. Running it alone used to hang the driver indefinitely, leaving the deadlocked child alive and holding the pipes. Now the run completes and the test passes. Assisted-by: Claude Fable 5 - - - - - 798f43df by Simon Jakobi at 2026-08-16T12:27:56+02:00 testsuite: Mark T25280 broken in threaded nonmoving ways (#27547) Under the threaded RTS with -xn, a forkProcess child inherits the nonmoving collector's state but not its nonmoving-mark thread, and deadlocks during RTS shutdown. Of the testsuite's forkProcess tests, only T25280 waits for its children, so it is the only one that observes the deadlock (as a timeout). T12903, T24672 and hpc_fork pass either way. Add a nonmoving_threaded_ways group and expect T25280 to be broken there, so the fix shows up as an unexpected pass. Assisted-by: Claude Fable 5 - - - - - 14fdef2e by Simon Jakobi at 2026-08-16T12:37:28+02:00 Make stopInterp wait for the external interpreter to exit stopInterp sent Shutdown and returned immediately, so GHC could exit while the interpreter process was still shutting down. Any buffered output the interpreter produced on GHC's behalf (e.g. from a TH splice writing to the shared stdout) was then lost if the process was killed before flushing on exit. The testsuite does exactly that since the straggler kill in timeout.py, which made T25155 flaky on slower CI machines. Wait for the process to exit after sending Shutdown, bounded by a timeout in case it is wedged. Context: #27547 Assisted-by: Claude Fable 5 - - - - - 4 changed files: - compiler/GHC/Runtime/Interpreter.hs - testsuite/config/ghc - testsuite/tests/rts/all.T - testsuite/timeout/timeout.py Changes: ===================================== compiler/GHC/Runtime/Interpreter.hs ===================================== @@ -113,6 +113,7 @@ import qualified GHC.Exts.Heap as Heap import GHC.Stack.CCS (CostCentre,CostCentreStack) import System.Directory import System.Process +import System.Timeout (timeout) import qualified GHC.InfoProv as InfoProv import GHC.Builtin.Modules( gHC_PRIM, gHC_PRIMOPWRAPPERS ) @@ -652,10 +653,16 @@ stopInterp interp = case interpInstance interp of case state of InterpPending -> pure state -- already stopped InterpRunning i -> do - ex <- getProcessExitCode (interpHandle (instProcess i)) - if isJust ex - then pure () - else sendMessage i Shutdown + let hdl = interpHandle (instProcess i) + ex <- getProcessExitCode hdl + unless (isJust ex) $ do + sendMessage i Shutdown + -- The interpreter process shares our stdout/stderr; wait for + -- it to exit (flushing its output), lest output it produced + -- on our behalf be lost if it is killed once we exit. + -- Bounded, in case the process is wedged. + _ <- timeout 5000000 {- 5s -} (waitForProcess hdl) + pure () pure InterpPending -- ----------------------------------------------------------------------------- ===================================== testsuite/config/ghc ===================================== @@ -197,6 +197,11 @@ debug_ways = [x[0] for x in config.way_flags.items() threaded_ways = [x[0] for x in config.way_flags.items() if '-threaded' in x[1] or 'ghci' == x[0] or 'ghci-opt' == x[0]] +# #27547 +nonmoving_threaded_ways = [name for name, flags in config.way_flags.items() + if '-threaded' in flags + if '-xn' in config.way_rts_flags.get(name, [])] + # Ways which run with multiple capabilities concurrent_ways = [name for name, flags in config.way_flags.items() if '-threaded' in flags or 'ghci' == name or 'ghci-opt' == name ===================================== testsuite/tests/rts/all.T ===================================== @@ -668,7 +668,11 @@ test('IOManager', [js_skip, when(arch('wasm32'), skip), when(opsys('mingw32'), s test('T24142', [req_target_smp], compile_and_run, ['-threaded -with-rtsopts "-N2"']) test('T25232', [unless(have_profiling(), skip), only_ways(['normal','nonmoving','nonmoving_prof','nonmoving_thr_prof']), extra_ways(['nonmoving', 'nonmoving_prof'] + (['nonmoving_thr_prof'] if have_threaded() else []))], compile_and_run, ['']) -test('T25280', [unless(opsys('linux'),skip),req_process,js_skip], compile_and_run, ['']) +# T25280 waits for its forked children, so it detects the forkProcess +# child-shutdown deadlock of #27547 (by timing out). +test('T25280', [unless(opsys('linux'),skip),req_process,js_skip, + expect_broken_for(27547, nonmoving_threaded_ways)], + compile_and_run, ['']) # N.B. This will likely issue a warning on stderr but we merely care that the # program doesn't crash. ===================================== testsuite/timeout/timeout.py ===================================== @@ -27,6 +27,19 @@ try: else: raise e + def killStragglers(pid): + # A test can leave descendants that outlive its main process (e.g. a + # deadlocked forkProcess child, #27547). They hold the inherited + # stdout/stderr pipes open and would hang the testsuite driver, so + # kill the whole process group. + try: + os.killpg(pid, signal.SIGKILL) + except OSError as e: + # ESRCH: group already gone. EPERM: macOS quirk — signalling a + # group whose leader is a reaped zombie. + if e.errno not in (errno.ESRCH, errno.EPERM): + raise e + pid = os.fork() if pid == 0: # child @@ -40,6 +53,9 @@ try: old = signal.signal(signal.SIGALRM, handler) signal.alarm(secs) (pid2, res) = os.waitpid(pid, 0) + # The handler signals pid, so it must not run once pid is reaped. + signal.alarm(0) + killStragglers(pid) if (os.WIFEXITED(res)): sys.exit(os.WEXITSTATUS(res)) elif os.WIFSIGNALED(res): View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/compare/49ef4cd7aafb781988f952e9f8ea626... -- View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/compare/49ef4cd7aafb781988f952e9f8ea626... You're receiving this email because of your account on gitlab.haskell.org. Manage all notifications: https://gitlab.haskell.org/-/profile/notifications | Help: https://gitlab.haskell.org/help
participants (1)
-
Simon Jakobi (@sjakobi)