Skip to content

Commit 768ad61

Browse files
authored
Merge pull request #6210 from cylc/8.3.x-sync
🤖 Merge 8.3.x-sync into master
2 parents a191d82 + 29d26cf commit 768ad61

File tree

12 files changed

+212
-126
lines changed

12 files changed

+212
-126
lines changed

changes.d/6200.fix.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed bug where a stalled paused workflow would be incorrectly reported as running, not paused

changes.d/6206.fix.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixes the spawning of multiple parentless tasks off the same sequential wall-clock xtrigger.

cylc/flow/commands.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,16 +189,19 @@ async def stop(
189189
schd.workflow_db_mgr.put_workflow_stop_cycle_point(
190190
schd.options.stopcp
191191
)
192+
schd._update_workflow_state()
192193
elif clock_time is not None:
193194
# schedule shutdown after wallclock time passes provided time
194195
parser = TimePointParser()
195196
schd.set_stop_clock(
196197
int(parser.parse(clock_time).seconds_since_unix_epoch)
197198
)
199+
schd._update_workflow_state()
198200
elif task is not None:
199201
# schedule shutdown after task succeeds
200202
task_id = TaskID.get_standardised_taskid(task)
201203
schd.pool.set_stop_task(task_id)
204+
schd._update_workflow_state()
202205
else:
203206
# immediate shutdown
204207
with suppress(KeyError):
@@ -229,6 +232,7 @@ async def release_hold_point(schd: 'Scheduler'):
229232
yield
230233
LOG.info("Releasing all tasks and removing hold cycle point.")
231234
schd.pool.release_hold_point()
235+
schd._update_workflow_state()
232236

233237

234238
@_command('resume')
@@ -287,6 +291,7 @@ async def set_hold_point(schd: 'Scheduler', point: str):
287291
"All tasks after this point will be held."
288292
)
289293
schd.pool.set_hold_point(cycle_point)
294+
schd._update_workflow_state()
290295

291296

292297
@_command('pause')

cylc/flow/data_store_mgr.py

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,10 @@
8585
pdeepcopy,
8686
poverride
8787
)
88-
from cylc.flow.workflow_status import get_workflow_status
88+
from cylc.flow.workflow_status import (
89+
get_workflow_status,
90+
get_workflow_status_msg,
91+
)
8992
from cylc.flow.task_job_logs import JOB_LOG_OPTS, get_task_job_log
9093
from cylc.flow.task_proxy import TaskProxy
9194
from cylc.flow.task_state import (
@@ -789,8 +792,9 @@ def increment_graph_window(
789792
source_tokens,
790793
point,
791794
flow_nums,
792-
False,
793-
itask
795+
is_parent=False,
796+
itask=itask,
797+
replace_existing=True,
794798
)
795799

796800
# Pre-populate from previous walks
@@ -1150,24 +1154,27 @@ def generate_ghost_task(
11501154
is_parent: bool = False,
11511155
itask: Optional['TaskProxy'] = None,
11521156
n_depth: int = 0,
1157+
replace_existing: bool = False,
11531158
) -> None:
11541159
"""Create task-point element populated with static data.
11551160
11561161
Args:
11571162
source_tokens
11581163
point
11591164
flow_nums
1160-
is_parent:
1161-
Used to determine whether to load DB state.
1162-
itask:
1163-
Update task-node from corresponding task proxy object.
1165+
is_parent: Used to determine whether to load DB state.
1166+
itask: Update task-node from corresponding task proxy object.
11641167
n_depth: n-window graph edge distance.
1168+
replace_existing: Replace any existing data for task as it may
1169+
be out of date (e.g. flow nums).
11651170
"""
11661171
tp_id = tokens.id
11671172
if (
11681173
tp_id in self.data[self.workflow_id][TASK_PROXIES]
11691174
or tp_id in self.added[TASK_PROXIES]
11701175
):
1176+
if replace_existing and itask is not None:
1177+
self.delta_from_task_proxy(itask)
11711178
return
11721179

11731180
name = tokens['task']
@@ -2174,8 +2181,8 @@ def update_workflow(self, reloaded=False):
21742181
w_delta.latest_state_tasks[state].task_proxies[:] = tp_queue
21752182

21762183
# Set status & msg if changed.
2177-
status, status_msg = map(
2178-
str, get_workflow_status(self.schd))
2184+
status = get_workflow_status(self.schd).value
2185+
status_msg = get_workflow_status_msg(self.schd)
21792186
if w_data.status != status or w_data.status_msg != status_msg:
21802187
w_delta.status = status
21812188
w_delta.status_msg = status_msg
@@ -2522,6 +2529,26 @@ def delta_task_xtrigger(self, sig, satisfied):
25222529
xtrigger.time = update_time
25232530
self.updates_pending = True
25242531

2532+
def delta_from_task_proxy(self, itask: TaskProxy) -> None:
2533+
"""Create delta from existing pool task proxy.
2534+
2535+
Args:
2536+
itask (cylc.flow.task_proxy.TaskProxy):
2537+
Update task-node from corresponding task proxy
2538+
objects from the workflow task pool.
2539+
2540+
"""
2541+
tproxy: Optional[PbTaskProxy]
2542+
tp_id, tproxy = self.store_node_fetcher(itask.tokens)
2543+
if not tproxy:
2544+
return
2545+
update_time = time()
2546+
tp_delta = self.updated[TASK_PROXIES].setdefault(
2547+
tp_id, PbTaskProxy(id=tp_id))
2548+
tp_delta.stamp = f'{tp_id}@{update_time}'
2549+
self._process_internal_task_proxy(itask, tp_delta)
2550+
self.updates_pending = True
2551+
25252552
# -----------
25262553
# Job Deltas
25272554
# -----------

cylc/flow/scheduler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2000,7 +2000,7 @@ def update_data_store(self):
20002000
20012001
Call this method whenever the Scheduler's state has changed in a way
20022002
that requires a data store update.
2003-
See cylc.flow.workflow_status.get_workflow_status() for a
2003+
See cylc.flow.workflow_status.get_workflow_status_msg() for a
20042004
(non-exhaustive?) list of properties that if changed will require
20052005
this update.
20062006

cylc/flow/tui/util.py

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -384,19 +384,6 @@ def get_task_status_summary(flow):
384384
]
385385

