Skip to content

Commit 5b19013

Browse files
authored
Merge pull request #6217 from cylc/8.3.x-sync
🤖 Merge 8.3.x-sync into master
2 parents e28e0c0 + 55872e4 commit 5b19013

File tree

9 files changed

+129
-69
lines changed

9 files changed

+129
-69
lines changed

CHANGES.md

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

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

14+
## __cylc-8.3.2 (Released 2024-07-10)__
15+
16+
### 🔧 Fixes
17+
18+
[#6178](https://github.com/cylc/cylc-flow/pull/6178) - Fix an issue where Tui could hang when closing.
19+
20+
[#6186](https://github.com/cylc/cylc-flow/pull/6186) - Fixed bug where using flow numbers with `cylc set` would not work correctly.
21+
22+
[#6200](https://github.com/cylc/cylc-flow/pull/6200) - Fixed bug where a stalled paused workflow would be incorrectly reported as running, not paused
23+
24+
[#6206](https://github.com/cylc/cylc-flow/pull/6206) - Fixes the spawning of multiple parentless tasks off the same sequential wall-clock xtrigger.
25+
1426
## __cylc-8.3.1 (Released 2024-07-04)__
1527

1628
### 🔧 Fixes

changes.d/6200.fix.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

changes.d/6206.fix.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

changes.d/fix.6178.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

cylc/flow/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,8 @@
1515
# along with this program. If not, see <http://www.gnu.org/licenses/>.
1616
"""Set up the cylc environment."""
1717

18-
import os
1918
import logging
20-
19+
import os
2120

2221
CYLC_LOG = 'cylc'
2322

cylc/flow/scripts/set.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@
9090

9191
from functools import partial
9292
import sys
93-
from typing import Tuple, TYPE_CHECKING
93+
from typing import Iterable, TYPE_CHECKING
9494

9595
from cylc.flow.exceptions import InputError
9696
from cylc.flow.network.client_factory import get_client
@@ -177,7 +177,7 @@ def get_option_parser() -> COP:
177177
return parser
178178

179179

180-
def validate_tokens(tokens_list: Tuple['Tokens']) -> None:
180+
def validate_tokens(tokens_list: Iterable['Tokens']) -> None:
181181
"""Check the cycles/tasks provided.
182182
183183
This checks that cycle/task selectors have not been provided in the IDs.
@@ -214,7 +214,7 @@ def validate_tokens(tokens_list: Tuple['Tokens']) -> None:
214214
async def run(
215215
options: 'Values',
216216
workflow_id: str,
217-
*tokens_list
217+
*tokens_list: 'Tokens'
218218
):
219219
validate_tokens(tokens_list)
220220

cylc/flow/task_pool.py

Lines changed: 51 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1578,50 +1578,42 @@ def can_be_spawned(self, name: str, point: 'PointBase') -> bool:
15781578

15791579
def _get_task_history(
15801580
self, name: str, point: 'PointBase', flow_nums: Set[int]
1581-
) -> Tuple[bool, int, str, bool]:
1582-
"""Get history of previous submits for this task.
1581+
) -> Tuple[int, Optional[str], bool]:
1582+
"""Get submit_num, status, flow_wait for point/name in flow_nums.
15831583
15841584
Args:
15851585
name: task name
15861586
point: task cycle point
15871587
flow_nums: task flow numbers
15881588
15891589
Returns:
1590-
never_spawned: if task never spawned before
1591-
submit_num: submit number of previous submit
1592-
prev_status: task status of previous sumbit
1593-
prev_flow_wait: if previous submit was a flow-wait task
1590+
(submit_num, status, flow_wait)
1591+
If no matching history, status will be None
15941592
15951593
"""
1594+
submit_num: int = 0
1595+
status: Optional[str] = None
1596+
flow_wait = False
1597+
15961598
info = self.workflow_db_mgr.pri_dao.select_prev_instances(
15971599
name, str(point)
15981600
)
1599-
try:
1600-
submit_num: int = max(s[0] for s in info)
1601-
except ValueError:
1602-
# never spawned in any flow
1603-
submit_num = 0
1604-
never_spawned = True
1605-
else:
1606-
never_spawned = False
1607-
# (submit_num could still be zero, if removed before submit)
1608-
1609-
prev_status: str = TASK_STATUS_WAITING
1610-
prev_flow_wait = False
1601+
with suppress(ValueError):
1602+
submit_num = max(s[0] for s in info)
16111603

1612-
for _snum, f_wait, old_fnums, status in info:
1604+
for _snum, f_wait, old_fnums, old_status in info:
16131605
if set.intersection(flow_nums, old_fnums):
16141606
# matching flows
1615-
prev_status = status
1616-
prev_flow_wait = f_wait
1617-
if prev_status in TASK_STATUSES_FINAL:
1607+
status = old_status
1608+
flow_wait = f_wait
1609+
if status in TASK_STATUSES_FINAL:
16181610
# task finished
16191611
break
16201612
# Else continue: there may be multiple entries with flow
16211613
# overlap due to merges (they'll have have same snum and
16221614
# f_wait); keep going to find the finished one, if any.
16231615

1624-
return never_spawned, submit_num, prev_status, prev_flow_wait
1616+
return submit_num, status, flow_wait
16251617

16261618
def _load_historical_outputs(self, itask: 'TaskProxy') -> None:
16271619
"""Load a task's historical outputs from the DB."""
@@ -1631,8 +1623,11 @@ def _load_historical_outputs(self, itask: 'TaskProxy') -> None:
16311623
# task never ran before
16321624
self.db_add_new_flow_rows(itask)
16331625
else:
1626+
flow_seen = False
16341627
for outputs_str, fnums in info.items():
16351628
if itask.flow_nums.intersection(fnums):
1629+
# DB row has overlap with itask's flows
1630+
flow_seen = True
16361631
# BACK COMPAT: In Cylc >8.0.0,<8.3.0, only the task
16371632
# messages were stored in the DB as a list.
16381633
# from: 8.0.0
@@ -1649,6 +1644,9 @@ def _load_historical_outputs(self, itask: 'TaskProxy') -> None:
16491644
# [message] - always the full task message
16501645
for msg in outputs:
16511646
itask.state.outputs.set_message_complete(msg)
1647+
if not flow_seen:
1648+
# itask never ran before in its assigned flows
1649+
self.db_add_new_flow_rows(itask)
16521650

16531651
def spawn_task(
16541652
self,
@@ -1658,44 +1656,52 @@ def spawn_task(
16581656
force: bool = False,
16591657
flow_wait: bool = False,
16601658
) -> Optional[TaskProxy]:
1661-
"""Return task proxy if not completed in this flow, or if forced.
1659+
"""Return a new task proxy for the given flow if possible.
16621660
1663-
If finished previously with flow wait, just try to spawn children.
1661+
We need to hit the DB for:
1662+
- submit number
1663+
- task status
1664+
- flow-wait
1665+
- completed outputs (e.g. via "cylc set")
16641666
1665-
Note finished tasks may be incomplete, but we don't automatically
1666-
re-run incomplete tasks in the same flow.
1667+
If history records a final task status (for this flow):
1668+
- if not flow wait, don't spawn (return None)
1669+
- if flow wait, don't spawn (return None) but do spawn children
1670+
- if outputs are incomplete, don't auto-rerun it (return None)
16671671
1668-
For every task spawned, we need a DB lookup for submit number,
1669-
and flow-wait.
1672+
Otherwise, spawn the task and load any completed outputs.
16701673
16711674
"""
1672-
if not self.can_be_spawned(name, point):
1673-
return None
1674-
1675-
never_spawned, submit_num, prev_status, prev_flow_wait = (
1675+
submit_num, prev_status, prev_flow_wait = (
16761676
self._get_task_history(name, point, flow_nums)
16771677
)
16781678

1679-
if (
1680-
not never_spawned and
1681-
not prev_flow_wait and
1682-
submit_num == 0
1683-
):
1684-
# Previous instance removed before completing any outputs.
1685-
LOG.debug(f"Not spawning {point}/{name} - task removed")
1686-
return None
1687-
1679+
# Create the task proxy with any completed outputs loaded.
16881680
itask = self._get_task_proxy_db_outputs(
16891681
point,
16901682
self.config.get_taskdef(name),
16911683
flow_nums,
1692-
status=prev_status,
1684+
status=prev_status or TASK_STATUS_WAITING,
16931685
submit_num=submit_num,
16941686
flow_wait=flow_wait,
16951687
)
16961688
if itask is None:
16971689
return None
16981690

1691+
if (
1692+
prev_status is not None
1693+
and not itask.state.outputs.get_completed_outputs()
1694+
):
1695+
# If itask has any history in this flow but no completed outputs
1696+
# we can infer it was deliberately removed, so don't respawn it.
1697+
# TODO (follow-up work):
1698+
# - this logic fails if task removed after some outputs completed
1699+
# - this is does not conform to future "cylc remove" flow-erasure
1700+
# behaviour which would result in respawning of the removed task
1701+
# See github.com/cylc/cylc-flow/pull/6186/#discussion_r1669727292
1702+
LOG.debug(f"Not respawning {point}/{name} - task was removed")
1703+
return None
1704+
16991705
if prev_status in TASK_STATUSES_FINAL:
17001706
# Task finished previously.
17011707
msg = f"[{point}/{name}:{prev_status}] already finished"
@@ -1878,7 +1884,6 @@ def set_prereqs_and_outputs(
18781884
- future tasks must be specified individually
18791885
- family names are not expanded to members
18801886
1881-
18821887
Uses a transient task proxy to spawn children. (Even if parent was
18831888
previously spawned in this flow its children might not have been).
18841889
@@ -1963,6 +1968,7 @@ def _set_outputs_itask(
19631968
self.data_store_mgr.delta_task_outputs(itask)
19641969
self.workflow_db_mgr.put_update_task_state(itask)
19651970
self.workflow_db_mgr.put_update_task_outputs(itask)
1971+
self.workflow_db_mgr.process_queued_ops()
19661972

19671973
def _set_prereqs_itask(
19681974
self,
@@ -2168,10 +2174,9 @@ def force_trigger_tasks(
21682174
if not self.can_be_spawned(name, point):
21692175
continue
21702176

2171-
_, submit_num, _prev_status, prev_fwait = (
2177+
submit_num, _, prev_fwait = (
21722178
self._get_task_history(name, point, flow_nums)
21732179
)
2174-
21752180
itask = TaskProxy(
21762181
self.tokens,
21772182
self.config.get_taskdef(name),

tests/integration/test_dbstatecheck.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,7 @@
1616

1717
"""Tests for the backend method of workflow_state"""
1818

19-
20-
from asyncio import sleep
2119
import pytest
22-
from textwrap import dedent
2320

2421
from cylc.flow.dbstatecheck import CylcWorkflowDBChecker
2522
from cylc.flow.scheduler import Scheduler
@@ -36,13 +33,15 @@ async def checker(
3633
"""
3734
wid = mod_flow({
3835
'scheduling': {
39-
'graph': {'P1Y': dedent('''
40-
good:succeeded
41-
bad:failed?
42-
output:custom_output
43-
''')},
4436
'initial cycle point': '1000',
45-
'final cycle point': '1001'
37+
'final cycle point': '1001',
38+
'graph': {
39+
'P1Y': '''
40+
good:succeeded
41+
bad:failed?
42+
output:custom_output
43+
'''
44+
},
4645
},
4746
'runtime': {
4847
'bad': {'simulation': {'fail cycle points': '1000'}},
@@ -51,11 +50,17 @@ async def checker(
5150
})
5251
schd: Scheduler = mod_scheduler(wid, paused_start=False)
5352
async with mod_run(schd):
53+
# allow a cycle of the main loop to pass so that flow 2 can be
54+
# added to db
5455
await mod_complete(schd)
56+
57+
# trigger a new task in flow 2
5558
schd.pool.force_trigger_tasks(['1000/good'], ['2'])
56-
# Allow a cycle of the main loop to pass so that flow 2 can be
57-
# added to db
58-
await sleep(1)
59+
60+
# update the database
61+
schd.process_workflow_db_queue()
62+
63+
# yield a DB checker
5964
with CylcWorkflowDBChecker(
6065
'somestring', 'utterbunkum', schd.workflow_db_mgr.pub_path
6166
) as _checker:
@@ -73,7 +78,7 @@ def test_basic(checker):
7378
['output', '10000101T0000Z', 'succeeded'],
7479
['output', '10010101T0000Z', 'succeeded'],
7580
['good', '10000101T0000Z', 'waiting', '(flows=2)'],
76-
]
81+
['good', '10010101T0000Z', 'waiting', '(flows=2)'], ]
7782
assert result == expect
7883

7984

@@ -131,5 +136,6 @@ def test_flownum(checker):
131136
result = checker.workflow_state_query(flow_num=2)
132137
expect = [
133138
['good', '10000101T0000Z', 'waiting', '(flows=2)'],
139+
['good', '10010101T0000Z', 'waiting', '(flows=2)'],
134140
]
135141
assert result == expect

tests/integration/test_task_pool.py

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1893,7 +1893,7 @@ async def test_fast_respawn(
18931893
# attempt to spawn it again
18941894
itask = task_pool.spawn_task("foo", IntegerPoint("1"), {1})
18951895
assert itask is None
1896-
assert "Not spawning 1/foo - task removed" in caplog.text
1896+
assert "Not respawning 1/foo - task was removed" in caplog.text
18971897

18981898

18991899
async def test_remove_active_task(
@@ -2019,9 +2019,50 @@ async def test_remove_no_respawn(flow, scheduler, start, log_filter):
20192019
# respawned as a result
20202020
schd.pool.spawn_on_output(b1, TASK_OUTPUT_SUCCEEDED)
20212021
assert log_filter(
2022-
log, contains='Not spawning 1/z - task removed'
2022+
log, contains='Not respawning 1/z - task was removed'
20232023
)
20242024
z1 = schd.pool.get_task(IntegerPoint("1"), "z")
20252025
assert (
20262026
z1 is None
20272027
), '1/z should have stayed removed (but has been added back into the pool'
2028+
2029+
2030+
async def test_set_future_flow(flow, scheduler, start, log_filter):
2031+
"""Manually-set outputs for new flow num must be recorded in the DB.
2032+
2033+
See https://github.com/cylc/cylc-flow/pull/6186
2034+
2035+
To trigger the bug, the flow must be new but the task must have been
2036+
spawned before in an earlier flow.
2037+
2038+
"""
2039+
# Scenario: after flow 1, set c1:succeeded in a future flow so
2040+
# when b succeeds in the new flow it will spawn c2 but not c1.
2041+
id_ = flow({
2042+
'scheduler': {
2043+
'allow implicit tasks': True
2044+
},
2045+
'scheduling': {
2046+
'cycling mode': 'integer',
2047+
'graph': {
2048+
'R1': 'b => c1 & c2',
2049+
},
2050+
},
2051+
})
2052+
schd: 'Scheduler' = scheduler(id_)
2053+
async with start(schd, level=logging.DEBUG) as log:
2054+
2055+
assert schd.pool.get_task(IntegerPoint("1"), "b") is not None, '1/b should be spawned on startup'
2056+
2057+
# set b, c1, c2 succeeded in flow 1
2058+
schd.pool.set_prereqs_and_outputs(['1/b', '1/c1', '1/c2'], prereqs=[], outputs=[], flow=[1])
2059+
schd.workflow_db_mgr.process_queued_ops()
2060+
2061+
# set task c1:succeeded in flow 2
2062+
schd.pool.set_prereqs_and_outputs(['1/c1'], prereqs=[], outputs=[], flow=[2])
2063+
schd.workflow_db_mgr.process_queued_ops()
2064+
2065+
# set b:succeeded in flow 2 and check downstream spawning
2066+
schd.pool.set_prereqs_and_outputs(['1/b'], prereqs=[], outputs=[], flow=[2])
2067+
assert schd.pool.get_task(IntegerPoint("1"), "c1") is None, '1/c1 (flow 2) should not be spawned after 1/b:succeeded'
2068+
assert schd.pool.get_task(IntegerPoint("1"), "c2") is not None, '1/c2 (flow 2) should be spawned after 1/b:succeeded'

0 commit comments

Comments
 (0)