diff --git a/antarest/launcher/adapters/slurm_launcher/slurm_launcher.py b/antarest/launcher/adapters/slurm_launcher/slurm_launcher.py index d2ce3ad1ec..6aba6f21b3 100644 --- a/antarest/launcher/adapters/slurm_launcher/slurm_launcher.py +++ b/antarest/launcher/adapters/slurm_launcher/slurm_launcher.py @@ -513,22 +513,15 @@ def _apply_params(self, launcher_params: LauncherParametersDTO) -> argparse.Name ): other_options.append("xpansion_sensitivity") - time_limit = launcher_params.time_limit - if time_limit is not None: - if MIN_TIME_LIMIT > time_limit: - logger.warning( - f"Invalid slurm launcher time limit ({time_limit})," - f" should be higher than {MIN_TIME_LIMIT}. Using min limit." - ) - launcher_args.time_limit = MIN_TIME_LIMIT - elif time_limit >= MAX_TIME_LIMIT: - logger.warning( - f"Invalid slurm launcher time limit ({time_limit})," - f" should be lower than {MAX_TIME_LIMIT}. Using max limit." - ) - launcher_args.time_limit = MAX_TIME_LIMIT - 3600 - else: - launcher_args.time_limit = time_limit + # The `time_limit` parameter could be `None`, in that case, the default value is used. + time_limit = launcher_params.time_limit or MIN_TIME_LIMIT + time_limit = min(max(time_limit, MIN_TIME_LIMIT), MAX_TIME_LIMIT) + if launcher_args.time_limit != time_limit: + logger.warning( + f"Invalid slurm launcher time_limit ({time_limit})," + f" should be between {MIN_TIME_LIMIT} and {MAX_TIME_LIMIT}" + ) + launcher_args.time_limit = time_limit post_processing = launcher_params.post_processing if post_processing is not None: diff --git a/antarest/launcher/model.py b/antarest/launcher/model.py index a8f856269d..3bd3427a07 100644 --- a/antarest/launcher/model.py +++ b/antarest/launcher/model.py @@ -25,13 +25,14 @@ class LauncherParametersDTO(BaseModel): adequacy_patch: t.Optional[t.Dict[str, t.Any]] = None nb_cpu: t.Optional[int] = None post_processing: bool = False - time_limit: t.Optional[int] = None # 3600 ≤ time_limit < 864000 (10 days) + time_limit: int = 240 * 3600 # Default value set to 240 hours (in seconds) xpansion: t.Union[XpansionParametersDTO, bool, None] = None xpansion_r_version: bool = False archive_output: bool = True auto_unzip: bool = True output_suffix: t.Optional[str] = None other_options: t.Optional[str] = None + # add extensions field here @classmethod diff --git a/tests/launcher/test_slurm_launcher.py b/tests/launcher/test_slurm_launcher.py index a3a8cfe90b..e1c69f63d4 100644 --- a/tests/launcher/test_slurm_launcher.py +++ b/tests/launcher/test_slurm_launcher.py @@ -10,7 +10,6 @@ from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb from antareslauncher.main import MainParameters from antareslauncher.study_dto import StudyDTO -from sqlalchemy.orm import Session # type: ignore from antarest.core.config import Config, LauncherConfig, NbCoresConfig, SlurmConfig from antarest.launcher.adapters.abstractlauncher import LauncherInitException @@ -37,7 +36,7 @@ def launcher_config(tmp_path: Path) -> Config: "key_password": "password", "password": "password", "default_wait_time": 10, - "default_time_limit": 20, + "default_time_limit": MAX_TIME_LIMIT, "default_json_db_name": "antares.db", "slurm_script_path": "/path/to/slurm/launcher.sh", "partition": "fake_partition", @@ -203,11 +202,14 @@ def test_extra_parameters(launcher_config: Config) -> None: launcher_params = apply_params(LauncherParametersDTO(nb_cpu=999)) assert launcher_params.n_cpu == slurm_config.nb_cores.default # out of range + launcher_params = apply_params(LauncherParametersDTO.construct(time_limit=None)) + assert launcher_params.time_limit == MIN_TIME_LIMIT + launcher_params = apply_params(LauncherParametersDTO(time_limit=10)) assert launcher_params.time_limit == MIN_TIME_LIMIT launcher_params = apply_params(LauncherParametersDTO(time_limit=999999999)) - assert launcher_params.time_limit == MAX_TIME_LIMIT - 3600 + assert launcher_params.time_limit == MAX_TIME_LIMIT launcher_params = apply_params(LauncherParametersDTO(time_limit=99999)) assert launcher_params.time_limit == 99999 diff --git a/webapp/src/components/App/Studies/LauncherDialog.tsx b/webapp/src/components/App/Studies/LauncherDialog.tsx index 1419bfe6ac..7e610e2f88 100644 --- a/webapp/src/components/App/Studies/LauncherDialog.tsx +++ b/webapp/src/components/App/Studies/LauncherDialog.tsx @@ -38,8 +38,10 @@ import CheckBoxFE from "../../common/fieldEditors/CheckBoxFE"; import { convertVersions } from "../../../services/utils"; import UsePromiseCond from "../../common/utils/UsePromiseCond"; import SwitchFE from "../../common/fieldEditors/SwitchFE"; +import moment from "moment"; -const LAUNCH_LOAD_DEFAULT = 22; +const DEFAULT_NB_CPU = 22; +const DEFAULT_TIME_LIMIT = 240 * 3600; // 240 hours in seconds interface Props { open: boolean; @@ -53,8 +55,9 @@ function LauncherDialog(props: Props) { const { enqueueSnackbar } = useSnackbar(); const enqueueErrorSnackbar = useEnqueueErrorSnackbar(); const [options, setOptions] = useState({ - nb_cpu: LAUNCH_LOAD_DEFAULT, + nb_cpu: DEFAULT_NB_CPU, auto_unzip: true, + time_limit: DEFAULT_TIME_LIMIT, }); const [solverVersion, setSolverVersion] = useState(); const [isLaunching, setIsLaunching] = useState(false); @@ -170,12 +173,16 @@ function LauncherDialog(props: Props) { // Utils //////////////////////////////////////////////////////////////// - const timeLimitParse = (value: string): number => { - try { - return parseInt(value, 10) * 3600; - } catch { - return 48 * 3600; - } + /** + * Parses an hour value from a string and converts it to seconds. + * If the input is invalid, returns a default value. + * + * @param hourString - A string representing the number of hours. + * @returns The equivalent number of seconds, or a default value for invalid inputs. + */ + const parseHoursToSeconds = (hourString: string): number => { + const seconds = moment.duration(hourString, "hours").asSeconds(); + return seconds > 0 ? seconds : DEFAULT_TIME_LIMIT; }; //////////////////////////////////////////////////////////////// @@ -265,9 +272,10 @@ function LauncherDialog(props: Props) { label={t("study.timeLimit")} type="number" variant="filled" - value={(options.time_limit ?? 864000) / 3600} // 240 hours default + // Convert from seconds to hours the displayed value + value={(options.time_limit ?? DEFAULT_TIME_LIMIT) / 3600} onChange={(e) => - handleChange("time_limit", timeLimitParse(e.target.value)) + handleChange("time_limit", parseHoursToSeconds(e.target.value)) } InputLabelProps={{ shrink: true,