386386

387-
def get_workflow_status_str(flow):
388-
"""Return a workflow status string for the header.
389-
390-
Arguments:
391-
flow (dict):
392-
GraphQL JSON response for this workflow.
393-
394-
Returns:
395-
list - Text list for the urwid.Text widget.
396-
397-
"""
398-
399-
400387
def _render_user(node, data):
401388
return f'~{ME}'
402389

cylc/flow/wallclock.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"""Wall clock related utilities."""
1717

1818
from calendar import timegm
19-
from datetime import datetime, timedelta
19+
from datetime import datetime, timedelta, timezone
2020

2121
from metomi.isodatetime.timezone import (
2222
get_local_time_zone_format, get_local_time_zone, TimeZoneFormatMode)
@@ -209,7 +209,7 @@ def get_time_string_from_unix_time(unix_time, display_sub_seconds=False,
209209
to use as the time zone designator.
210210
211211
"""
212-
date_time = datetime.utcfromtimestamp(unix_time)
212+
date_time = datetime.fromtimestamp(unix_time, timezone.utc)
213213
return get_time_string(date_time,
214214
display_sub_seconds=display_sub_seconds,
215215
use_basic_format=use_basic_format,

cylc/flow/workflow_status.py

Lines changed: 57 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,18 @@
1616
"""Workflow status constants."""
1717

1818
from enum import Enum
19-
from typing import Tuple, TYPE_CHECKING
19+
from typing import TYPE_CHECKING, Optional, Union
2020

21+
from cylc.flow.cycling.loader import get_point
22+
from cylc.flow.id import tokenise
2123
from cylc.flow.wallclock import get_time_string_from_unix_time as time2str
2224

2325
if TYPE_CHECKING:
2426
from optparse import Values
27+
28+
from cylc.flow.cycling import PointBase
2529
from cylc.flow.scheduler import Scheduler
30+
from cylc.flow.task_pool import TaskPool
2631

2732
# Keys for identify API call
2833
KEY_GROUP = "group"
@@ -143,62 +148,60 @@ class AutoRestartMode(Enum):
143148
"""Workflow will stop immediately but *not* attempt to restart."""
144149

145150

146-
def get_workflow_status(schd: 'Scheduler') -> Tuple[str, str]:
147-
"""Return the status of the provided workflow.
148-
149-
This should be a short, concise description of the workflow state.
150-
151-
Args:
152-
schd: The running workflow
153-
154-
Returns:
155-
tuple - (state, state_msg)
156-
157-
state:
158-
The WorkflowState.
159-
state_msg:
160-
Text describing the current state (may be an empty string).
151+
def get_workflow_status(schd: 'Scheduler') -> WorkflowStatus:
152+
"""Return the status of the provided workflow."""
153+
if schd.stop_mode is not None:
154+
return WorkflowStatus.STOPPING
155+
if schd.is_paused or schd.reload_pending:
156+
return WorkflowStatus.PAUSED
157+
return WorkflowStatus.RUNNING
161158

162-
"""
163-
status = WorkflowStatus.RUNNING
164-
status_msg = ''
165159

160+
def get_workflow_status_msg(schd: 'Scheduler') -> str:
161+
"""Return a short, concise status message for the provided workflow."""
166162
if schd.stop_mode is not None:
167-
status = WorkflowStatus.STOPPING
168-
status_msg = f'stopping: {schd.stop_mode.explain()}'
169-
elif schd.reload_pending:
170-
status = WorkflowStatus.PAUSED
171-
status_msg = f'reloading: {schd.reload_pending}'
172-
elif schd.is_stalled:
173-
status_msg = 'stalled'
174-
elif schd.is_paused:
175-
status = WorkflowStatus.PAUSED
176-
status_msg = 'paused'
177-
elif schd.pool.hold_point:
178-
status_msg = (
179-
WORKFLOW_STATUS_RUNNING_TO_HOLD %
180-
schd.pool.hold_point)
181-
elif schd.pool.stop_point:
182-
status_msg = (
183-
WORKFLOW_STATUS_RUNNING_TO_STOP %
184-
schd.pool.stop_point)
185-
elif schd.stop_clock_time is not None:
186-
status_msg = (
187-
WORKFLOW_STATUS_RUNNING_TO_STOP %
188-
time2str(schd.stop_clock_time))
189-
elif schd.pool.stop_task_id:
190-
status_msg = (
191-
WORKFLOW_STATUS_RUNNING_TO_STOP %
192-
schd.pool.stop_task_id)
193-
elif schd.config and schd.config.final_point:
194-
status_msg = (
195-
WORKFLOW_STATUS_RUNNING_TO_STOP %
196-
schd.config.final_point)
197-
else:
198-
# fallback - running indefinitely
199-
status_msg = 'running'
200-
201-
return (status.value, status_msg)
163+
return f'stopping: {schd.stop_mode.explain()}'
164+
if schd.reload_pending:
165+
return f'reloading: {schd.reload_pending}'
166+
if schd.is_stalled:
167+
if schd.is_paused:
168+
return 'stalled and paused'
169+
return 'stalled'
170+
if schd.is_paused:
171+
return 'paused'
172+
if schd.stop_clock_time is not None:
173+
return WORKFLOW_STATUS_RUNNING_TO_STOP % time2str(
174+
schd.stop_clock_time
175+
)
176+
stop_point_msg = _get_earliest_stop_point_status_msg(schd.pool)
177+
if stop_point_msg is not None:
178+
return stop_point_msg
179+
if schd.config and schd.config.final_point:
180+
return WORKFLOW_STATUS_RUNNING_TO_STOP % schd.config.final_point
181+
# fallback - running indefinitely
182+
return 'running'
183+
184+
185+
def _get_earliest_stop_point_status_msg(pool: 'TaskPool') -> Optional[str]:
186+
"""Return the status message for the earliest stop point in the pool,
187+
if any."""
188+
template = WORKFLOW_STATUS_RUNNING_TO_STOP
189+
prop: Union[PointBase, str, None] = pool.stop_task_id
190+
min_point: Optional[PointBase] = get_point(
191+
tokenise(pool.stop_task_id, relative=True)['cycle']
192+
if pool.stop_task_id else None
193+
)
194+
for point, tmpl in (
195+
(pool.stop_point, WORKFLOW_STATUS_RUNNING_TO_STOP),
196+
(pool.hold_point, WORKFLOW_STATUS_RUNNING_TO_HOLD)
197+
):
198+
if point is not None and (min_point is None or point < min_point):
199+
template = tmpl
200+
min_point = point
201+
prop = point
202+
if prop is None:
203+
return None
204+
return template % prop
202205

203206

204207
class RunMode:

cylc/flow/xtrigger_mgr.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,8 @@ def call_xtriggers_async(self, itask: 'TaskProxy'):
644644
if sig in self.sat_xtrig:
645645
# Already satisfied, just update the task
646646
itask.state.xtriggers[label] = True
647+
if self.all_task_seq_xtriggers_satisfied(itask):
648+
self.sequential_spawn_next.add(itask.identity)
647649
elif _wall_clock(*ctx.func_args, **ctx.func_kwargs):
648650
# Newly satisfied
649651
itask.state.xtriggers[label] = True

tests/functional/cylc-show/06-past-present-future.t

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ TEST_NAME="${TEST_NAME_BASE}-show.present"
4646
contains_ok "${WORKFLOW_RUN_DIR}/show-c.txt" <<__END__
4747
state: running
4848
prerequisites: ('⨯': not satisfied)
49-
1/b succeeded
49+
1/b succeeded
5050
__END__
5151

5252
TEST_NAME="${TEST_NAME_BASE}-show.future"

0 commit comments

Comments
 (0)