Skip to content

Commit 54c7052

Browse files
committed
feat: support omit_in_job attribute on submit args to drop args invalid inside a job
Schedulers like flux give each job an ephemeral nested instance where some submit args (e.g. -p <partition>) are invalid, so args marked omit_in_job="true" are dropped when submission happens from inside an already-running batch job, detected via well-known scheduler environment variables. Closes #5018
1 parent 2c4d0c7 commit 54c7052

3 files changed

Lines changed: 154 additions & 8 deletions

File tree

CIME/XML/env_batch.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,22 @@
2424

2525
logger = logging.getLogger(__name__)
2626

27+
# Static mapping of batch system type to the well-known environment
28+
# variable that indicates the current process is running inside an
29+
# active job for that scheduler. Batch schedulers change infrequently
30+
# so this is maintained in code rather than in per-machine config.
31+
IN_JOB_ENVIRONMENT_VARIABLES = {
32+
"flux": "FLUX_JOB_ID",
33+
"lsf": "LSB_JOBID",
34+
"pbs": "PBS_JOBID",
35+
"pbspro": "PBS_JOBID",
36+
"moab": "PBS_JOBID",
37+
"slurm": "SLURM_JOB_ID",
38+
"slurm_single_node": "SLURM_JOB_ID",
39+
"cobalt": "COBALT_JOBID",
40+
"cobalt_theta": "COBALT_JOBID",
41+
}
42+
2743
# pragma pylint: disable=attribute-defined-outside-init
2844

2945

@@ -704,7 +720,37 @@ def _process_args(self, case, submit_arg_nodes, job, resolve=True):
704720

705721
return submitargs
706722

723+
def is_in_batch_job(self, environ=None):
724+
"""Checks whether the current process is running inside a batch job.
725+
726+
Detection is based on the presence of the scheduler specific
727+
environment variable for the case's batch system, e.g.
728+
``FLUX_JOB_ID`` for flux or ``SLURM_JOB_ID`` for slurm. This is
729+
used to drop submit args marked ``omit_in_job`` which are only
730+
valid when submitting from outside a job, e.g. flux nested
731+
instances define no partitions so ``-p`` must be omitted when
732+
resubmitting from inside a job.
733+
734+
Args:
735+
environ (dict, optional): Environment mapping to check,
736+
defaults to ``os.environ``.
737+
738+
Returns:
739+
bool: True if inside an active batch job, otherwise False.
740+
"""
741+
if environ is None:
742+
environ = os.environ
743+
744+
env_var = IN_JOB_ENVIRONMENT_VARIABLES.get(self._batchtype)
745+
746+
return env_var is not None and env_var in environ
747+
707748
def _get_argument(self, case, arg):
749+
omit_in_job = self.get(arg, "omit_in_job", default="false")
750+
751+
if omit_in_job.lower() in ("true", "1") and self.is_in_batch_job():
752+
raise ValueError()
753+
708754
flag = self.get(arg, "flag")
709755

710756
name = self.get(arg, "name")

CIME/data/config/xml_schemas/config_batch.xsd

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,11 +132,13 @@
132132
<xs:complexType>
133133
<xs:attribute name="flag" use="required"/>
134134
<xs:attribute name="name"/>
135+
<xs:attribute name="omit_in_job" type="xs:boolean"/>
135136
</xs:complexType>
136137
</xs:element>
137138
<xs:element name="argument" maxOccurs="unbounded">
138139
<xs:complexType mixed="true">
139140
<xs:attribute name="job_queue"/>
141+
<xs:attribute name="omit_in_job" type="xs:boolean"/>
140142
</xs:complexType>
141143
</xs:element>
142144
</xs:choice>

CIME/tests/test_unit_xml_env_batch.py

Lines changed: 106 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -514,8 +514,7 @@ def test_get_job_deps(self):
514514

515515
def test_get_submit_args_job_queue(self):
516516
with tempfile.NamedTemporaryFile() as tfile:
517-
tfile.write(
518-
b"""<?xml version="1.0"?>
517+
tfile.write(b"""<?xml version="1.0"?>
519518
<file id="env_batch.xml" version="2.0">
520519
<header>
521520
These variables may be changed anytime during a run, they
@@ -548,8 +547,7 @@ def test_get_submit_args_job_queue(self):
548547
</queues>
549548
</batch_system>
550549
</file>
551-
"""
552-
)
550+
""")
553551

554552
tfile.seek(0)
555553

