Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/newsfragments/3283_changed.prevent_parallel_run.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Prevent parallel remote testplan executions on same runpath.
44 changes: 44 additions & 0 deletions testplan/common/remote/remote_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ def _prepare_remote(self) -> None:
self._check_remote_os()

self._define_remote_dirs()
self._check_remote_pidfile()
self._create_remote_dirs()

if self.cfg.push:
Expand All @@ -273,6 +274,9 @@ def _define_remote_dirs(self) -> None:
self._remote_runid_file = os.path.join(
self._remote_plan_runpath, self._get_plan().runid_filename
)
self._remote_pid_file = os.path.join(
self._remote_plan_runpath, "testplan.pid"
)

self._remote_resource_runpath = rebase_path(
self.runpath,
Expand Down Expand Up @@ -317,6 +321,41 @@ def _remote_working_dir(self) -> str:
self._workspace_paths.remote,
)

def _check_remote_pidfile(self) -> None:
"""Check remote PID file and block if another testplan process is running on the same runpath."""

status, stdout, _ = self._ssh_client.exec_command(
cmd=f"/bin/cat {self._remote_pid_file}",
label="read remote pid file",
check=False,
)

if status:
# No PID file found, no other process is using the runpath
return

hostname, pid = (
stdout.strip().split(";")
if ";" in stdout
else (None, stdout.strip())
)
if pid and pid.isdigit():
if hostname:
ssh_client = SSHClient(hostname)
else:
ssh_client = self._ssh_client

check_status, _, _ = ssh_client.exec_command(
cmd=f"/bin/ps -p {pid}",
label="check remote pid running",
check=False,
)

if check_status == 0:
raise RuntimeError(
f"Another testplan instance (hostname: {hostname or self.cfg.remote_host}, pid: {pid}) is already using the same runpath"
)

def _create_remote_dirs(self) -> None:
"""Create mandatory directories in remote host."""

Expand Down Expand Up @@ -347,6 +386,11 @@ def _create_remote_dirs(self) -> None:
label="create remote runid file",
)

self._ssh_client.exec_command(
cmd=f'/bin/echo "{os.uname()[1]};{os.getpid()}" > {self._remote_pid_file}',
label="create initial pid file",
)

# corner cases:
# - local workspace under remote runpath
# more?
Expand Down
4 changes: 4 additions & 0 deletions testplan/runnable/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1316,6 +1316,10 @@ def make_runpath_dirs(self):
makeemptydirs(self._runpath)
makeemptydirs(self._scratch)

self.pidfile_path = os.path.join(self._runpath, "testplan.pid")
with open(self.pidfile_path, "w") as pid_file:
pid_file.write(str(os.getpid()))

with open(
os.path.join(self._runpath, self.runid_filename), "wb"
) as fp:
Expand Down