diff --git a/.ci/collect_mapdl_logs.sh b/.ci/collect_mapdl_logs.sh index c0fcb8cee8..5e075ae096 100755 --- a/.ci/collect_mapdl_logs.sh +++ b/.ci/collect_mapdl_logs.sh @@ -17,11 +17,11 @@ mkdir "$LOG_NAMES" && echo "Successfully generated directory $LOG_NAMES" #### echo "Collecting MAPDL logs..." -docker exec "$MAPDL_INSTANCE" /bin/bash -c "mkdir -p /mapdl_logs && echo 'Successfully created directory inside docker container'" || echo "Failed to create a directory inside docker container for logs." -docker exec "$MAPDL_INSTANCE" /bin/bash -c "if compgen -G '$FILE*.out' > /dev/null ;then cp -f /file*.out /mapdl_logs && echo 'Successfully copied out files.'; fi" || echo "Failed to copy the 'out' files into a local file" -docker exec "$MAPDL_INSTANCE" /bin/bash -c "if compgen -G '$FILE*.err' > /dev/null ;then cp -f /file*.err /mapdl_logs && echo 'Successfully copied err files.'; fi" || echo "Failed to copy the 'err' files into a local file" -docker exec "$MAPDL_INSTANCE" /bin/bash -c "if compgen -G '$FILE*.log' > /dev/null ;then cp -f /file*.log /mapdl_logs && echo 'Successfully copied log files.'; fi" || echo "Failed to copy the 'log' files into a local file" -docker exec "$MAPDL_INSTANCE" /bin/bash -c "if compgen -G '$WDIR*.crash' > /dev/null ;then cp -f /*.crash /mapdl_logs && echo 'Successfully copied crash files.'; fi" || echo "Failed to copy the 'crash' files into a local file" +(docker exec "$MAPDL_INSTANCE" /bin/bash -c "mkdir -p /mapdl_logs && echo 'Successfully created directory inside docker container'") || echo "Failed to create a directory inside docker container for logs." +(docker exec "$MAPDL_INSTANCE" /bin/bash -c "if compgen -G '$FILE*.out' > /dev/null ;then cp -f /file*.out /mapdl_logs && echo 'Successfully copied out files.'; fi") || echo "Failed to copy the 'out' files into a local file" +(docker exec "$MAPDL_INSTANCE" /bin/bash -c "if compgen -G '$FILE*.err' > /dev/null ;then cp -f /file*.err /mapdl_logs && echo 'Successfully copied err files.'; fi") || echo "Failed to copy the 'err' files into a local file" +(docker exec "$MAPDL_INSTANCE" /bin/bash -c "if compgen -G '$FILE*.log' > /dev/null ;then cp -f /file*.log /mapdl_logs && echo 'Successfully copied log files.'; fi") || echo "Failed to copy the 'log' files into a local file" +(docker exec "$MAPDL_INSTANCE" /bin/bash -c "if compgen -G '$WDIR*.crash' > /dev/null ;then cp -f /*.crash /mapdl_logs && echo 'Successfully copied crash files.'; fi") || echo "Failed to copy the 'crash' files into a local file" docker cp "$MAPDL_INSTANCE":/mapdl_logs/. ./"$LOG_NAMES"/. || echo "Failed to copy the 'log-build-docs' files into a local directory" diff --git a/.ci/start_mapdl.sh b/.ci/start_mapdl.sh index cb6453770c..8870e5fd81 100755 --- a/.ci/start_mapdl.sh +++ b/.ci/start_mapdl.sh @@ -1,4 +1,5 @@ #!/bin/bash +echo "MAPDL Instance name: $INSTANCE_NAME" echo "MAPDL_VERSION: $MAPDL_VERSION" export MAPDL_IMAGE="$MAPDL_PACKAGE:$MAPDL_VERSION" @@ -28,7 +29,7 @@ echo "P_SCHEMA: $P_SCHEMA" docker run \ --entrypoint "/bin/bash" \ - --name mapdl \ + --name "$INSTANCE_NAME" \ --restart always \ --health-cmd="ps aux | grep \"[/]ansys_inc/.*ansys\.e.*grpc\" -q && echo 0 || echo 1" \ --health-interval=0.5s \ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6970b91334..ffcdf92e23 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,11 +28,11 @@ env: MEILISEARCH_PUBLIC_API_KEY: ${{ secrets.MEILISEARCH_PUBLIC_API_KEY }} PYANSYS_OFF_SCREEN: True DPF_START_SERVER: False - DPF_PORT: 21002 + DPF_PORT: 21004 MAPDL_PACKAGE: ghcr.io/ansys/mapdl MAPDL_IMAGE_VERSION_DOCS_BUILD: v24.1-ubuntu-student ON_CI: True - PYTEST_ARGUMENTS: '-vv --durations=10 --maxfail=3 --reruns 3 --reruns-delay 4 --cov=ansys.mapdl.core --cov-report=html' + PYTEST_ARGUMENTS: '-vvv --durations=10 --maxfail=3 --reruns 3 --reruns-delay 4 --cov=ansys.mapdl.core --cov-report=html' # Following env vars when changed will "reset" the mentioned cache, # by changing the cache file name. It is rendered as ...-v%RESET_XXX%-... @@ -131,6 +131,7 @@ jobs: MAPDL_VERSION: ${{ env.MAPDL_IMAGE_VERSION_DOCS_BUILD }} DISTRIBUTED_MODE: "dmp" run: | + export INSTANCE_NAME=MAPDL_0 .ci/start_mapdl.sh &> mapdl_launch.log & export DOCKER_PID=$! echo "Launching MAPDL service at PID: $DOCKER_PID" echo "DOCKER_PID=$(echo $DOCKER_PID)" >> $GITHUB_OUTPUT @@ -261,7 +262,7 @@ jobs: if: always() env: MAPDL_VERSION: ${{ env.MAPDL_IMAGE_VERSION_DOCS_BUILD }} - MAPDL_INSTANCE: mapdl + MAPDL_INSTANCE: MAPDL_0 LOG_NAMES: logs-build-docs run: | .ci/collect_mapdl_logs.sh @@ -281,7 +282,6 @@ jobs: run: | .ci/display_logs.sh - build-test: name: "Remote: Build & test MAPDL ${{ matrix.mapdl-version }} (Extended testing ${{ matrix.extended_testing }})" runs-on: ubuntu-latest @@ -308,7 +308,9 @@ jobs: mapdl-version: 'v24.1.0' env: PYMAPDL_PORT: 21000 # default won't work on GitHub runners - PYMAPDL_DB_PORT: 21001 # default won't work on GitHub runners + PYMAPDL_PORT2: 21001 # for the pool testing and default won't work on GitHub runners + PYMAPDL_DB_PORT: 21002 # default won't work on GitHub runners + PYMAPDL_DB_PORT2: 21003 # default won't work on GitHub runners PYMAPDL_START_INSTANCE: FALSE ON_LOCAL: FALSE ON_UBUNTU: FALSE @@ -345,9 +347,18 @@ jobs: MAPDL_VERSION: ${{ matrix.mapdl-version }} DISTRIBUTED_MODE: ${{ steps.distributed_mode.outputs.distributed_mode }} run: | - .ci/start_mapdl.sh &> mapdl_launch.log & export DOCKER_PID=$! - echo "Launching MAPDL service at PID: $DOCKER_PID" - echo "DOCKER_PID=$(echo $DOCKER_PID)" >> $GITHUB_OUTPUT + echo "Launching first MAPDL instance..." + export INSTANCE_NAME=MAPDL_0 + .ci/start_mapdl.sh &> mapdl_launch_0.log & export DOCKER_PID_0=$! + echo "Launching a second instance for MAPDL pool testing..." + export PYMAPDL_PORT=${{ env.PYMAPDL_PORT2 }} + export PYMAPDL_DB_PORT=${{ env.PYMAPDL_DB_PORT2 }} + export INSTANCE_NAME=MAPDL_1 + .ci/start_mapdl.sh &> mapdl_launch_1.log & export DOCKER_PID_1=$! + echo "Launching MAPDL service 0 at PID: $DOCKER_PID_0" + echo "Launching MAPDL service 1 at PID: $DOCKER_PID_2" + echo "DOCKER_PID_0=$(echo $DOCKER_PID_0)" >> $GITHUB_OUTPUT + echo "DOCKER_PID_1=$(echo $DOCKER_PID_1)" >> $GITHUB_OUTPUT - name: "DPF server activation" run: | diff --git a/src/ansys/mapdl/core/errors.py b/src/ansys/mapdl/core/errors.py index a2c660879e..79aaa60deb 100644 --- a/src/ansys/mapdl/core/errors.py +++ b/src/ansys/mapdl/core/errors.py @@ -316,7 +316,9 @@ def wrapper(*args, **kwargs): # Must close unfinished processes mapdl._close_process() - raise MapdlExitedError("MAPDL server connection terminated") from None + raise MapdlExitedError( + f"MAPDL server connection terminated with the following error\n{error}" + ) from None if threading.current_thread().__class__.__name__ == "_MainThread": received_interrupt = bool(SIGINT_TRACKER) diff --git a/src/ansys/mapdl/core/launcher.py b/src/ansys/mapdl/core/launcher.py index 2813c01c0b..f768d88151 100644 --- a/src/ansys/mapdl/core/launcher.py +++ b/src/ansys/mapdl/core/launcher.py @@ -820,6 +820,11 @@ def get_start_instance(start_instance: bool = True): ) return start_instance + elif start_instance is None: + LOG.debug( + "'PYMAPDL_START_INSTANCE' is unset, and there is no supplied value. Using default, which is 'True'." + ) + return True # Default is true else: raise ValueError("Only booleans are allowed as arguments.") diff --git a/src/ansys/mapdl/core/mapdl_grpc.py b/src/ansys/mapdl/core/mapdl_grpc.py index cb32d17216..440aabfb41 100644 --- a/src/ansys/mapdl/core/mapdl_grpc.py +++ b/src/ansys/mapdl/core/mapdl_grpc.py @@ -880,7 +880,9 @@ def _run(self, cmd: str, verbose: bool = False, mute: Optional[bool] = None) -> mute = self._mute if self._exited: - raise MapdlExitedError + raise MapdlExitedError( + f"The MAPDL instance has been exited before running the command: {cmd}" + ) # don't allow empty commands if not cmd.strip(): @@ -1141,6 +1143,7 @@ def _close_process(self, timeout=2): # pragma: no cover processes making this method ineffective for a local instance of MAPDL. """ + self._log.debug("Closing processes") if self._local: # killing server process self._kill_server() diff --git a/src/ansys/mapdl/core/pool.py b/src/ansys/mapdl/core/pool.py index 7bf540916d..a8741c418c 100755 --- a/src/ansys/mapdl/core/pool.py +++ b/src/ansys/mapdl/core/pool.py @@ -25,12 +25,16 @@ import shutil import tempfile import time -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Union import warnings from ansys.mapdl.core import LOG, launch_mapdl from ansys.mapdl.core.errors import MapdlRuntimeError, VersionError -from ansys.mapdl.core.launcher import MAPDL_DEFAULT_PORT, port_in_use +from ansys.mapdl.core.launcher import ( + MAPDL_DEFAULT_PORT, + get_start_instance, + port_in_use, +) from ansys.mapdl.core.mapdl_grpc import _HAS_TQDM from ansys.mapdl.core.misc import create_temp_dir, threaded, threaded_daemon @@ -114,9 +118,15 @@ class LocalMapdlPool: the index of each instance in the pool. By default, the instances directories are named as "Instances_{i}". + start_instance : bool, optional + Set it to ``False`` to make PyMAPDL to connect to remote instances instead + of launching them. In that case, you need to supply the MAPDL instances + ports as a list of ``int`` s. + **kwargs : dict, optional - Additional keyword arguments. For a complete listing, see the description for the - :func:`ansys.mapdl.core.launcher.launch_mapdl` method. + Additional keyword arguments. For a complete listing, see the + description for the :func:`ansys.mapdl.core.launcher.launch_mapdl` + method. Examples -------- @@ -154,12 +164,14 @@ def __init__( n_instances: int, wait: bool = True, run_location: Optional[str] = None, - port: int = MAPDL_DEFAULT_PORT, + port: Union[int, List[int]] = MAPDL_DEFAULT_PORT, progress_bar: bool = DEFAULT_PROGRESS_BAR, restart_failed: bool = True, remove_temp_files: bool = True, names: Optional[str] = None, override=True, + start_instance: bool = None, + exec_file: Optional[str] = None, **kwargs, ) -> None: """Initialize several instances of mapdl""" @@ -176,6 +188,11 @@ def __init__( self._exiting_i: int = 0 self._override = override + # Getting start_instance + start_instance = get_start_instance(start_instance) + self._start_instance = start_instance + LOG.debug(f"'start_instance' equals to '{start_instance}'") + if not names: names = "Instance" @@ -188,35 +205,56 @@ def __init__( "Only strings or functions are allowed in the argument 'name'." ) - # verify that mapdl is 2021R1 or newer - if "exec_file" in kwargs: - exec_file = kwargs["exec_file"] - else: # get default executable - if _HAS_ATP: - exec_file = get_ansys_path() - else: - raise ValueError( - "Please use 'exec_file' argument to specify the location of the ansys installation.\n" - "Alternatively, PyMAPDL can detect your ansys installation if you install 'ansys-tools-path' library." - ) + # verify executable + exec_file = os.getenv("PYMAPDL_MAPDL_EXEC", exec_file) - if exec_file is None: - raise FileNotFoundError( - "Invalid exec_file path or cannot load cached " - "ansys path. Enter one manually using " - "exec_file=" - ) + if start_instance: + exec_file = kwargs.get("exec_file", exec_file) + + if not exec_file: # get default executable + if _HAS_ATP: + exec_file = get_ansys_path() + else: + raise ValueError( + "Please use 'exec_file' argument to specify the location of the ansys installation.\n" + "Alternatively, PyMAPDL can detect your ansys installation if you install 'ansys-tools-path' library." + ) + + if exec_file is None: + raise FileNotFoundError( + "Invalid exec_file path or cannot load cached " + "ansys path. Enter one manually using " + "exec_file=" + ) - if _HAS_ATP: - if version_from_path("mapdl", exec_file) < 211: - raise VersionError("LocalMapdlPool requires MAPDL 2021R1 or later.") + # Checking version + if _HAS_ATP: + if version_from_path("mapdl", exec_file) < 211: + raise VersionError("LocalMapdlPool requires MAPDL 2021R1 or later.") + + self._exec_file = exec_file # grab available ports - ports = available_ports(n_instances, port) + if start_instance: + if isinstance(port, int) or len(port) == 1: + ports = available_ports(n_instances, port) + else: + ports = port - if self._root_dir is not None: - if not os.path.isdir(self._root_dir): - os.makedirs(self._root_dir) + if self._root_dir is not None: + if not os.path.isdir(self._root_dir): + os.makedirs(self._root_dir) + else: + if isinstance(port, int) or len(port) == 1: + ports = [port + i for i in range(n_instances)] + else: + ports = port + + if len(ports) != n_instances: + raise ValueError( + "The number of instances should be the same as the number of ports." + ) + LOG.debug(f"Using ports: {ports}") self._instances = [] self._active = True # used by pool monitor @@ -241,7 +279,13 @@ def __init__( # threaded spawn threads = [ self._spawn_mapdl( - i, ports[i], pbar, name=self._names(i), thread_name=self._names(i) + i, + ports[i], + pbar, + name=self._names(i), + thread_name=self._names(i), + start_instance=start_instance, + exec_file=exec_file, ) for i in range(n_instances) ] @@ -612,8 +656,6 @@ def next_available(self, return_index=False): return instance, i else: return instance - else: - instance._exited = True def __del__(self): self.exit() @@ -688,7 +730,13 @@ def __iter__(self): @threaded_daemon def _spawn_mapdl( - self, index: int, port: int = None, pbar: Optional[bool] = None, name: str = "" + self, + index: int, + port: int = None, + pbar: Optional[bool] = None, + name: str = "", + start_instance=True, + exec_file=None, ): """Spawn a mapdl instance at an index""" # create a new temporary directory for each instance @@ -697,9 +745,11 @@ def _spawn_mapdl( run_location = create_temp_dir(self._root_dir, name=name) self._instances[index] = launch_mapdl( + exec_file=exec_file, run_location=run_location, port=port, override=True, + start_instance=start_instance, **self._spawn_kwargs, ) @@ -726,20 +776,21 @@ def _monitor_pool(self, refresh=1.0): """ while self._active: for index, instance in enumerate(self._instances): - name = self._names[index] + name = self._names(index) if not instance: # encountered placeholder continue if instance._exited: try: - # use the next port after the current available port self._spawning_i += 1 - port = max(self._ports) + 1 + self._spawn_mapdl( index, - port=port, + port=instance.port, name=name, thread_name=name, + exec_file=self._exec_file, + start_instance=self._start_instance, ).join() except Exception as e: diff --git a/tests/test_pool.py b/tests/test_pool.py index 93370ed82a..daa178bc60 100644 --- a/tests/test_pool.py +++ b/tests/test_pool.py @@ -27,7 +27,7 @@ import numpy as np import pytest -from conftest import ON_STUDENT, has_dependency +from conftest import ON_LOCAL, ON_STUDENT, START_INSTANCE, has_dependency if has_dependency("ansys-tools-path"): from ansys.tools.path import find_ansys @@ -37,9 +37,6 @@ else: EXEC_FILE = os.environ.get("PYMAPDL_MAPDL_EXEC") -if not EXEC_FILE or ON_STUDENT: - pytest.skip(allow_module_level=True) - from ansys.mapdl.core import LocalMapdlPool, examples from ansys.mapdl.core.errors import VersionError from conftest import QUICK_LAUNCH_SWITCHES, requires @@ -47,6 +44,10 @@ # skip entire module unless HAS_GRPC pytestmark = requires("grpc") +# skipping if ON_STUDENT and ON_LOCAL because we cannot spawn that many instances. +if ON_STUDENT and ON_LOCAL: + pytest.skip(allow_module_level=True) + skip_if_ignore_pool = pytest.mark.skipif( os.environ.get("IGNORE_POOL", "").upper() == "TRUE", @@ -67,16 +68,30 @@ def pool(tmpdir_factory): run_path = str(tmpdir_factory.mktemp("ansys_pool")) - mapdl_pool = LocalMapdlPool( - 4, - license_server_check=False, - run_location=run_path, - port=50056, - start_timeout=30, - exec_file=EXEC_FILE, - additional_switches=QUICK_LAUNCH_SWITCHES, - nproc=NPROC, - ) + port = os.environ.get("PYMAPDL_PORT", 50056) + + if ON_LOCAL: + + mapdl_pool = LocalMapdlPool( + 2, + license_server_check=False, + run_location=run_path, + port=port, + start_timeout=30, + exec_file=EXEC_FILE, + additional_switches=QUICK_LAUNCH_SWITCHES, + nproc=NPROC, + ) + else: + port2 = os.environ.get("PYMAPDL_PORT2", 50057) + + mapdl_pool = LocalMapdlPool( + 2, + license_server_check=False, + start_instance=False, + port=[port, port2], + ) + yield mapdl_pool ########################################################################## @@ -111,7 +126,6 @@ def test_invalid_exec(): # @pytest.mark.xfail(strict=False, reason="Flaky test. See #2435") -@requires("local") def test_heal(pool): pool_sz = len(pool) pool_names = pool._names # copy pool names @@ -131,7 +145,6 @@ def test_heal(pool): pool._verify_unique_ports() -@requires("local") @skip_if_ignore_pool def test_simple_map(pool): pool_sz = len(pool) @@ -139,8 +152,8 @@ def test_simple_map(pool): assert len(pool) == pool_sz -@requires("local") @skip_if_ignore_pool +@requires("local") def test_map_timeout(pool): pool_sz = len(pool) @@ -168,7 +181,6 @@ def func(mapdl, tsleep): assert len(pool) == pool_sz -@requires("local") @skip_if_ignore_pool def test_simple(pool): pool_sz = len(pool) @@ -182,16 +194,14 @@ def func(mapdl): # fails intermittently -@requires("local") @skip_if_ignore_pool def test_batch(pool): - input_files = [examples.vmfiles["vm%d" % i] for i in range(1, 11)] + input_files = [examples.vmfiles["vm%d" % i] for i in range(1, len(pool) + 3)] outputs = pool.run_batch(input_files) assert len(outputs) == len(input_files) # fails intermittently -@requires("local") @skip_if_ignore_pool def test_map(pool): completed_indices = [] @@ -204,14 +214,16 @@ def func(mapdl, input_file, index): completed_indices.append(index) return mapdl.parameters.routine - inputs = [(examples.vmfiles["vm%d" % i], i) for i in range(1, 11)] + inputs = [(examples.vmfiles["vm%d" % i], i) for i in range(1, len(pool) + 1)] outputs = pool.map(func, inputs, wait=True) assert len(outputs) == len(inputs) -@requires("local") @skip_if_ignore_pool +@pytest.mark.skipif( + not START_INSTANCE, reason="This test requires the pool to be local" +) def test_abort(pool, tmpdir): pool_sz = len(pool) # initial pool size @@ -245,14 +257,12 @@ def test_abort(pool, tmpdir): assert path_deleted -@requires("local") @skip_if_ignore_pool def test_directory_names_default(pool): dirs_path_pool = os.listdir(pool._root_dir) - assert "Instance_0" in dirs_path_pool - assert "Instance_1" in dirs_path_pool - assert "Instance_2" in dirs_path_pool - assert "Instance_3" in dirs_path_pool + for i, _ in enumerate(pool._instances): + assert pool._names(i) in dirs_path_pool + assert f"Instance_{i}" in dirs_path_pool @requires("local") @@ -313,7 +323,6 @@ def test_num_instances(): ) -@requires("local") @skip_if_ignore_pool def test_only_one_instance(): pool = LocalMapdlPool(