Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improving port being busy detection #2603

Merged
merged 11 commits into from
Jan 5, 2024
16 changes: 16 additions & 0 deletions src/ansys/mapdl/core/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,22 @@ def __init__(self, msg=""):
RuntimeError.__init__(self, msg)


class PortAlreadyInUse(MapdlDidNotStart):
"""Error when the port is already occupied"""

def __init__(self, msg="The port {port} is already being used.", port=50052):
MapdlDidNotStart.__init__(self, msg.format(port=port))


class PortAlreadyInUseByAnMAPDLInstance(PortAlreadyInUse):
"""Error when the port is already occupied"""

def __init__(
self, msg="The port {port} is already used by an MAPDL instance.", port=50052
):
PortAlreadyInUse.__init__(self, msg.format(port=port))


class MapdlConnectionError(RuntimeError):
"""Provides the error when connecting to the MAPDL instance fails."""

Expand Down
103 changes: 79 additions & 24 deletions src/ansys/mapdl/core/launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@
import tempfile
import threading
import time
from typing import TYPE_CHECKING, Tuple, Union
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union
import warnings

import psutil

try:
import ansys.platform.instancemanagement as pypim

Expand All @@ -57,6 +59,8 @@
LockFileException,
MapdlDidNotStart,
MapdlRuntimeError,
PortAlreadyInUse,
PortAlreadyInUseByAnMAPDLInstance,
VersionError,
)
from ansys.mapdl.core.licensing import ALLOWABLE_LICENSES, LicenseChecker
Expand Down Expand Up @@ -113,7 +117,7 @@
GALLERY_INSTANCE = [None]


