Skip to content

Commit acd4cce

Browse files
Merge pull request #6922 from cylc/8.5.x-sync
🤖 Merge 8.5.x-sync into master
2 parents 8af9942 + 07b2922 commit acd4cce

File tree

16 files changed

+402
-70
lines changed

16 files changed

+402
-70
lines changed

CHANGES.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ $ towncrier create <PR-number>.<break|feat|fix>.md --content "Short description"
1111

1212
<!-- towncrier release notes start -->
1313

14+
## __cylc-8.5.1 (Released 2025-08-08)__
15+
16+
### 🚀 Enhancements
17+
18+
[#6902](https://github.com/cylc/cylc-flow/pull/6902) - Invalid workflow events in the `mail events` or `handler events` configurations will result in warnings rather than errors.
19+
1420
## __cylc-8.5.0 (Released 2025-07-24)__
1521

1622
### 🚀 Enhancements

changes.d/6892.feat.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added `--format=json` option to `cylc broadcast` for use with the `--display` option. Deprecated the `--raw` option in favour of `--format=raw`.

cylc/flow/cfgspec/globalcfg.py

Lines changed: 66 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
)
6464
from cylc.flow.pathutil import SYMLINKABLE_LOCATIONS
6565
from cylc.flow.platforms import validate_platforms
66+
from cylc.flow.task_events_mgr import TaskEventsManager as TEM
6667
from cylc.flow.workflow_events import WorkflowEventHandler
6768

6869

@@ -171,9 +172,11 @@
171172
Configure :term:`event handlers` that run when certain workflow
172173
events occur.
173174
174-
This section configures *workflow* event handlers; see
175-
:cylc:conf:`flow.cylc[runtime][<namespace>][events]` for *task* event
176-
handlers.
175+
.. admonition:: Not to be confused with
176+
:class: tip
177+
178+
For *task* events, see
179+
:cylc:conf:`flow.cylc[runtime][<namespace>][events]`.
177180
178181
Event handlers can be held in the workflow ``bin/`` directory,
179182
otherwise it is up to you to ensure their location is in ``$PATH``
@@ -194,18 +197,24 @@
194197
195198
.. seealso::
196199
197-
:ref:`user_guide.scheduler.workflow_events`
200+
:ref:`user_guide.scheduler.workflow_events.list`
198201
''',
199202
'options': WorkflowEventHandler.EVENTS.copy(),
200203
'depr_options': WorkflowEventHandler.EVENTS_DEPRECATED.copy(),
204+
'warn_options': True,
201205
},
202206
'mail events': {
203207
'desc': '''
204208
Specify the workflow events for which notification emails should
205209
be sent.
210+
211+
.. seealso::
212+
213+
:ref:`user_guide.scheduler.workflow_events.list`
206214
''',
207215
'options': WorkflowEventHandler.EVENTS.copy(),
208216
'depr_options': WorkflowEventHandler.EVENTS_DEPRECATED.copy(),
217+
'warn_options': True,
209218
},
210219
'startup handlers': f'''
211220
Handlers to run at scheduler startup.
@@ -546,7 +555,10 @@
546555
TASK_EVENTS_DESCR = '''
547556
Configure the task event handling system.
548557
549-
See also :cylc:conf:`flow.cylc[scheduler][events]` for *workflow* events.
558+
.. admonition:: Not to be confused with
559+
:class: tip
560+
561+
For *workflow* events, see :cylc:conf:`flow.cylc[scheduler][events]`.
550562
551563
Task :term:`event handlers` are scripts to run when task events occur.
552564
@@ -576,26 +588,18 @@
576588
For more information, see
577589
:ref:`user_guide.runtime.task_event_handling`.
578590
579-
For workflow events, see
580-
:ref:`user_guide.scheduler.workflow_event_handling`.
581-
582591
Example::
583592
584593
echo %(event)s occurred in %(workflow)s >> my-log-file
585594
586595
''',
587-
'execution timeout': '''
588-
If a task has not finished after the specified interval, the execution
589-
timeout event handler(s) will be called.
590-
''',
591-
'handler events': '''
592-
A list of events for which :cylc:conf:`[..]handlers` are run.
596+
'handler events': f'''
597+
:Options: ``{"``, ``".join(TEM.STD_EVENTS)}`` & any custom event
593598
594-
Specify the events for which the general task event handlers
595-
:cylc:conf:`flow.cylc[runtime][<namespace>][events]handlers`
596-
should be invoked.
599+
A list of events for which :cylc:conf:`[..]handlers` are run.
597600
598-
See :ref:`user_guide.runtime.task_event_handling` for more information.
601+
See :ref:`user_guide.runtime.task_event_handling.list` for more
602+
information on task events.
599603
600604
Example::
601605
@@ -612,16 +616,25 @@
612616
613617
PT10S, PT1M, PT5M
614618
''',
615-
'mail events': '''
616-
Specify the events for which notification emails should be sent.
619+
'mail events': f'''
620+
:Options: ``{"``, ``".join(TEM.STD_EVENTS)}`` & any custom event
621+
622+
A list of events for which notification emails should be sent.
623+
624+
See :ref:`user_guide.runtime.task_event_handling.list` for more
625+
information on task events.
617626
618627
Example::
619628
620629
submission failed, failed
621630
''',
631+
'execution timeout': '''
632+
If a task has not finished after the specified interval, any configured
633+
execution timeout event handler(s) will be called.
634+
''',
622635
'submission timeout': '''
623-
If a task has not started after the specified interval, the submission
624-
timeout event handler(s) will be called.
636+
If a task has not started after the specified interval, any configured
637+
submission timeout event handler(s) will be called.
625638
'''
626639
}
627640

