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

Allow pool to connect to remote MAPDL instances #2862

Merged
merged 27 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
9ae3dd3
Allowing to connect to remote instances in pool library
germa89 Mar 4, 2024
2a5407b
Better error logging
germa89 Mar 6, 2024
d574d71
checking port len
germa89 Mar 7, 2024
54fe72e
Update tests
germa89 Mar 7, 2024
7a0521a
Fix local
germa89 Mar 8, 2024
6f17ff6
Merge branch 'main' into feat/allow-pool-to-connect-to-remote-instances
germa89 Mar 8, 2024
36ffa04
Fixing launcher
germa89 Mar 8, 2024
d3a69e2
Merge branch 'main' into feat/allow-pool-to-connect-to-remote-instances
germa89 Mar 8, 2024
5735cbd
Run tests on remote and local
germa89 Mar 9, 2024
00f55fe
setting ports in remote.
germa89 Mar 9, 2024
3679d26
Launching two docker images
germa89 Mar 9, 2024
a4fcf5d
fix cicd runs
germa89 Mar 9, 2024
25c84bd
fixing port issue
germa89 Mar 9, 2024
4e84f87
skipping one test if not in local
germa89 Mar 9, 2024
ea4138f
Reusing port for re-spawning
germa89 Mar 9, 2024
115d065
Using full verbose
germa89 Mar 9, 2024
23be131
reducing the length of testing
germa89 Mar 9, 2024
0993219
Using different instance names
germa89 Mar 11, 2024
248a0eb
Changing log file names
germa89 Mar 11, 2024
5aa15ec
Skipping if ON_STUDENT
germa89 Mar 11, 2024
2be7f9e
fixing logs
germa89 Mar 11, 2024
bb31b09
fix doc build
germa89 Mar 11, 2024
ac1faad
Uploading docker launch log
germa89 Mar 11, 2024
ba93a87
fixing extracting logs
germa89 Mar 11, 2024
a76e89b
Update src/ansys/mapdl/core/launcher.py
germa89 Mar 11, 2024
b390589
Fixing codacy suggestions
germa89 Mar 11, 2024
fdb89c3
Merge branch 'main' into feat/allow-pool-to-connect-to-remote-instances
germa89 Mar 11, 2024
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
4 changes: 3 additions & 1 deletion src/ansys/mapdl/core/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions src/ansys/mapdl/core/launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -820,6 +820,11 @@ def get_start_instance(start_instance: bool = True):
)
return start_instance

elif start_instance is None:
LOG.debug(
f"'PYMAPDL_START_INSTANCE' is unset, and there is no supplied value, hence using default value 'True'"
germa89 marked this conversation as resolved.
Show resolved Hide resolved
)
return True # Default is true
else:
raise ValueError("Only booleans are allowed as arguments.")

Expand Down
5 changes: 4 additions & 1 deletion src/ansys/mapdl/core/mapdl_grpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down Expand Up @@ -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(f"Closing processes")
if self._local:
# killing server process
self._kill_server()
Expand Down
117 changes: 86 additions & 31 deletions src/ansys/mapdl/core/pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -114,6 +118,11 @@ class LocalMapdlPool:
the index of each instance in the pool.
By default, the instances directories are named as "Instances_{i}".

start_instance : bool
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.
Expand Down Expand Up @@ -154,12 +163,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"""
Expand All @@ -176,6 +187,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"

Expand All @@ -189,34 +205,55 @@ def __init__(
)

# verify that mapdl is 2021R1 or newer
if "exec_file" in kwargs:
exec_file = kwargs["exec_file"]
else: # get default executable
if start_instance:
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."
)

if exec_file is None:
raise FileNotFoundError(
"Invalid exec_file path or cannot load cached "
"ansys path. Enter one manually using "
"exec_file=<path to 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 version_from_path("mapdl", exec_file) < 211:
raise VersionError("LocalMapdlPool requires MAPDL 2021R1 or later.")

if exec_file is None:
raise FileNotFoundError(
"Invalid exec_file path or cannot load cached "
"ansys path. Enter one manually using "
"exec_file=<path to executable>"
)
else:
exec_file = None

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
Expand All @@ -241,7 +278,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)
]
Expand Down Expand Up @@ -612,8 +655,6 @@ def next_available(self, return_index=False):
return instance, i
else:
return instance
else:
instance._exited = True

def __del__(self):
self.exit()
Expand Down Expand Up @@ -688,7 +729,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
Expand All @@ -697,9 +744,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,
)

Expand All @@ -726,20 +775,26 @@ 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
if self._start_instance:
port = max(self._ports) + 1
else:
port = instance.port

self._spawn_mapdl(
index,
port=port,
name=name,
thread_name=name,
exec_file=self._exec_file,
start_instance=self._start_instance,
).join()

except Exception as e:
Expand Down
35 changes: 14 additions & 21 deletions tests/test_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
import numpy as np
import pytest

from conftest import ON_STUDENT, has_dependency
from conftest import ON_LOCAL, START_INSTANCE, has_dependency

if has_dependency("ansys-tools-path"):
from ansys.tools.path import find_ansys
Expand All @@ -37,7 +37,7 @@
else:
EXEC_FILE = os.environ.get("PYMAPDL_MAPDL_EXEC")

if not EXEC_FILE or ON_STUDENT:
if not ON_LOCAL:
pytest.skip(allow_module_level=True)

from ansys.mapdl.core import LocalMapdlPool, examples
Expand Down Expand Up @@ -67,11 +67,13 @@
def pool(tmpdir_factory):
run_path = str(tmpdir_factory.mktemp("ansys_pool"))

port = os.environ.get("PYMAPDL_PORT", 50056)

mapdl_pool = LocalMapdlPool(
4,
2,
license_server_check=False,
run_location=run_path,
port=50056,
port=port,
start_timeout=30,
exec_file=EXEC_FILE,
additional_switches=QUICK_LAUNCH_SWITCHES,
Expand Down Expand Up @@ -111,7 +113,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
Expand All @@ -131,15 +132,13 @@ def test_heal(pool):
pool._verify_unique_ports()


@requires("local")
@skip_if_ignore_pool
def test_simple_map(pool):
pool_sz = len(pool)
_ = pool.map(lambda mapdl: mapdl.prep7())
assert len(pool) == pool_sz


@requires("local")
@skip_if_ignore_pool
def test_map_timeout(pool):
pool_sz = len(pool)
Expand Down Expand Up @@ -168,7 +167,6 @@ def func(mapdl, tsleep):
assert len(pool) == pool_sz


@requires("local")
@skip_if_ignore_pool
def test_simple(pool):
pool_sz = len(pool)
Expand All @@ -182,16 +180,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 = []
Expand All @@ -204,14 +200,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) + 3)]
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

Expand Down Expand Up @@ -245,17 +243,14 @@ 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")
@skip_if_ignore_pool
def test_directory_names_custom_string(tmpdir):
pool = LocalMapdlPool(
Expand All @@ -275,7 +270,6 @@ def test_directory_names_custom_string(tmpdir):
pool.exit(block=True)


@requires("local")
@skip_if_ignore_pool
def test_directory_names_function(tmpdir):
def myfun(i):
Expand Down Expand Up @@ -313,7 +307,6 @@ def test_num_instances():
)


@requires("local")
@skip_if_ignore_pool
def test_only_one_instance():
pool = LocalMapdlPool(
Expand Down
Loading