def _cleanup_gallery_instance(): # pragma: no cover
def _cleanup_gallery_instance() -> None: # pragma: no cover
"""This cleans up any left over instances of MAPDL from building the gallery."""
if GALLERY_INSTANCE[0] is not None:
mapdl = MapdlGrpc(
Expand All @@ -126,7 +130,7 @@
atexit.register(_cleanup_gallery_instance)


def _is_ubuntu():
def _is_ubuntu() -> bool:
"""Determine if running as Ubuntu.

It's a bit complicated because sometimes the distribution is
Expand Down Expand Up @@ -157,7 +161,7 @@
return "ubuntu" in platform.platform().lower()


def close_all_local_instances(port_range=None):
def close_all_local_instances(port_range: range = None) -> None:
"""Close all MAPDL instances within a port_range.

This function can be used when cleaning up from a failed pool or
Expand All @@ -181,7 +185,8 @@
port_range = range(50000, 50200)

@threaded
def close_mapdl(port, name="Closing mapdl thread."):
def close_mapdl(port: Union[int, str], name: str = "Closing mapdl thread."):

Check warning on line 188 in src/ansys/mapdl/core/launcher.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/mapdl/core/launcher.py#L188

Added line #L188 was not covered by tests
# Name argument is used by the threaded decorator.
try:
mapdl = MapdlGrpc(port=port, set_no_abort=False)
mapdl.exit()
Expand All @@ -194,21 +199,34 @@
close_mapdl(port)


def check_ports(port_range, ip="localhost"):
def check_ports(port_range: range, ip: str = "localhost") -> List[int]:
"""Check the state of ports in a port range"""
ports = {}
for port in port_range:
ports[port] = port_in_use(port, ip)
return ports


def port_in_use(port, host=LOCALHOST):
def port_in_use(port: Union[int, str], host: str = LOCALHOST) -> bool:
"""Returns True when a port is in use at the given host.

Must actually "bind" the address. Just checking if we can create
a socket is insufficient as it's possible to run into permission
errors like:

- An attempt was made to access a socket in a way forbidden by its
access permissions.
"""
return port_in_use_using_socket(port, host) or port_in_use_using_psutil(port)


def port_in_use_using_socket(port: Union[int, str], host: str) -> bool:
"""Returns True when a port is in use at the given host using socket librry.

Must actually "bind" the address. Just checking if we can create
a socket is insufficient as it's possible to run into permission
errors like:

- An attempt was made to access a socket in a way forbidden by its
access permissions.
"""
Expand All @@ -220,21 +238,49 @@
return True


def is_ansys_process(proc: psutil.Process) -> bool:
"""Check if the given process is an Ansys MAPDL process"""
return (
proc.name().lower().startswith(("ansys", "mapdl")) and "-grpc" in proc.cmdline()
)


def get_process_at_port(port) -> Optional[psutil.Process]:
"""Get the process (psutil.Process) running at the given port"""
for proc in psutil.process_iter():
for conns in proc.connections(kind="inet"):
if conns.laddr.port == port:
return proc
return None


def port_in_use_using_psutil(port: Union[int, str]) -> bool:
"""Returns True when a port is in use at the given host using psutil.

This function iterate over all the process, and their connections until
it finds one using the given port.
"""
if get_process_at_port(port):
return True

Check warning on line 264 in src/ansys/mapdl/core/launcher.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/mapdl/core/launcher.py#L264

Added line #L264 was not covered by tests
else:
return False


def launch_grpc(
exec_file="",
jobname="file",
nproc=2,
ram=None,
run_location=None,
port=MAPDL_DEFAULT_PORT,
ip=LOCALHOST,
additional_switches="",
override=True,
timeout=20,
verbose=None,
add_env_vars=None,
replace_env_vars=None,
**kwargs,
exec_file: str = "",
jobname: str = "file",
nproc: int = 2,
ram: Optional[int] = None,
run_location: str = None,
port: int = MAPDL_DEFAULT_PORT,
ip: str = LOCALHOST,
additional_switches: str = "",
override: bool = True,
timeout: int = 20,
verbose: Optional[bool] = None,
add_env_vars: Optional[Dict[str, str]] = None,
replace_env_vars: Optional[Dict[str, str]] = None,
**kwargs, # to keep compatibility with corba and console interface.
) -> Tuple[int, str, subprocess.Popen]:
"""Start MAPDL locally in gRPC mode.

Expand Down Expand Up @@ -457,9 +503,18 @@
port = max(pymapdl._LOCAL_PORTS) + 1
LOG.debug(f"Using next available port: {port}")

while port_in_use(port) or port in pymapdl._LOCAL_PORTS:
port += 1
LOG.debug(f"Port in use. Incrementing port number. port={port}")
while port_in_use(port) or port in pymapdl._LOCAL_PORTS:
port += 1
LOG.debug(f"Port in use. Incrementing port number. port={port}")

Check warning on line 508 in src/ansys/mapdl/core/launcher.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/mapdl/core/launcher.py#L506-L508

Added lines #L506 - L508 were not covered by tests

else:
if port_in_use(port):
proc = get_process_at_port(port)
if is_ansys_process(proc):
raise PortAlreadyInUseByAnMAPDLInstance
else:
raise PortAlreadyInUse

Check warning on line 516 in src/ansys/mapdl/core/launcher.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/mapdl/core/launcher.py#L516

Added line #L516 was not covered by tests

pymapdl._LOCAL_PORTS.append(port)

cpu_sw = "-np %d" % nproc
Expand Down
2 changes: 1 addition & 1 deletion src/ansys/mapdl/core/mapdl_grpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@ def __init__(
except MapdlConnectionError as err: # pragma: no cover
self._post_mortem_checks()
self._log.debug(
"The error wasn't catch by the post-mortem checks.\nThe stdout is printed now:"
"The error wasn't caught by the post-mortem checks.\nThe stdout is printed now:"
)
self._log.debug(self._stdout)

Expand Down
45 changes: 34 additions & 11 deletions tests/test_launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@
import pytest

from ansys.mapdl import core as pymapdl
from ansys.mapdl.core.errors import LicenseServerConnectionError, MapdlDidNotStart
from ansys.mapdl.core.errors import (
LicenseServerConnectionError,
MapdlDidNotStart,
PortAlreadyInUseByAnMAPDLInstance,
)
from ansys.mapdl.core.launcher import (
_check_license_argument,
_force_smp_student_version,
Expand Down Expand Up @@ -130,19 +134,23 @@ def test_find_ansys_linux():

@requires("ansys-tools-path")
@requires("local")
def test_invalid_mode():
def test_invalid_mode(mapdl):
with pytest.raises(ValueError):
exec_file = find_ansys(installed_mapdl_versions[0])[0]
pymapdl.launch_mapdl(exec_file, mode="notamode", start_timeout=start_timeout)
pymapdl.launch_mapdl(
exec_file, port=mapdl.port + 1, mode="notamode", start_timeout=start_timeout
)


@requires("ansys-tools-path")
@requires("local")
@pytest.mark.skipif(not os.path.isfile(V150_EXEC), reason="Requires v150")
def test_old_version():
def test_old_version(mapdl):
exec_file = find_ansys("150")[0]
with pytest.raises(ValueError):
pymapdl.launch_mapdl(exec_file, mode="console", start_timeout=start_timeout)
pymapdl.launch_mapdl(
exec_file, port=mapdl.port + 1, mode="console", start_timeout=start_timeout
)


@requires("ansys-tools-path")
Expand Down Expand Up @@ -238,19 +246,22 @@ def test_license_type_additional_switch():

@requires("ansys-tools-path")
@requires("local")
def test_license_type_dummy():
def test_license_type_dummy(mapdl):
dummy_license_type = "dummy"
with pytest.raises(LicenseServerConnectionError):
launch_mapdl(
port=mapdl.port + 1,
additional_switches=f" -p {dummy_license_type}" + QUICK_LAUNCH_SWITCHES,
start_timeout=start_timeout,
)


@requires("local")
def test_remove_temp_files():
@requires("nostudent")
def test_remove_temp_files(mapdl):
"""Ensure the working directory is removed when run_location is not set."""
mapdl = launch_mapdl(
port=mapdl.port + 1,
remove_temp_files=True,
start_timeout=start_timeout,
additional_switches=QUICK_LAUNCH_SWITCHES,
Expand All @@ -269,9 +280,11 @@ def test_remove_temp_files():


@requires("local")
def test_remove_temp_files_fail(tmpdir):
@requires("nostudent")
def test_remove_temp_files_fail(tmpdir, mapdl):
"""Ensure the working directory is not removed when the cwd is changed."""
mapdl = launch_mapdl(
port=mapdl.port + 1,
remove_temp_files=True,
start_timeout=start_timeout,
additional_switches=QUICK_LAUNCH_SWITCHES,
Expand Down Expand Up @@ -434,6 +447,7 @@ def test_find_ansys(mapdl):
def test_version(mapdl):
version = int(10 * mapdl.version)
mapdl_ = launch_mapdl(
port=mapdl.port + 1,
version=version,
start_timeout=start_timeout,
additional_switches=QUICK_LAUNCH_SWITCHES,
Expand All @@ -442,10 +456,11 @@ def test_version(mapdl):


@requires("local")
def test_raise_exec_path_and_version_launcher():
def test_raise_exec_path_and_version_launcher(mapdl):
with pytest.raises(ValueError):
launch_mapdl(
exec_file="asdf",
port=mapdl.port + 1,
version="asdf",
start_timeout=start_timeout,
additional_switches=QUICK_LAUNCH_SWITCHES,
Expand All @@ -464,10 +479,12 @@ def test_get_default_ansys():
assert get_default_ansys() is not None


def test_launch_mapdl_non_recognaised_arguments():
def test_launch_mapdl_non_recognaised_arguments(mapdl):
with pytest.raises(ValueError, match="my_fake_argument"):
launch_mapdl(
my_fake_argument="my_fake_value", additional_switches=QUICK_LAUNCH_SWITCHES
port=mapdl.port + 1,
my_fake_argument="my_fake_value",
additional_switches=QUICK_LAUNCH_SWITCHES,
)


Expand Down Expand Up @@ -496,3 +513,9 @@ def test_launched(mapdl):
assert mapdl.launched
else:
assert not mapdl.launched


@requires("local")
def test_launching_on_busy_port(mapdl):
with pytest.raises(PortAlreadyInUseByAnMAPDLInstance):
launch_mapdl(port=mapdl.port)
7 changes: 5 additions & 2 deletions tests/test_mapdl.py
Original file line number Diff line number Diff line change
Expand Up @@ -533,11 +533,14 @@ def test_lines(cleared, mapdl):


@requires("local")
def test_apdl_logging_start(tmpdir):
def test_apdl_logging_start(tmpdir, mapdl):
filename = str(tmpdir.mkdir("tmpdir").join("tmp.inp"))

mapdl = launch_mapdl(
start_timeout=30, log_apdl=filename, additional_switches=QUICK_LAUNCH_SWITCHES
port=mapdl.port + 1,
start_timeout=30,
log_apdl=filename,
additional_switches=QUICK_LAUNCH_SWITCHES,
)

mapdl.prep7()
Expand Down
Loading