@@ -670,15 +683,36 @@ def comma_sep_section_note(version_changed: str = '') -> str:
670683

671684

672685
def short_descr(text: str) -> str:
673-
"""Get dedented one-paragraph description from long description."""
674-
return dedent(text).split('\n\n', 1)[0]
686+
r"""Get dedented one-paragraph description from long description.
687+
688+
Examples:
689+
>>> short_descr('foo\n\nbar')
690+
'foo'
691+
692+
>>> short_descr(':Field: Value\n\nfoo\n\nbar')
693+
':Field: Value\n\nfoo'
694+
695+
"""
696+
lines = []
697+
for line in dedent(text).splitlines():
698+
if not line:
699+
continue
700+
elif line.startswith(':'):
701+
lines.append(line)
702+
else:
703+
lines.append(line)
704+
break
705+
return '\n\n'.join(lines)
675706

676707

677708
def default_for(
678709
text: str, config_path: str, section: bool = False
679710
) -> str:
680-
"""Get dedented short description and insert a 'Default(s) For' directive
681-
that links to this config item's flow.cylc counterpart."""
711+
"""Return a ":Default For: field for this config.
712+
713+
Get dedented short description and insert a 'Default(s) For' field
714+
that links to this config item's flow.cylc counterpart.
715+
"""
682716
directive = f":Default{'s' if section else ''} For:"
683717
return (
684718
f"{directive} :cylc:conf:`flow.cylc{config_path}`.\n\n"
@@ -1998,12 +2032,6 @@ def default_for(
19982032
TASK_EVENTS_DESCR, "[runtime][<namespace>][events]", section=True
19992033
) + "\n\n" + ".. versionadded:: 8.0.0"
20002034
)):
2001-
Conf('execution timeout', VDR.V_INTERVAL, desc=(
2002-
default_for(
2003-
TASK_EVENTS_SETTINGS['execution timeout'],
2004-
"[runtime][<namespace>][events]execution timeout"
2005-
)
2006-
))
20072035
Conf('handlers', VDR.V_STRING_LIST, desc=(
20082036
default_for(
20092037
TASK_EVENTS_SETTINGS['handlers'],
@@ -2028,6 +2056,12 @@ def default_for(
20282056
"[runtime][<namespace>][events]mail events"
20292057
)
20302058
))
2059+
Conf('execution timeout', VDR.V_INTERVAL, desc=(
2060+
default_for(
2061+
TASK_EVENTS_SETTINGS['execution timeout'],
2062+
"[runtime][<namespace>][events]execution timeout"
2063+
)
2064+
))
20312065
Conf('submission timeout', VDR.V_INTERVAL, desc=(
20322066
default_for(
20332067
TASK_EVENTS_SETTINGS['submission timeout'],

cylc/flow/cfgspec/workflow.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1646,12 +1646,6 @@ def get_script_common_text(this: str, example: Optional[str] = None):
16461646
with Conf('events', desc=(
16471647
global_default(TASK_EVENTS_DESCR, "[task events]")
16481648
)):
1649-
Conf('execution timeout', VDR.V_INTERVAL, desc=(
1650-
global_default(
1651-
TASK_EVENTS_SETTINGS['execution timeout'],
1652-
"[task events]execution timeout"
1653-
)
1654-
))
16551649
Conf('handlers', VDR.V_STRING_LIST, None, desc=(
16561650
global_default(
16571651
TASK_EVENTS_SETTINGS['handlers'],
@@ -1676,6 +1670,12 @@ def get_script_common_text(this: str, example: Optional[str] = None):
16761670
"[task events]mail events"
16771671
)
16781672
))
1673+
Conf('execution timeout', VDR.V_INTERVAL, desc=(
1674+
global_default(
1675+
TASK_EVENTS_SETTINGS['execution timeout'],
1676+
"[task events]execution timeout"
1677+
)
1678+
))
16791679
Conf('submission timeout', VDR.V_INTERVAL, desc=(
16801680
global_default(
16811681
TASK_EVENTS_SETTINGS['submission timeout'],

cylc/flow/parsec/config.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,10 @@ class ConfigNode(ContextNode):
249249
depr_options:
250250
List of deprecated options. These are not displayed in the docs
251251
but are used for backwards compatibility.
252+
warn_options:
253+
If True, parsec will warn if invalid options are present rather
254+
than raising an exception. Any invalid options will be stripped
255+
from the config.
252256
default:
253257
The default value.
254258
desc:
@@ -279,6 +283,7 @@ class ConfigNode(ContextNode):
279283
'vdr',
280284
'options',
281285
'depr_options',
286+
'warn_options',
282287
'default',
283288
'desc',
284289
'display_name',
@@ -294,6 +299,7 @@ def __init__(
294299
desc: Optional[str] = None,
295300
meta: Optional['ConfigNode'] = None,
296301
depr_options: Optional[list] = None,
302+
warn_options: bool = False,
297303
):
298304
display_name = name
299305
if name.startswith('<'):
@@ -318,6 +324,7 @@ def __init__(
318324
self.default = default
319325
self.options = options or []
320326
self.depr_options = depr_options or []
327+
self.warn_options = warn_options
321328
self.desc = dedent(desc).strip() if desc else None
322329
self.meta = meta
323330

cylc/flow/parsec/validate.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from metomi.isodatetime.parsers import TimePointParser, DurationParser
3434
from metomi.isodatetime.exceptions import IsodatetimeError, ISO8601SyntaxError
3535

36+
from cylc.flow import LOG
3637
from cylc.flow.parsec.exceptions import (
3738
ListValueError, IllegalValueError, IllegalItemError)
3839
from cylc.flow.subprocctx import SubFuncContext
@@ -236,9 +237,19 @@ def validate(self, cfg_root, spec_root):
236237
str(i) for i in cfg[key] if i not in voptions
237238
]
238239
if bad:
239-
raise IllegalValueError(
240+
exc = IllegalValueError(
240241
'option', [*keys, key], ', '.join(bad)
241242
)
243+
if specval.warn_options:
244+
LOG.warning(
245+
f'{exc}'
246+
'\nInvalid items have been removed'
247+
)
248+
cfg[key] = [
249+
x for x in cfg[key] if x not in bad
250+
]
251+
else:
252+
raise exc
242253
elif cfg[key] not in voptions:
243254
raise IllegalValueError(
244255
'option', [*keys, key], cfg[key]

cylc/flow/remote.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ def watch_and_kill(proc):
8484
"""Kill proc if my PPID (etc.) changed - e.g. ssh connection dropped."""
8585
gpa = get_proc_ancestors()
8686
while True:
87-
sleep(0.5)
87+
sleep(60)
8888
if proc.poll() is not None:
8989
break
9090
if get_proc_ancestors() != gpa:

cylc/flow/scripts/broadcast.py

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@
111111
from optparse import Values
112112

113113

114+
RAW_DEPR_MSG = (
115+
"DEPRECATED: the --raw option will be removed at Cylc 8.7; "
116+
"use --format=raw instead."
117+
)
118+
114119
REC_ITEM = re.compile(r'^\[([^\]]*)\](.*)$')
115120

116121
MUTATION = '''
@@ -312,12 +317,33 @@ def get_option_parser() -> COP:
312317
help="Use unicode box characters with -d, -k.",
313318
action="store_true", default=False, dest="unicode")
314319

320+
# BACK COMPAT: --raw
321+
# From: < 8.5.1
322+
# To: 8.5.1
323+
# Remove at: 8.7.0
315324
parser.add_option(
316325
"-r", "--raw",
317-
help="With -d/--display or -k/--display-task, write out "
318-
"the broadcast config structure in raw Python form.",
326+
help=(
327+
"With -d/--display or -k/--display-task, write out "
328+
"the broadcast config structure in raw Python form. "
329+
f"{RAW_DEPR_MSG}"
330+
),
319331
action="store_true", default=False, dest="raw")
320332

333+
parser.add_option(
334+
'--format',
335+
help=(
336+
"With -d/--display or -k/--display-task, write out "
337+
"the broadcast config structure in one of the following formats: "
338+
"tree, json, or raw (like json but as a Python dictionary). "
339+
r"Default: %default."
340+
),
341+
action='store',
342+
dest='format',
343+
choices=('tree', 'json', 'raw'),
344+
default='tree',
345+
)
346+
321347
return parser
322348

323349

@@ -356,6 +382,9 @@ async def run(options: 'Values', workflow_id):
356382

357383
}
358384

385+
if options.raw:
386+
ret['stderr'].append(cparse(f"<yellow>{RAW_DEPR_MSG}</yellow>"))
387+
359388
if options.show or options.showtask:
360389
if options.showtask:
361390
try:
@@ -369,7 +398,12 @@ async def run(options: 'Values', workflow_id):
369398
for wflow in result['workflows']:
370399
settings = wflow['broadcasts']
371400
padding = get_padding(settings) * ' '
372-
if options.raw:
401+
if options.format == 'json':
402+
import json
403+
ret['stdout'].append(
404+
json.dumps(settings, indent=2, sort_keys=True)
405+
)
406+
elif options.raw or options.format == 'raw':
373407
ret['stdout'].append(str(settings))
374408
else:
375409
ret['stdout'].extend(

cylc/flow/task_pool.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -708,14 +708,12 @@ def load_db_task_action_timers(self, row_idx: int, row: Iterable) -> None:
708708
if ctx_key == "poll_timer":
709709
itask = self._get_task_by_id(id_)
710710
if itask is None:
711-
LOG.warning("%(id)s: task not found, skip" % {"id": id_})
712711
return
713712
itask.poll_timer = TaskActionTimer(
714713
ctx, delays, num, delay, timeout)
715714
elif ctx_key[0] == "try_timers":
716715
itask = self._get_task_by_id(id_)
717716
if itask is None:
718-
LOG.warning("%(id)s: task not found, skip" % {"id": id_})
719717
return
720718
if 'retrying' in ctx_key[1]:
721719
if 'submit' in ctx_key[1]:

0 commit comments

Comments
 (0)