Skip to content

Commit ec86594

Browse files
fix: exiting on class deletion (#3738)
* test: __del__method. * refactor: simplifying exit * chore: adding changelog file 3738.fixed.md [dependabot-skip] * refactor: simplyfing for other interfaces. * feat: exiting mapdl * feat: adding __del__ to mapdl_console * fix: improve job ID handling and remove atexit registration in MAPDL core --------- Co-authored-by: pyansys-ci-bot <[email protected]>
1 parent 7896085 commit ec86594

File tree

7 files changed

+174
-71
lines changed

7 files changed

+174
-71
lines changed

doc/changelog.d/3738.fixed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
fix: exiting on class deletion

src/ansys/mapdl/core/mapdl_console.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import os
2828
import re
2929
import time
30+
from warnings import warn
3031

3132
from ansys.mapdl.core import LOG
3233
from ansys.mapdl.core.errors import MapdlExitedError, MapdlRuntimeError
@@ -274,6 +275,20 @@ def mesh(self):
274275
"""
275276
return self._mesh
276277

278+
def __del__(self):
279+
"""Garbage cleaning the class"""
280+
self._exit()
281+
282+
def _exit(self):
283+
"""Minimal exit command. No logging or cleanup so it does not raise
284+
exceptions"""
285+
if self._process is not None:
286+
try:
287+
self._process.sendline("FINISH")
288+
self._process.sendline("EXIT")
289+
except Exception as e:
290+
LOG.warning(f"Unable to exit ANSYS MAPDL: {e}")
291+
277292
def exit(self, close_log=True, timeout=3):
278293
"""Exit MAPDL process.
279294
@@ -284,12 +299,7 @@ def exit(self, close_log=True, timeout=3):
284299
``None`` to not wait until MAPDL stops.
285300
"""
286301
self._log.debug("Exiting ANSYS")
287-
if self._process is not None:
288-
try:
289-
self._process.sendline("FINISH")
290-
self._process.sendline("EXIT")
291-
except Exception as e:
292-
LOG.warning(f"Unable to exit ANSYS MAPDL: {e}")
302+
self._exit()
293303

294304
if close_log:
295305
self._close_apdl_log()
@@ -302,11 +312,10 @@ def exit(self, close_log=True, timeout=3):
302312
tstart = time.time()
303313
while self._process.isalive():
304314
time.sleep(0.05)
305-
telap = tstart - time.time()
306-
if telap > timeout:
307-
return 1
308-
309-
return 0
315+
if (time.time() - tstart) > timeout:
316+
if self._process.isalive():
317+
warn("MAPDL couldn't be exited on time.")
318+
return
310319

311320
def kill(self):
312321
"""Forces ANSYS process to end and removes lock file"""

src/ansys/mapdl/core/mapdl_core.py

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
"""Module to control interaction with MAPDL through Python"""
2424

25-
import atexit
25+
# import atexit
2626
from functools import wraps
2727
import glob
2828
import logging
@@ -247,7 +247,6 @@ def __init__(
247247
**start_parm,
248248
):
249249
"""Initialize connection with MAPDL."""
250-
atexit.register(self.__del__) # registering to exit properly
251250
self._show_matplotlib_figures = True # for testing
252251
self._query = None
253252
self._exited: bool = False
@@ -2344,21 +2343,8 @@ def exit(self): # pragma: no cover
23442343
raise NotImplementedError("Implemented by child class")
23452344

23462345
def __del__(self):
2347-
"""Clean up when complete"""
2348-
if self._cleanup:
2349-
# removing logging handlers if they are closed to avoid I/O errors
2350-
# when exiting after the logger file has been closed.
2351-
# self._cleanup_loggers()
2352-
logging.disable(logging.CRITICAL)
2353-
2354-
try:
2355-
self.exit()
2356-
except Exception as e:
2357-
try: # logger might be closed
2358-
if hasattr(self, "_log") and self._log is not None:
2359-
self._log.error("exit: %s", str(e))
2360-
except ValueError:
2361-
pass
2346+
"""Kill MAPDL when garbage cleaning"""
2347+
self.exit()
23622348

23632349
def _cleanup_loggers(self):
23642350
"""Clean up all the loggers"""

src/ansys/mapdl/core/mapdl_grpc.py

Lines changed: 34 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@ def __init__(
424424

425425
self._busy: bool = False # used to check if running a command on the server
426426
self._local: bool = start_parm.get("local", True)
427-
self._launched: bool = start_parm.get("launched", True)
427+
self._launched: bool = start_parm.get("launched", False)
428428
self._health_response_queue: Optional["Queue"] = None
429429
self._exiting: bool = False
430430
self._exited: Optional[bool] = None
@@ -1117,32 +1117,31 @@ def exit(self, save=False, force=False, **kwargs):
11171117
11181118
Notes
11191119
-----
1120+
If Mapdl didn't start the instance, then this will be ignored unless
1121+
``force=True``.
1122+
11201123
If ``PYMAPDL_START_INSTANCE`` is set to ``False`` (generally set in
11211124
remote testing or documentation build), then this will be
11221125
ignored. Override this behavior with ``force=True`` to always force
11231126
exiting MAPDL regardless of your local environment.
11241127
1128+
If ``Mapdl.finish_job_on_exit`` is set to ``True`` and there is a valid
1129+
JobID in ``Mapdl.jobid``, then the SLURM job will be canceled.
1130+
11251131
Examples
11261132
--------
11271133
>>> mapdl.exit()
11281134
"""
11291135
# check if permitted to start (and hence exit) instances
11301136
from ansys.mapdl import core as pymapdl
11311137

1132-
if hasattr(self, "_log"):
1133-
self._log.debug(
1134-
f"Exiting MAPLD gRPC instance {self.ip}:{self.port} on '{self._path}'."
1135-
)
1138+
self._log.debug(
1139+
f"Exiting MAPLD gRPC instance {self.ip}:{self.port} on '{self._path}'."
1140+
)
11361141

11371142
mapdl_path = self._path # using cached version
1138-
if self._exited is None:
1139-
self._log.debug("'self._exited' is none.")
1140-
return # Some edge cases the class object is not completely
1141-
# initialized but the __del__ method
1142-
# is called when exiting python. So, early exit here instead an
1143-
# error in the following self.directory command.
1144-
# See issue #1796
1145-
elif self._exited:
1143+
1144+
if self._exited:
11461145
# Already exited.
11471146
self._log.debug("Already exited")
11481147
return
@@ -1153,26 +1152,25 @@ def exit(self, save=False, force=False, **kwargs):
11531152

11541153
if not force:
11551154
# ignore this method if PYMAPDL_START_INSTANCE=False
1156-
if not self._start_instance:
1157-
self._log.info("Ignoring exit due to PYMAPDL_START_INSTANCE=False")
1155+
if not self._start_instance or not self._launched:
1156+
self._log.info(
1157+
"Ignoring exit due to PYMAPDL_START_INSTANCE=False or because PyMAPDL didn't launch the instance."
1158+
)
11581159
return
11591160

11601161
# or building the gallery
11611162
if pymapdl.BUILDING_GALLERY:
11621163
self._log.info("Ignoring exit due as BUILDING_GALLERY=True")
11631164
return
11641165

1165-
# Actually exiting MAPDL instance
1166-
if self.finish_job_on_exit:
1167-
self._exiting = True
1168-
self._exit_mapdl(path=mapdl_path)
1169-
self._exited = True
1166+
# Exiting MAPDL instance if we launched.
1167+
self._exiting = True
1168+
self._exit_mapdl(path=mapdl_path)
1169+
self._exited = True
11701170

1171-
# Exiting HPC job
1172-
if self._mapdl_on_hpc:
1173-
self.kill_job(self.jobid)
1174-
if hasattr(self, "_log"):
1175-
self._log.debug(f"Job (id: {self.jobid}) has been cancel.")
1171+
if self.finish_job_on_exit and self._mapdl_on_hpc:
1172+
self.kill_job(self.jobid)
1173+
self._log.debug(f"Job (id: {self.jobid}) has been cancel.")
11761174

11771175
# Exiting remote instances
11781176
if self._remote_instance: # pragma: no cover
@@ -3818,20 +3816,17 @@ def __del__(self):
38183816
"""In case the object is deleted"""
38193817
# We are just going to escape early if needed, and kill the HPC job.
38203818
# The garbage collector remove attributes before we can evaluate this.
3821-
try:
3822-
# Exiting HPC job
3823-
if (
3824-
hasattr(self, "_mapdl_on_hpc")
3825-
and self._mapdl_on_hpc
3826-
and hasattr(self, "finish_job_on_exit")
3827-
and self.finish_job_on_exit
3828-
):
3819+
if self._exited:
3820+
return
38293821

3830-
self.kill_job(self.jobid)
3822+
if not self._start_instance:
3823+
# Early skip if start_instance is False
3824+
return
38313825

3832-
if not self._start_instance:
3833-
return
3826+
# Killing the instance if we launched it.
3827+
if self._launched:
3828+
self._exit_mapdl(self._path)
38343829

3835-
except Exception as e: # nosec B110
3836-
# This is on clean up.
3837-
pass # nosec B110
3830+
# Exiting HPC job
3831+
if self._mapdl_on_hpc and self.finish_job_on_exit:
3832+
self.kill_job(self.jobid)

tests/conftest.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -658,8 +658,9 @@ def mapdl(request, tmpdir_factory):
658658
with pytest.raises(MapdlExitedError):
659659
mapdl._send_command_stream("/PREP7")
660660

661-
# Delete Mapdl object
662-
del mapdl
661+
# Delete Mapdl object
662+
mapdl.exit()
663+
del mapdl
663664

664665

665666
################################################################

tests/test_console.py

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
"""
2828
import os
2929
import time
30+
from unittest.mock import patch
31+
from warnings import catch_warnings
3032

3133
import pytest
3234

@@ -111,6 +113,8 @@ def test_basic_command(cleared, mapdl_console):
111113
def test_allow_ignore(mapdl_console, cleared):
112114
mapdl_console.allow_ignore = False
113115
assert mapdl_console.allow_ignore is False
116+
117+
mapdl_console.finish()
114118
with pytest.raises(pymapdl.errors.MapdlInvalidRoutineError):
115119
mapdl_console.k()
116120

@@ -132,7 +136,7 @@ def test_chaining(mapdl_console, cleared):
132136

133137

134138
def test_e(mapdl_console, cleared):
135-
mapdl.prep7()
139+
mapdl_console.prep7()
136140
mapdl_console.et("", 183)
137141
n0 = mapdl_console.n("", 0, 0, 0)
138142
n1 = mapdl_console.n("", 1, 0, 0)
@@ -613,7 +617,10 @@ def test_load_table(mapdl_console, cleared):
613617
]
614618
)
615619
mapdl_console.load_table("my_conv", my_conv, "TIME")
616-
assert np.allclose(mapdl_console.parameters["my_conv"], my_conv[:, -1])
620+
assert np.allclose(
621+
mapdl_console.parameters["my_conv"].reshape(-1, 1),
622+
my_conv[:, -1].reshape(-1, 1),
623+
)
617624

618625

619626
def test_mode_console(mapdl_console, cleared):
@@ -647,3 +654,57 @@ def test_console_apdl_logging_start(tmpdir):
647654
assert "K,2,1,0,0" in text
648655
assert "K,3,1,1,0" in text
649656
assert "K,4,0,1,0" in text
657+
658+
659+
def test__del__console():
660+
from ansys.mapdl.core.mapdl_console import MapdlConsole
661+
662+
class FakeProcess:
663+
def sendline(self, command):
664+
pass
665+
666+
class DummyMapdl(MapdlConsole):
667+
@property
668+
def _process(self):
669+
return _proc
670+
671+
def __init__(self):
672+
self._proc = FakeProcess()
673+
674+
with (
675+
patch.object(DummyMapdl, "_process", autospec=True) as mock_process,
676+
patch.object(DummyMapdl, "_close_apdl_log") as mock_close_log,
677+
):
678+
679+
mock_close_log.return_value = None
680+
681+
# Setup
682+
mapdl = DummyMapdl()
683+
684+
del mapdl
685+
686+
mock_close_log.assert_not_called()
687+
assert [each.args[0] for each in mock_process.sendline.call_args_list] == [
688+
"FINISH",
689+
"EXIT",
690+
]
691+
692+
693+
@pytest.mark.parametrize("close_log", [True, False])
694+
def test_exit_console(mapdl_console, close_log):
695+
with (
696+
patch.object(mapdl_console, "_close_apdl_log") as mock_close_log,
697+
patch.object(mapdl_console, "_exit") as mock_exit,
698+
):
699+
mock_exit.return_value = None
700+
mock_close_log.return_value = None
701+
702+
with catch_warnings(record=True):
703+
mapdl_console.exit(close_log=close_log, timeout=1)
704+
705+
if close_log:
706+
mock_close_log.assert_called_once()
707+
else:
708+
mock_close_log.assert_not_called()
709+
710+
mock_exit.assert_called_once()

tests/test_mapdl.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2715,3 +2715,53 @@ def my_func(i):
27152715

27162716
for i in range(1_000_000):
27172717
my_func(i)
2718+
2719+
2720+
@pytest.mark.parametrize("start_instance", [True, False])
2721+
@pytest.mark.parametrize("exited", [True, False])
2722+
@pytest.mark.parametrize("launched", [True, False])
2723+
@pytest.mark.parametrize("on_hpc", [True, False])
2724+
@pytest.mark.parametrize("finish_job_on_exit", [True, False])
2725+
def test_garbage_clean_del(
2726+
start_instance, exited, launched, on_hpc, finish_job_on_exit
2727+
):
2728+
from ansys.mapdl.core import Mapdl
2729+
2730+
class DummyMapdl(Mapdl):
2731+
def __init__(self):
2732+
pass
2733+
2734+
with (
2735+
patch.object(DummyMapdl, "_exit_mapdl") as mock_exit,
2736+
patch.object(DummyMapdl, "kill_job") as mock_kill,
2737+
):
2738+
2739+
mock_exit.return_value = None
2740+
mock_kill.return_value = None
2741+
2742+
# Setup
2743+
mapdl = DummyMapdl()
2744+
mapdl._path = ""
2745+
mapdl._jobid = 1001
2746+
2747+
# Config
2748+
mapdl._start_instance = start_instance
2749+
mapdl._exited = exited
2750+
mapdl._launched = launched
2751+
mapdl._mapdl_on_hpc = on_hpc
2752+
mapdl.finish_job_on_exit = finish_job_on_exit
2753+
2754+
del mapdl
2755+
2756+
if exited or not start_instance or not launched:
2757+
mock_exit.assert_not_called()
2758+
else:
2759+
mock_exit.assert_called_once()
2760+
2761+
if exited or not start_instance:
2762+
mock_kill.assert_not_called()
2763+
else:
2764+
if on_hpc and finish_job_on_exit:
2765+
mock_kill.assert_called_once()
2766+
else:
2767+
mock_kill.assert_not_called()

0 commit comments

Comments
 (0)