@@ -571,8 +569,7 @@ def test_get_submit_args_job_queue(self):
571569
@mock.patch.dict(os.environ, {"TEST": "GOOD"})
572570
def test_get_submit_args(self):
573571
with tempfile.NamedTemporaryFile() as tfile:
574-
tfile.write(
575-
b"""<?xml version="1.0"?>
572+
tfile.write(b"""<?xml version="1.0"?>
576573
<file id="env_batch.xml" version="2.0">
577574
<header>
578575
These variables may be changed anytime during a run, they
@@ -629,8 +626,7 @@ def test_get_submit_args(self):
629626
</queues>
630627
</batch_system>
631628
</file>
632-
"""
633-
)
629+
""")
634630

635631
tfile.seek(0)
636632

@@ -1302,5 +1298,107 @@ def run_get_job_overrides(
13021298
return overrides
13031299

13041300

1301+
XML_OMIT_IN_JOB = b"""<?xml version="1.0"?>
1302+
<file id="env_batch.xml" version="2.0">
1303+
<header>
1304+
These variables may be changed anytime during a run, they
1305+
control arguments to the batch submit command.
1306+
</header>
1307+
<group id="config_batch">
1308+
<entry id="BATCH_SYSTEM" value="flux">
1309+
<type>char</type>
1310+
<valid_values>flux,slurm,pbs,lsf,none</valid_values>
1311+
<desc>The batch system type to use for this machine.</desc>
1312+
</entry>
1313+
</group>
1314+
<batch_system type="flux">
1315+
<submit_args>
1316+
<arg flag="--fixed" name="$PROJECT" omit_in_job="true"/>
1317+
</submit_args>
1318+
</batch_system>
1319+
<batch_system MACH="docker" type="flux">
1320+
<submit_args>
1321+
<argument>-o exit-timeout=none</argument>
1322+
<argument omit_in_job="true">-p pbatch</argument>
1323+
</submit_args>
1324+
</batch_system>
1325+
</file>
1326+
"""
1327+
1328+
1329+
def _create_omit_in_job_batch(tmp_path):
1330+
infile = tmp_path / "env_batch.xml"
1331+
1332+
infile.write_bytes(XML_OMIT_IN_JOB)
1333+
1334+
batch = EnvBatch(infile=str(infile))
1335+
1336+
case = mock.MagicMock()
1337+
1338+
case.get_value.side_effect = lambda *args, **kwargs: {
1339+
"BATCH_SPEC_FILE": str(infile),
1340+
"PROJECT": "CIME",
1341+
"JOB_QUEUE": "pbatch",
1342+
}.get(args[0])
1343+
1344+
case.get_resolved_value.side_effect = lambda val: val
1345+
1346+
return batch, case
1347+
1348+
1349+
def test_get_submit_args_omit_in_job_not_in_job(tmp_path, monkeypatch):
1350+
# Context
1351+
batch, case = _create_omit_in_job_batch(tmp_path)
1352+
1353+
monkeypatch.delenv("FLUX_JOB_ID", raising=False)
1354+
1355+
# Act
1356+
submit_args = batch.get_submit_args(case, ".case.run")
1357+
1358+
# Assert
1359+
assert submit_args == " --fixed CIME -o exit-timeout=none -p pbatch"
1360+
1361+
1362+
def test_get_submit_args_omit_in_job_in_job(tmp_path, monkeypatch):
1363+
# Context
1364+
batch, case = _create_omit_in_job_batch(tmp_path)
1365+
1366+
monkeypatch.setenv("FLUX_JOB_ID", "fuzzybunny")
1367+
1368+
# Act
1369+
submit_args = batch.get_submit_args(case, ".case.run")
1370+
1371+
# Assert
1372+
assert submit_args == " -o exit-timeout=none"
1373+
1374+
1375+
def test_get_submit_args_omit_in_job_other_scheduler_env(tmp_path, monkeypatch):
1376+
# Context
1377+
batch, case = _create_omit_in_job_batch(tmp_path)
1378+
1379+
monkeypatch.delenv("FLUX_JOB_ID", raising=False)
1380+
1381+
# Only the current batch system's env var is considered
1382+
monkeypatch.setenv("SLURM_JOB_ID", "1234")
1383+
1384+
# Act
1385+
submit_args = batch.get_submit_args(case, ".case.run")
1386+
1387+
# Assert
1388+
assert submit_args == " --fixed CIME -o exit-timeout=none -p pbatch"
1389+
1390+
1391+
def test_is_in_batch_job_unknown_batch_system(monkeypatch):
1392+
# Context
1393+
batch = EnvBatch()
1394+
1395+
batch._batchtype = "made_up_scheduler"
1396+
1397+
monkeypatch.setenv("SLURM_JOB_ID", "1234")
1398+
1399+
# Act/Assert
1400+
assert not batch.is_in_batch_job()
1401+
1402+
13051403
if __name__ == "__main__":
13061404
unittest.main()

0 commit comments

Comments
 (0)