From 11e713fd0544a0f5f3926c1f3c263c3a6b773b03 Mon Sep 17 00:00:00 2001 From: Chris Burr Date: Tue, 20 Oct 2026 07:01:50 +0200 Subject: [PATCH] fix (WMS): clear the signal mask before starting the payload Creating a gfal2 context leaves most signals (SIGTERM, SIGINT, SIGQUIT, SIGALRM, ...) blocked in the calling thread. The payload inherits the signal mask of the thread that starts it, so it couldn't be stopped with SIGTERM, and Gaudi's EventWatchdog could not abort stuck jobs with SIGQUIT (gaudi/Gaudi#428). --- .../JobWrapper/JobWrapper.py | 4 +++ .../JobWrapper/test/Test_JobWrapper.py | 26 ++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/DIRAC/WorkloadManagementSystem/JobWrapper/JobWrapper.py b/src/DIRAC/WorkloadManagementSystem/JobWrapper/JobWrapper.py index fc3c20f5e9c..692be1984ea 100755 --- a/src/DIRAC/WorkloadManagementSystem/JobWrapper/JobWrapper.py +++ b/src/DIRAC/WorkloadManagementSystem/JobWrapper/JobWrapper.py @@ -17,6 +17,7 @@ import time import datetime import shutil +import signal import threading import tarfile import glob @@ -1512,6 +1513,9 @@ def run(self): """ log = gLogger.getSubLogger(self.__class__.__name__) + # Unblock signals left blocked by libraries such as gfal2, which the payload would inherit + signal.pthread_sigmask(signal.SIG_SETMASK, set()) + # FIXME: why local instances of object variables are created? cmd = self.cmd spObject = self.spObject diff --git a/src/DIRAC/WorkloadManagementSystem/JobWrapper/test/Test_JobWrapper.py b/src/DIRAC/WorkloadManagementSystem/JobWrapper/test/Test_JobWrapper.py index 551d3b70d7a..5ab71ca1801 100644 --- a/src/DIRAC/WorkloadManagementSystem/JobWrapper/test/Test_JobWrapper.py +++ b/src/DIRAC/WorkloadManagementSystem/JobWrapper/test/Test_JobWrapper.py @@ -2,6 +2,8 @@ """ import os import shutil +import signal +import sys import pytest from unittest.mock import MagicMock from pathlib import Path @@ -11,7 +13,9 @@ from DIRAC.DataManagementSystem.Client.test.mock_DM import dm_mock from DIRAC.Resources.Catalog.test.mock_FC import fc_mock -from DIRAC.WorkloadManagementSystem.JobWrapper.JobWrapper import JobWrapper +from DIRAC.Core.Utilities.Subprocess import Subprocess +from DIRAC.WorkloadManagementSystem.JobWrapper import JobWrapper as JobWrapperModule +from DIRAC.WorkloadManagementSystem.JobWrapper.JobWrapper import ExecutionThread, JobWrapper from DIRAC.WorkloadManagementSystem.JobWrapper.Watchdog import Watchdog from DIRAC.WorkloadManagementSystem.Client import JobStatus, JobMinorStatus @@ -29,6 +33,26 @@ def uploadFileMockFunc(**kwargs): gLogger.setLevel("DEBUG") +@pytest.mark.skipif(not sys.platform.startswith("linux"), reason="Reads /proc/self/status") +def test_executionThread_clears_signal_mask(tmp_path): + """The payload must not inherit signals blocked in the JobWrapper (e.g. by gfal2).""" + stdout = tmp_path / "std.out" + blocked = {signal.SIGQUIT, signal.SIGTERM} + exeThread = ExecutionThread( + Subprocess(), "grep SigBlk /proc/self/status", 100, str(stdout), None, os.environ.copy() + ) + oldMask = signal.pthread_sigmask(signal.SIG_BLOCK, blocked) + try: + # The thread inherits the mask of this one when it starts, as it would in the JobWrapper + exeThread.start() + finally: + signal.pthread_sigmask(signal.SIG_SETMASK, oldMask) + exeThread.join() + + assert JobWrapperModule.EXECUTION_RESULT["Thread"]["OK"], JobWrapperModule.EXECUTION_RESULT["Thread"] + assert stdout.read_text().split() == ["SigBlk:", "0000000000000000"] + + def test_InputData(mocker): mocker.patch( "DIRAC.WorkloadManagementSystem.JobWrapper.JobWrapper.getSystemSection", side_effect=getSystemSectionMock