Skip to content

Commit

Permalink
Improving port being busy detection (#2603)
Browse files Browse the repository at this point in the history
* Improving port being busy

* Adding typing hints and docstrings

* fixing port error

* skipping tests in nostudent

* adding test.
Avoiding tests issues

* fixing tests

* fixing test

* fixing test

* skipping test on remote (since it does not make sense?)
  • Loading branch information
germa89 authored Jan 5, 2024
1 parent c04eb09 commit 2dea105
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 38 deletions.
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 @@ def _cleanup_gallery_instance(): # pragma: no cover
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 @@ def _is_ubuntu():
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 @@ def close_all_local_instances(port_range=None):
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."):
# 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 @@ def close_mapdl(port, name="Closing mapdl thread."):
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 @@ def port_in_use(port, host=LOCALHOST):
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
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 @@ def launch_grpc(
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}")

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

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

0 comments on commit 2dea105

Please sign in to comment.