From 61d7a6c06eaf814625ca9e494b8ad98e57f16b6c Mon Sep 17 00:00:00 2001 From: "jean-francois.figue" Date: Wed, 12 Feb 2025 10:59:04 +0100 Subject: [PATCH 01/11] docs: example showing how to use RetryDiscipline. --- .../types/plot_retry_discipline.py | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 doc_src/_examples/disciplines/types/plot_retry_discipline.py diff --git a/doc_src/_examples/disciplines/types/plot_retry_discipline.py b/doc_src/_examples/disciplines/types/plot_retry_discipline.py new file mode 100644 index 0000000000..3b243fe38b --- /dev/null +++ b/doc_src/_examples/disciplines/types/plot_retry_discipline.py @@ -0,0 +1,154 @@ +# Copyright 2021 IRT Saint Exupéry, https://www.irt-saintexupery.com +# +# This work is licensed under a BSD 0-Clause License. +# +# Permission to use, copy, modify, and/or distribute this software +# for any purpose with or without fee is hereby granted. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL +# THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, +# OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING +# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION +# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# Contributors: +# INITIAL AUTHORS - initial API and implementation and/or initial +# documentation +# :author: Jean-François Figué +# OTHER AUTHORS - MACROSCOPIC CHANGES +""" +Create a retry discipline +========================= +""" + +from __future__ import annotations + +import sys +import time +from typing import TYPE_CHECKING + +from numpy import array + +from gemseo import configure_logger +from gemseo import create_discipline +from gemseo.core.discipline import Discipline +from gemseo.disciplines.wrappers.retry_discipline import RetryDiscipline + +if TYPE_CHECKING: + from gemseo.typing import StrKeyMapping + +LOGGER = configure_logger(level="INFO") + +# %% +# First create the discipline to wrap in :class:`.RetryDiscipline` +# ---------------------------------------------------------------- +# For that example, we create an :class:`.AnalyticDiscipline`, +# using an analytic formula: :math:`y=1/x`. +# This Analytic discipline will crash when :math:`x=0` , it permits to illustrate +# the features of the :class:`.RetryDiscipline`. +analytic_disc = create_discipline("AnalyticDiscipline", expressions={"y": "1.0/x"}) + +# %% +# Let's wrap the :class:`.AnalyticDiscipline` in the :class:`.RetryDiscipline` and +# set the maximum number of execution retry to ``n_retry=3``. It means that, if the +# computation of ``y`` fails, another attempt to calculate ``y`` will be performed, +# and so on until it has been retried 3 times. + +retry_disc = RetryDiscipline(analytic_disc, n_retry=3) + +# %% +# Execute the discipline +# ---------------------- +# - First try with a value of :math:`x=2`. +# + +retry_disc.execute({"x": array([2.0])}) + +# %% +# We can see that the computation is correctly performed, :math:`y=0.5` , and the +# number of attempt to achieve the calculation is only :math:`1`. + +LOGGER.info("retry_disc.n_executions = %s", retry_disc.n_executions) + +# %% +# Execute the discipline raising an exception +# ------------------------------------------- +# - Let's try now with :math:`x=0`. +# +# As we know that the execution will crash, we must use a try except statement to +# catch the error, and take it into account, otherwise the program is stopped. +# +# In parameter ``fatal_exceptions``, to be expressed as a ``tuple``, we put the +# ``ZeroDivisionError`` exception. It means that if that error is raised, then the +# discipline :class:`.RetryDiscipline` will stop execution, without retrying another +# attempt (in the log below, we can verify the number of attempt is only :math:`1`). + +retry_disc = RetryDiscipline( + analytic_disc, n_retry=3, fatal_exceptions=(ZeroDivisionError,) +) + +try: + retry_disc.execute({"x": array([0.0])}) +except ZeroDivisionError: + LOGGER.info("There is an issue ...") + LOGGER.info("It is a 'ZeroDivisionError: float division by zero'") + LOGGER.info("Developper must take this point into account in his code.") +except Exception as err: # noqa: BLE001 + LOGGER.info("There is another issue ... :") + LOGGER.info(err) + +LOGGER.info("retry_disc.n_executions = %s", retry_disc.n_executions) + +LOGGER.info("... program continues ...") + + +# %% +# Trying the ``timeout`` option +# ----------------------------- +# If you want to limit the duration of the wrapped discipline, use the ``timeout`` +# option. Here is an example, by implementing a class with a ``time.sleep`` in the +# ``_run`` method: + + +class DisciplineLongTimeRunning(Discipline): + """A discipline that could run for a while, to test the timeout feature.""" + + def _run(self, input_data: StrKeyMapping) -> None: + time.sleep(5.0) + + +# %% +# Now we wrap it in :class:`.RetryDiscipline` , and set ``timeout = 2`` (seconds). +# In the ``INFO line``, we can see the time the discipline started and ended its +# execution. +# +# The line ``LOGGER.info("Discipline completed, due to a TimeoutError.")`` should be +# executed as the timeout is reached. + +retry_disc = RetryDiscipline(analytic_disc, n_retry=1, timeout=2.0) + +sys.tracebacklimit = 0 +try: + LOGGER.info("Running discipline...") + retry_disc.execute({}) + LOGGER.info("Discipline completed without reaching the time limit.") +except TimeoutError: + LOGGER.info("Discipline completed, due to a TimeoutError.") + +# %% +# In some cases, this option could be very useful. For example if you wrapped an SSH +# discipline (please report to gemseo-ssh plugin) in :class:`.RetryDiscipline`. In +# that context, it can be important to limit the duration in case of a ssh +# connexion that is too slow. + + +# %% +# .. note:: +# +# ``sys.tracebacklimit = 0`` to limit message output by exception. In order the +# output is only focused on what we aim to demonstrate with that example. +# Please don't put this statement in normal use, otherwise you could miss some +# important messages in the output. +# -- GitLab From 494c9dee9d0024fb7b97e346cedc7536b80c99f2 Mon Sep 17 00:00:00 2001 From: "jean-francois.figue" Date: Wed, 12 Feb 2025 11:22:41 +0100 Subject: [PATCH 02/11] docs: modified 2 sentences --- doc_src/_examples/disciplines/types/plot_retry_discipline.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc_src/_examples/disciplines/types/plot_retry_discipline.py b/doc_src/_examples/disciplines/types/plot_retry_discipline.py index 3b243fe38b..af129de47d 100644 --- a/doc_src/_examples/disciplines/types/plot_retry_discipline.py +++ b/doc_src/_examples/disciplines/types/plot_retry_discipline.py @@ -52,7 +52,7 @@ analytic_disc = create_discipline("AnalyticDiscipline", expressions={"y": "1.0/x # %% # Let's wrap the :class:`.AnalyticDiscipline` in the :class:`.RetryDiscipline` and -# set the maximum number of execution retry to ``n_retry=3``. It means that, if the +# set the maximum number of execution retries to ``n_retry=3``. It means that, if the # computation of ``y`` fails, another attempt to calculate ``y`` will be performed, # and so on until it has been retried 3 times. @@ -78,7 +78,7 @@ LOGGER.info("retry_disc.n_executions = %s", retry_disc.n_executions) # - Let's try now with :math:`x=0`. # # As we know that the execution will crash, we must use a try except statement to -# catch the error, and take it into account, otherwise the program is stopped. +# catch the error, and take it into account, otherwise the program will stop. # # In parameter ``fatal_exceptions``, to be expressed as a ``tuple``, we put the # ``ZeroDivisionError`` exception. It means that if that error is raised, then the -- GitLab From dbc0cb4509ff5dace2ca815d74895bed1277deaf Mon Sep 17 00:00:00 2001 From: "jean-francois.figue" Date: Wed, 12 Feb 2025 13:39:40 +0100 Subject: [PATCH 03/11] docs: fix an issue with timeout demo --- doc_src/_examples/disciplines/types/plot_retry_discipline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc_src/_examples/disciplines/types/plot_retry_discipline.py b/doc_src/_examples/disciplines/types/plot_retry_discipline.py index af129de47d..4bbf04d2b2 100644 --- a/doc_src/_examples/disciplines/types/plot_retry_discipline.py +++ b/doc_src/_examples/disciplines/types/plot_retry_discipline.py @@ -127,7 +127,7 @@ class DisciplineLongTimeRunning(Discipline): # The line ``LOGGER.info("Discipline completed, due to a TimeoutError.")`` should be # executed as the timeout is reached. -retry_disc = RetryDiscipline(analytic_disc, n_retry=1, timeout=2.0) +retry_disc = RetryDiscipline(DisciplineLongTimeRunning(), n_retry=1, timeout=2.0) sys.tracebacklimit = 0 try: -- GitLab From d91a19a9ab373ce445456433f64470735ca73d80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Figu=C3=A9?= Date: Wed, 12 Feb 2025 16:14:41 +0000 Subject: [PATCH 04/11] docs: some improvments after first code review. --- .../types/plot_retry_discipline.py | 84 +++++++++---------- 1 file changed, 39 insertions(+), 45 deletions(-) diff --git a/doc_src/_examples/disciplines/types/plot_retry_discipline.py b/doc_src/_examples/disciplines/types/plot_retry_discipline.py index 4bbf04d2b2..31e15de0d1 100644 --- a/doc_src/_examples/disciplines/types/plot_retry_discipline.py +++ b/doc_src/_examples/disciplines/types/plot_retry_discipline.py @@ -21,6 +21,11 @@ """ Create a retry discipline ========================= + +Sometimes, +the execution of a discipline can fail and work after several repetitions. +The :class:`.RetryDiscipline` facilitates the management of these failures and repetitions. +This class illustrates this feature. """ from __future__ import annotations @@ -39,58 +44,48 @@ from gemseo.disciplines.wrappers.retry_discipline import RetryDiscipline if TYPE_CHECKING: from gemseo.typing import StrKeyMapping -LOGGER = configure_logger(level="INFO") +LOGGER = configure_logger() # %% -# First create the discipline to wrap in :class:`.RetryDiscipline` -# ---------------------------------------------------------------- -# For that example, we create an :class:`.AnalyticDiscipline`, -# using an analytic formula: :math:`y=1/x`. -# This Analytic discipline will crash when :math:`x=0` , it permits to illustrate -# the features of the :class:`.RetryDiscipline`. +# Toy discipline +# -------------- +# For that example, +# we create an :class:`.AnalyticDiscipline` to evaluate the expression :math:`y=1/x`: analytic_disc = create_discipline("AnalyticDiscipline", expressions={"y": "1.0/x"}) # %% -# Let's wrap the :class:`.AnalyticDiscipline` in the :class:`.RetryDiscipline` and -# set the maximum number of execution retries to ``n_retry=3``. It means that, if the -# computation of ``y`` fails, another attempt to calculate ``y`` will be performed, -# and so on until it has been retried 3 times. - +# Execution without failure +# ------------------------- +# Let's wrap this toy discipline in a :class:`.RetryDiscipline` +# parametrized by a maximum number of 3 execution attempts: retry_disc = RetryDiscipline(analytic_disc, n_retry=3) # %% -# Execute the discipline -# ---------------------- -# - First try with a value of :math:`x=2`. -# - +# We can execute this :class:`.RetryDiscipline` at :math:`x=2`: retry_disc.execute({"x": array([2.0])}) +retry_disc.io.data # %% -# We can see that the computation is correctly performed, :math:`y=0.5` , and the -# number of attempt to achieve the calculation is only :math:`1`. - +# and verify that the computation is correctly performed, :math:`y=0.5`, +# with only one execution attempt: LOGGER.info("retry_disc.n_executions = %s", retry_disc.n_executions) # %% -# Execute the discipline raising an exception -# ------------------------------------------- -# - Let's try now with :math:`x=0`. -# -# As we know that the execution will crash, we must use a try except statement to -# catch the error, and take it into account, otherwise the program will stop. -# -# In parameter ``fatal_exceptions``, to be expressed as a ``tuple``, we put the -# ``ZeroDivisionError`` exception. It means that if that error is raised, then the -# discipline :class:`.RetryDiscipline` will stop execution, without retrying another -# attempt (in the log below, we can verify the number of attempt is only :math:`1`). - +# Execution with failure +# ---------------------- +# Let's try now with :math:`x=0`, which will make the toy discipline crash. +# To do this, +# we need to define the fatal exceptions for which the execution is not retried. +# It means that if that error is raised, +# then the discipline :class:`.RetryDiscipline` will stop execution, +# without retrying another attempt +# (in the log below, we can verify the number of attempt is only :math:`1`). retry_disc = RetryDiscipline( analytic_disc, n_retry=3, fatal_exceptions=(ZeroDivisionError,) ) try: - retry_disc.execute({"x": array([0.0])}) + retry_disc.execute() except ZeroDivisionError: LOGGER.info("There is an issue ...") LOGGER.info("It is a 'ZeroDivisionError: float division by zero'") @@ -101,9 +96,6 @@ except Exception as err: # noqa: BLE001 LOGGER.info("retry_disc.n_executions = %s", retry_disc.n_executions) -LOGGER.info("... program continues ...") - - # %% # Trying the ``timeout`` option # ----------------------------- @@ -120,7 +112,8 @@ class DisciplineLongTimeRunning(Discipline): # %% -# Now we wrap it in :class:`.RetryDiscipline` , and set ``timeout = 2`` (seconds). +# Now we wrap it in :class:`.RetryDiscipline`, +# and set the ``timeout`` argument to 2 seconds. # In the ``INFO line``, we can see the time the discipline started and ended its # execution. # @@ -138,16 +131,17 @@ except TimeoutError: LOGGER.info("Discipline completed, due to a TimeoutError.") # %% -# In some cases, this option could be very useful. For example if you wrapped an SSH -# discipline (please report to gemseo-ssh plugin) in :class:`.RetryDiscipline`. In -# that context, it can be important to limit the duration in case of a ssh -# connexion that is too slow. - - -# %% +# In some cases, +# this option could be very useful. +# For example if you wrap an SSH discipline +# (see `gemseo-ssh plugin__`) +# in :class:`.RetryDiscipline`. +# In that context, +# it can be important to limit the duration when an ssh connexion is too slow. +# # .. note:: # -# ``sys.tracebacklimit = 0`` to limit message output by exception. In order the +# Set ``sys.tracebacklimit = 0`` to limit message output by exception. In order the # output is only focused on what we aim to demonstrate with that example. # Please don't put this statement in normal use, otherwise you could miss some # important messages in the output. -- GitLab From 3d147b3e37d4d05e814a10047cc655dc0e0db98f Mon Sep 17 00:00:00 2001 From: "jean-francois.figue" Date: Wed, 12 Feb 2025 17:16:44 +0100 Subject: [PATCH 05/11] docs: fix trailing whitespace --- .../types/plot_retry_discipline.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/doc_src/_examples/disciplines/types/plot_retry_discipline.py b/doc_src/_examples/disciplines/types/plot_retry_discipline.py index 31e15de0d1..327d5a563f 100644 --- a/doc_src/_examples/disciplines/types/plot_retry_discipline.py +++ b/doc_src/_examples/disciplines/types/plot_retry_discipline.py @@ -22,7 +22,7 @@ Create a retry discipline ========================= -Sometimes, +Sometimes, the execution of a discipline can fail and work after several repetitions. The :class:`.RetryDiscipline` facilitates the management of these failures and repetitions. This class illustrates this feature. @@ -49,7 +49,7 @@ LOGGER = configure_logger() # %% # Toy discipline # -------------- -# For that example, +# For that example, # we create an :class:`.AnalyticDiscipline` to evaluate the expression :math:`y=1/x`: analytic_disc = create_discipline("AnalyticDiscipline", expressions={"y": "1.0/x"}) @@ -66,7 +66,7 @@ retry_disc.execute({"x": array([2.0])}) retry_disc.io.data # %% -# and verify that the computation is correctly performed, :math:`y=0.5`, +# and verify that the computation is correctly performed, :math:`y=0.5`, # with only one execution attempt: LOGGER.info("retry_disc.n_executions = %s", retry_disc.n_executions) @@ -78,7 +78,7 @@ LOGGER.info("retry_disc.n_executions = %s", retry_disc.n_executions) # we need to define the fatal exceptions for which the execution is not retried. # It means that if that error is raised, # then the discipline :class:`.RetryDiscipline` will stop execution, -# without retrying another attempt +# without retrying another attempt # (in the log below, we can verify the number of attempt is only :math:`1`). retry_disc = RetryDiscipline( analytic_disc, n_retry=3, fatal_exceptions=(ZeroDivisionError,) @@ -112,7 +112,7 @@ class DisciplineLongTimeRunning(Discipline): # %% -# Now we wrap it in :class:`.RetryDiscipline`, +# Now we wrap it in :class:`.RetryDiscipline`, # and set the ``timeout`` argument to 2 seconds. # In the ``INFO line``, we can see the time the discipline started and ended its # execution. @@ -131,12 +131,12 @@ except TimeoutError: LOGGER.info("Discipline completed, due to a TimeoutError.") # %% -# In some cases, -# this option could be very useful. -# For example if you wrap an SSH discipline -# (see `gemseo-ssh plugin__`) -# in :class:`.RetryDiscipline`. -# In that context, +# In some cases, +# this option could be very useful. +# For example if you wrap an SSH discipline +# (see `gemseo-ssh plugin__`) +# in :class:`.RetryDiscipline`. +# In that context, # it can be important to limit the duration when an ssh connexion is too slow. # # .. note:: -- GitLab From 2ca68ebdd0ce1e5235c436b48d6b637de76db57a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Figu=C3=A9?= Date: Wed, 12 Feb 2025 16:26:01 +0000 Subject: [PATCH 06/11] docs: changed analytic discipline formula. --- .../_examples/disciplines/types/plot_retry_discipline.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/doc_src/_examples/disciplines/types/plot_retry_discipline.py b/doc_src/_examples/disciplines/types/plot_retry_discipline.py index 327d5a563f..2dbdba3865 100644 --- a/doc_src/_examples/disciplines/types/plot_retry_discipline.py +++ b/doc_src/_examples/disciplines/types/plot_retry_discipline.py @@ -51,7 +51,11 @@ LOGGER = configure_logger() # -------------- # For that example, # we create an :class:`.AnalyticDiscipline` to evaluate the expression :math:`y=1/x`: -analytic_disc = create_discipline("AnalyticDiscipline", expressions={"y": "1.0/x"}) +analytic_disc = create_discipline("AnalyticDiscipline", expressions={"y": "1/x"}) + +# %% +# This discipline will raise a ``ZeroDivisionError`` when :math:`x=0`: +analytic_disc.execute() # %% # Execution without failure -- GitLab From 8689d83e497264424cbe4a71cc08642b3a9d7f20 Mon Sep 17 00:00:00 2001 From: "jean-francois.figue" Date: Thu, 13 Feb 2025 10:59:25 +0100 Subject: [PATCH 07/11] docs: fix problem with execute --- doc_src/_examples/disciplines/types/plot_retry_discipline.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc_src/_examples/disciplines/types/plot_retry_discipline.py b/doc_src/_examples/disciplines/types/plot_retry_discipline.py index 2dbdba3865..47825d4b3d 100644 --- a/doc_src/_examples/disciplines/types/plot_retry_discipline.py +++ b/doc_src/_examples/disciplines/types/plot_retry_discipline.py @@ -55,7 +55,6 @@ analytic_disc = create_discipline("AnalyticDiscipline", expressions={"y": "1/x"} # %% # This discipline will raise a ``ZeroDivisionError`` when :math:`x=0`: -analytic_disc.execute() # %% # Execution without failure @@ -67,7 +66,7 @@ retry_disc = RetryDiscipline(analytic_disc, n_retry=3) # %% # We can execute this :class:`.RetryDiscipline` at :math:`x=2`: retry_disc.execute({"x": array([2.0])}) -retry_disc.io.data +LOGGER.info(retry_disc.io.data) # %% # and verify that the computation is correctly performed, :math:`y=0.5`, -- GitLab From 6df72766d73b09b933dbcb4bd005da5ded46bf2b Mon Sep 17 00:00:00 2001 From: "jean-francois.figue" Date: Thu, 13 Feb 2025 18:12:09 +0100 Subject: [PATCH 08/11] refactor: renamed some variables in retry_discipline and modify the doc --- .../types/plot_retry_discipline.py | 44 ++++++++++++------- .../disciplines/wrappers/retry_discipline.py | 22 +++++----- .../wrappers/test_retry_discipline.py | 34 +++++++------- 3 files changed, 57 insertions(+), 43 deletions(-) diff --git a/doc_src/_examples/disciplines/types/plot_retry_discipline.py b/doc_src/_examples/disciplines/types/plot_retry_discipline.py index 47825d4b3d..5025f7d14e 100644 --- a/doc_src/_examples/disciplines/types/plot_retry_discipline.py +++ b/doc_src/_examples/disciplines/types/plot_retry_discipline.py @@ -54,50 +54,62 @@ LOGGER = configure_logger() analytic_disc = create_discipline("AnalyticDiscipline", expressions={"y": "1/x"}) # %% -# This discipline will raise a ``ZeroDivisionError`` when :math:`x=0`: +# This discipline will raise a ``ZeroDivisionError`` when :math:`x=0`. # %% # Execution without failure # ------------------------- # Let's wrap this toy discipline in a :class:`.RetryDiscipline` # parametrized by a maximum number of 3 execution attempts: -retry_disc = RetryDiscipline(analytic_disc, n_retry=3) +retry_disc = RetryDiscipline(analytic_disc, n_trials=3) # %% # We can execute this :class:`.RetryDiscipline` at :math:`x=2`: retry_disc.execute({"x": array([2.0])}) -LOGGER.info(retry_disc.io.data) +retry_disc.io.data # %% # and verify that the computation is correctly performed, :math:`y=0.5`, # with only one execution attempt: -LOGGER.info("retry_disc.n_executions = %s", retry_disc.n_executions) +retry_disc.n_executions # %% # Execution with failure # ---------------------- # Let's try now with :math:`x=0`, which will make the toy discipline crash. +# +# - First, run the discipline with 3 attempts: + +retry_disc = RetryDiscipline(analytic_disc, n_trials=3) +try: + retry_disc.execute() +except Exception as err: # noqa: BLE001 + LOGGER.info(err) + +# %% +# We can verify, the calculation has been tried 3 times: +LOGGER.info(retry_disc.n_executions) + + +# %% +# - If such an issue occurs, we just want to retry it and just do something else. # To do this, # we need to define the fatal exceptions for which the execution is not retried. # It means that if that error is raised, # then the discipline :class:`.RetryDiscipline` will stop execution, -# without retrying another attempt -# (in the log below, we can verify the number of attempt is only :math:`1`). +# without retrying another attempt. retry_disc = RetryDiscipline( - analytic_disc, n_retry=3, fatal_exceptions=(ZeroDivisionError,) + analytic_disc, n_trials=3, fatal_exceptions=[ZeroDivisionError] ) try: retry_disc.execute() except ZeroDivisionError: - LOGGER.info("There is an issue ...") - LOGGER.info("It is a 'ZeroDivisionError: float division by zero'") - LOGGER.info("Developper must take this point into account in his code.") -except Exception as err: # noqa: BLE001 - LOGGER.info("There is another issue ... :") - LOGGER.info(err) + LOGGER.info("Manage this fatal exception.") -LOGGER.info("retry_disc.n_executions = %s", retry_disc.n_executions) +# %% +# We can verify the number of attempt is only :math:`1`: +retry_disc.n_executions # %% # Trying the ``timeout`` option @@ -123,7 +135,7 @@ class DisciplineLongTimeRunning(Discipline): # The line ``LOGGER.info("Discipline completed, due to a TimeoutError.")`` should be # executed as the timeout is reached. -retry_disc = RetryDiscipline(DisciplineLongTimeRunning(), n_retry=1, timeout=2.0) +retry_disc = RetryDiscipline(DisciplineLongTimeRunning(), n_trials=1, timeout=2.0) sys.tracebacklimit = 0 try: @@ -137,7 +149,7 @@ except TimeoutError: # In some cases, # this option could be very useful. # For example if you wrap an SSH discipline -# (see `gemseo-ssh plugin__`) +# (see `gemseo-ssh plugin `__) # in :class:`.RetryDiscipline`. # In that context, # it can be important to limit the duration when an ssh connexion is too slow. diff --git a/src/gemseo/disciplines/wrappers/retry_discipline.py b/src/gemseo/disciplines/wrappers/retry_discipline.py index d7d617713d..40fa2ff159 100644 --- a/src/gemseo/disciplines/wrappers/retry_discipline.py +++ b/src/gemseo/disciplines/wrappers/retry_discipline.py @@ -66,7 +66,7 @@ class RetryDiscipline(Discipline): ) """The possible timeout exceptions that can be raised during execution.""" - n_retry: int + n_trials: int """The number of retry of the discipline.""" wait_time: float @@ -82,7 +82,7 @@ class RetryDiscipline(Discipline): def __init__( self, discipline: Discipline, - n_retry: int = 5, + n_trials: int = 5, wait_time: float = 0.0, timeout: float = math.inf, fatal_exceptions: Iterable[type[Exception]] = (), @@ -90,7 +90,7 @@ class RetryDiscipline(Discipline): """ Args: discipline: The discipline to wrap in the retry loop. - n_retry: The number of retry of the discipline. + n_trials: The number of trials of the discipline. wait_time: The time to wait between 2 retries (in seconds). timeout: The maximum duration, in seconds, that the discipline is allowed to run. If this time limit is exceeded, the @@ -109,7 +109,7 @@ class RetryDiscipline(Discipline): self.__discipline = discipline self.io.input_grammar = discipline.io.input_grammar self.io.output_grammar = discipline.io.output_grammar - self.n_retry = n_retry + self.n_trials = n_trials self.wait_time = wait_time self.timeout = timeout self.fatal_exceptions = fatal_exceptions @@ -123,11 +123,13 @@ class RetryDiscipline(Discipline): def _run(self, input_data: StrKeyMapping) -> StrKeyMapping | None: self.__n_executions = 0 - for n_try in range(1, self.n_retry + 1): + for n_trial in range(1, self.n_trials + 1): self.__n_executions += 1 LOGGER.debug( - "Trying to execute the discipline: attempt %d/%d", n_try, self.n_retry + "Trying to execute the discipline: attempt %d/%d", + n_trial, + self.n_trials, ) try: @@ -144,7 +146,7 @@ class RetryDiscipline(Discipline): current_error = TimeoutError(msg) except Exception as error: # noqa: BLE001 - if isinstance(error, self.fatal_exceptions): + if isinstance(error, tuple(self.fatal_exceptions)): LOGGER.info( "Failed to execute discipline %s, " "aborting retry because of the exception type %s.", @@ -155,14 +157,14 @@ class RetryDiscipline(Discipline): current_error = error time.sleep(self.wait_time) - - plural_suffix = "s" if self.n_retry > 1 else "" + plural_suffix = "s" if self.n_trials > 1 else "" LOGGER.error( "Failed to execute discipline %s after %d attempt%s.", self.__discipline.name, - self.n_retry, + self.n_trials, plural_suffix, ) + raise current_error def _execute_discipline(self, input_data: StrKeyMapping) -> StrKeyMapping: diff --git a/tests/disciplines/wrappers/test_retry_discipline.py b/tests/disciplines/wrappers/test_retry_discipline.py index 8d2e3ff85d..7af07c2e9d 100644 --- a/tests/disciplines/wrappers/test_retry_discipline.py +++ b/tests/disciplines/wrappers/test_retry_discipline.py @@ -85,13 +85,13 @@ def test_retry_discipline(an_analytic_discipline, timeout, caplog) -> None: @pytest.mark.parametrize("wait_time", [0.5, 1.0]) -@pytest.mark.parametrize("n_retry", [1, 3]) +@pytest.mark.parametrize("n_trials", [1, 3]) def test_failure_retry_discipline_with_timeout( - an_analytic_discipline, n_retry, wait_time, caplog + an_analytic_discipline, n_trials, wait_time, caplog ) -> None: """Test failure of the discipline with a too much very short timeout.""" disc_with_timeout = RetryDiscipline( - an_analytic_discipline, timeout=1e-4, n_retry=n_retry, wait_time=wait_time + an_analytic_discipline, timeout=1e-4, n_trials=n_trials, wait_time=wait_time ) with ( @@ -105,16 +105,16 @@ def test_failure_retry_discipline_with_timeout( disc_with_timeout.execute({"x": array([4.0])}) elapsed_time = timer.elapsed_time - assert elapsed_time > 0.05 + (n_retry - 1) * wait_time + assert elapsed_time > 0.05 + (n_trials - 1) * wait_time - assert disc_with_timeout.n_executions == n_retry + assert disc_with_timeout.n_executions == n_trials assert disc_with_timeout.local_data == {"x": array([4.0])} assert "Process stopped as it exceeds timeout" in caplog.text - plural_suffix = "s" if n_retry > 1 else "" + plural_suffix = "s" if n_trials > 1 else "" log_message = ( - f"Failed to execute discipline AnalyticDiscipline after {n_retry}" + f"Failed to execute discipline AnalyticDiscipline after {n_trials}" f" attempt{plural_suffix}." ) assert log_message in caplog.text @@ -123,9 +123,9 @@ def test_failure_retry_discipline_with_timeout( def test_failure_zero_division_error(a_crashing_analytic_discipline, caplog) -> None: """Test failure of the discipline with a bad x entry. - In order to catch the ZeroDivisionError, set n_retry=1 + In order to catch the ZeroDivisionError, set n_trials=1 """ - disc = RetryDiscipline(a_crashing_analytic_discipline, n_retry=1) + disc = RetryDiscipline(a_crashing_analytic_discipline, n_trials=1) with pytest.raises(ZeroDivisionError, match="float division by zero"): disc.execute({"x": array([0.0])}) @@ -144,9 +144,9 @@ def test_failure_zero_division_error(a_crashing_analytic_discipline, caplog) -> (OverflowError, FloatingPointError, ZeroDivisionError), ], ) -@pytest.mark.parametrize("n_try", [1, 3]) +@pytest.mark.parametrize("n_trials", [1, 3]) def test_failure_zero_division_error_with_timeout( - n_try: int, + n_trials: int, fatal_exceptions: Iterable[type[Exception]], a_crashing_analytic_discipline, caplog, @@ -154,11 +154,11 @@ def test_failure_zero_division_error_with_timeout( """Test failure of the discipline with timeout and a bad x entry. In order to catch the ZeroDivisionError that arises before timeout (5s), test with - n_retry=1 and 3 to be sure every case is ok. + n_trials=1 and 3 to be sure every case is ok. """ disc = RetryDiscipline( a_crashing_analytic_discipline, - n_retry=n_try, + n_trials=n_trials, timeout=10.0, fatal_exceptions=fatal_exceptions, ) @@ -184,7 +184,7 @@ def test_a_not_implemented_error_analytic_discipline( """ retry_discipline = RetryDiscipline( a_crashing_discipline_in_run, - n_retry=5, + n_trials=5, timeout=100.0, fatal_exceptions=( ZeroDivisionError, @@ -212,10 +212,10 @@ def test_retry_discipline_timeout_feature( a_long_time_running_discipline, caplog ) -> None: """Test the timeout feature of discipline with a long computation.""" - n_retry = 1 + n_trials = 1 disc_with_timeout = RetryDiscipline( - a_long_time_running_discipline, timeout=2.0, n_retry=n_retry + a_long_time_running_discipline, timeout=2.0, n_trials=n_trials ) with pytest.raises( TimeoutError, @@ -224,7 +224,7 @@ def test_retry_discipline_timeout_feature( ): disc_with_timeout.execute({"x": array([0.0])}) - assert disc_with_timeout.n_executions == n_retry + assert disc_with_timeout.n_executions == n_trials assert disc_with_timeout.local_data == {} assert "Process stopped as it exceeds timeout" in caplog.text -- GitLab From a9ae0598651282c8c038de877c4b999af35f332c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Figu=C3=A9?= Date: Fri, 14 Feb 2025 09:33:26 +0000 Subject: [PATCH 09/11] docs: improve some comments. --- .../types/plot_retry_discipline.py | 38 +++++++++---------- .../disciplines/wrappers/retry_discipline.py | 8 ++-- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/doc_src/_examples/disciplines/types/plot_retry_discipline.py b/doc_src/_examples/disciplines/types/plot_retry_discipline.py index 5025f7d14e..3eeddc654f 100644 --- a/doc_src/_examples/disciplines/types/plot_retry_discipline.py +++ b/doc_src/_examples/disciplines/types/plot_retry_discipline.py @@ -55,8 +55,7 @@ analytic_disc = create_discipline("AnalyticDiscipline", expressions={"y": "1/x"} # %% # This discipline will raise a ``ZeroDivisionError`` when :math:`x=0`. - -# %% +# # Execution without failure # ------------------------- # Let's wrap this toy discipline in a :class:`.RetryDiscipline` @@ -78,7 +77,7 @@ retry_disc.n_executions # ---------------------- # Let's try now with :math:`x=0`, which will make the toy discipline crash. # -# - First, run the discipline with 3 attempts: +# First, run the discipline with 3 attempts: retry_disc = RetryDiscipline(analytic_disc, n_trials=3) try: @@ -90,14 +89,14 @@ except Exception as err: # noqa: BLE001 # We can verify, the calculation has been tried 3 times: LOGGER.info(retry_disc.n_executions) - # %% -# - If such an issue occurs, we just want to retry it and just do something else. +# If an exception like this ``ZeroDivisionError`` occurs, +# we do not want to retry the execution and just do something else. # To do this, # we need to define the fatal exceptions for which the execution is not retried. # It means that if that error is raised, -# then the discipline :class:`.RetryDiscipline` will stop execution, -# without retrying another attempt. +# then the discipline :class:`.RetryDiscipline` will stop execution +# rather than retrying the execution. retry_disc = RetryDiscipline( analytic_disc, n_trials=3, fatal_exceptions=[ZeroDivisionError] ) @@ -108,15 +107,16 @@ except ZeroDivisionError: LOGGER.info("Manage this fatal exception.") # %% -# We can verify the number of attempt is only :math:`1`: +# We can verify the number of attempts is only :math:`1`: retry_disc.n_executions # %% -# Trying the ``timeout`` option -# ----------------------------- -# If you want to limit the duration of the wrapped discipline, use the ``timeout`` -# option. Here is an example, by implementing a class with a ``time.sleep`` in the -# ``_run`` method: +# Limit the execution time +# ------------------------ +# If you want to limit the duration of the wrapped discipline, +# use the ``timeout`` option. +# Here is an example of a discipline +# whose execution does nothing except sleep for 5 seconds: class DisciplineLongTimeRunning(Discipline): @@ -128,12 +128,8 @@ class DisciplineLongTimeRunning(Discipline): # %% # Now we wrap it in :class:`.RetryDiscipline`, -# and set the ``timeout`` argument to 2 seconds. -# In the ``INFO line``, we can see the time the discipline started and ended its -# execution. -# -# The line ``LOGGER.info("Discipline completed, due to a TimeoutError.")`` should be -# executed as the timeout is reached. +# set the ``timeout`` argument to 2 seconds +# and execute this new discipline: retry_disc = RetryDiscipline(DisciplineLongTimeRunning(), n_trials=1, timeout=2.0) @@ -146,6 +142,10 @@ except TimeoutError: LOGGER.info("Discipline completed, due to a TimeoutError.") # %% +# In the log, +# we can see the initial and final times of the discipline execution. +# We can also read that the timeout is reached. +# # In some cases, # this option could be very useful. # For example if you wrap an SSH discipline diff --git a/src/gemseo/disciplines/wrappers/retry_discipline.py b/src/gemseo/disciplines/wrappers/retry_discipline.py index 40fa2ff159..94eb050b5c 100644 --- a/src/gemseo/disciplines/wrappers/retry_discipline.py +++ b/src/gemseo/disciplines/wrappers/retry_discipline.py @@ -41,7 +41,7 @@ class RetryDiscipline(Discipline): This :class:`.Discipline` wraps another discipline. - It can be executed multiple times (up to a specified number of retries) + It can be executed multiple times (up to a specified number of trials) if the previous attempts fail to produce any result. A timeout in seconds can be specified to prevent executions from becoming stuck. @@ -67,10 +67,10 @@ class RetryDiscipline(Discipline): """The possible timeout exceptions that can be raised during execution.""" n_trials: int - """The number of retry of the discipline.""" + """The number of trials to execute the discipline.""" wait_time: float - """The time to wait between 2 retries (in seconds).""" + """The time to wait between 2 trials (in seconds).""" timeout: float """The maximum duration, in seconds, that the discipline is allowed to run.""" @@ -91,7 +91,7 @@ class RetryDiscipline(Discipline): Args: discipline: The discipline to wrap in the retry loop. n_trials: The number of trials of the discipline. - wait_time: The time to wait between 2 retries (in seconds). + wait_time: The time to wait between 2 trials (in seconds). timeout: The maximum duration, in seconds, that the discipline is allowed to run. If this time limit is exceeded, the execution is terminated. If ``math.inf``, the -- GitLab From f6a9d87d2917a245ba2f07f01359716b0a5ada22 Mon Sep 17 00:00:00 2001 From: "jean-francois.figue" Date: Fri, 14 Feb 2025 10:39:03 +0100 Subject: [PATCH 10/11] style: removed trailing-whitespace --- .../disciplines/types/plot_retry_discipline.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc_src/_examples/disciplines/types/plot_retry_discipline.py b/doc_src/_examples/disciplines/types/plot_retry_discipline.py index 3eeddc654f..e34bcda098 100644 --- a/doc_src/_examples/disciplines/types/plot_retry_discipline.py +++ b/doc_src/_examples/disciplines/types/plot_retry_discipline.py @@ -55,7 +55,7 @@ analytic_disc = create_discipline("AnalyticDiscipline", expressions={"y": "1/x"} # %% # This discipline will raise a ``ZeroDivisionError`` when :math:`x=0`. -# +# # Execution without failure # ------------------------- # Let's wrap this toy discipline in a :class:`.RetryDiscipline` @@ -90,7 +90,7 @@ except Exception as err: # noqa: BLE001 LOGGER.info(retry_disc.n_executions) # %% -# If an exception like this ``ZeroDivisionError`` occurs, +# If an exception like this ``ZeroDivisionError`` occurs, # we do not want to retry the execution and just do something else. # To do this, # we need to define the fatal exceptions for which the execution is not retried. @@ -113,8 +113,8 @@ retry_disc.n_executions # %% # Limit the execution time # ------------------------ -# If you want to limit the duration of the wrapped discipline, -# use the ``timeout`` option. +# If you want to limit the duration of the wrapped discipline, +# use the ``timeout`` option. # Here is an example of a discipline # whose execution does nothing except sleep for 5 seconds: @@ -142,7 +142,7 @@ except TimeoutError: LOGGER.info("Discipline completed, due to a TimeoutError.") # %% -# In the log, +# In the log, # we can see the initial and final times of the discipline execution. # We can also read that the timeout is reached. # -- GitLab From dc1a5695d6bd10263d2a2e9797146a4bbfbe9e87 Mon Sep 17 00:00:00 2001 From: "jean-francois.figue" Date: Fri, 14 Feb 2025 16:22:08 +0100 Subject: [PATCH 11/11] fix: solve execution status of the discipline , and add new case in the demo for n_trials > 1 --- .../types/plot_retry_discipline.py | 71 ++++++++++++++----- .../disciplines/wrappers/retry_discipline.py | 3 + 2 files changed, 55 insertions(+), 19 deletions(-) diff --git a/doc_src/_examples/disciplines/types/plot_retry_discipline.py b/doc_src/_examples/disciplines/types/plot_retry_discipline.py index e34bcda098..d760a5d581 100644 --- a/doc_src/_examples/disciplines/types/plot_retry_discipline.py +++ b/doc_src/_examples/disciplines/types/plot_retry_discipline.py @@ -75,28 +75,13 @@ retry_disc.n_executions # %% # Execution with failure # ---------------------- -# Let's try now with :math:`x=0`, which will make the toy discipline crash. -# -# First, run the discipline with 3 attempts: - -retry_disc = RetryDiscipline(analytic_disc, n_trials=3) -try: - retry_disc.execute() -except Exception as err: # noqa: BLE001 - LOGGER.info(err) - -# %% -# We can verify, the calculation has been tried 3 times: -LOGGER.info(retry_disc.n_executions) - -# %% -# If an exception like this ``ZeroDivisionError`` occurs, +# If an exception like a ``ZeroDivisionError`` occurs, # we do not want to retry the execution and just do something else. # To do this, # we need to define the fatal exceptions for which the execution is not retried. # It means that if that error is raised, # then the discipline :class:`.RetryDiscipline` will stop execution -# rather than retrying the execution. +# rather than retrying an attempt. retry_disc = RetryDiscipline( analytic_disc, n_trials=3, fatal_exceptions=[ZeroDivisionError] ) @@ -110,6 +95,44 @@ except ZeroDivisionError: # We can verify the number of attempts is only :math:`1`: retry_disc.n_executions +# %% +# To highlight the use of ``n_trials`` parameter, let's try another toy discipline, +# which will crash the first 2 executions and finally succeed at the third attempt. + + +class FictiveDiscipline(Discipline): + """Discipline to be executed several times. + + - The first 2 times, raise a RuntimeError, + - and finally succeed. + """ + + def __init__(self) -> None: + super().__init__() + self.attempt = 0 + + def _run(self, input_data: StrKeyMapping) -> StrKeyMapping: + self.attempt += 1 + LOGGER.info("attempt: %s", self.attempt) + if self.attempt < 3: + raise RuntimeError + return {} + + +# %% +# We can then illustrate the use of ``n_trials`` parameter. Here we intentionally set +# this value to 4, knowing the discipline will complete before at the third trial: + + +test_n_trials = FictiveDiscipline() +retry_disc = RetryDiscipline(test_n_trials, n_trials=4) + +retry_disc.execute() + +# %% +# and verify the calculation has been tried 3 times to succeed: +retry_disc.n_executions + # %% # Limit the execution time # ------------------------ @@ -139,7 +162,7 @@ try: retry_disc.execute({}) LOGGER.info("Discipline completed without reaching the time limit.") except TimeoutError: - LOGGER.info("Discipline completed, due to a TimeoutError.") + LOGGER.info("Discipline stopped, due to a TimeoutError.") # %% # In the log, @@ -154,9 +177,19 @@ except TimeoutError: # In that context, # it can be important to limit the duration when an ssh connexion is too slow. # + +# %% +# .. note:: +# +# The user can build his :class:`.RetryDiscipline` with a combination of all the +# available parameters. +# Some attributes of the discipline are public and can be modified after +# instantiation (``fatal_exceptions``, ``n_trials``, ...) +# # .. note:: # -# Set ``sys.tracebacklimit = 0`` to limit message output by exception. In order the +# In the previous example, we added ``sys.tracebacklimit = 0`` to +# limit message output by exception, just in order the # output is only focused on what we aim to demonstrate with that example. # Please don't put this statement in normal use, otherwise you could miss some # important messages in the output. diff --git a/src/gemseo/disciplines/wrappers/retry_discipline.py b/src/gemseo/disciplines/wrappers/retry_discipline.py index 94eb050b5c..aff19faac1 100644 --- a/src/gemseo/disciplines/wrappers/retry_discipline.py +++ b/src/gemseo/disciplines/wrappers/retry_discipline.py @@ -27,6 +27,7 @@ from typing import TYPE_CHECKING from typing import ClassVar from gemseo.core.discipline import Discipline +from gemseo.core.execution_status import ExecutionStatus if TYPE_CHECKING: from collections.abc import Iterable @@ -157,6 +158,8 @@ class RetryDiscipline(Discipline): current_error = error time.sleep(self.wait_time) + self.__discipline.execution_status.value = ExecutionStatus.Status.DONE + plural_suffix = "s" if self.n_trials > 1 else "" LOGGER.error( "Failed to execute discipline %s after %d attempt%s.", -- GitLab