Skip to content

Commit e8ca0c2

Browse files
committed
Allow broadcasts to modify sim mode tasks.
1 parent 8606f93 commit e8ca0c2

File tree

7 files changed

+188
-31
lines changed

7 files changed

+188
-31
lines changed

changes.d/5721.feat.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Allow users to broadcast run_mode to tasks.

cylc/flow/scheduler.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -1740,11 +1740,13 @@ async def main_loop(self) -> None:
17401740

17411741
self.pool.set_expired_tasks()
17421742
self.release_queued_tasks()
1743-
17441743
if (
17451744
self.pool.config.run_mode('simulation')
17461745
and sim_time_check(
1747-
self.message_queue, self.pool.get_tasks())
1746+
self.message_queue,
1747+
self.pool.get_tasks(),
1748+
self.broadcast_mgr
1749+
)
17481750
):
17491751
# A simulated task state change occurred.
17501752
self.reset_inactivity_timer()

cylc/flow/simulation.py

+119-27
Original file line numberDiff line numberDiff line change
@@ -37,59 +37,86 @@
3737
from cylc.flow.task_proxy import TaskProxy
3838

3939

40-
def configure_sim_modes(taskdefs, sim_mode):
41-
"""Adjust task defs for simulation and dummy mode.
40+
# Exotic: Recursive Type hint.
41+
NestedDict = Dict[str, Union['NestedDict', Any]]
42+
4243

44+
def configure_sim_modes(taskdefs, sim_mode):
45+
"""Adjust task definitions for simulation and dummy modes.
4346
"""
4447
dummy_mode = bool(sim_mode == 'dummy')
4548

4649
for tdef in taskdefs:
4750
# Compute simulated run time by scaling the execution limit.
48-
rtc = tdef.rtconfig
49-
sleep_sec = get_simulated_run_len(rtc)
51+
configure_rtc_sim_mode(tdef.rtconfig, dummy_mode)
5052

51-
rtc['execution time limit'] = (
52-
sleep_sec + DurationParser().parse(str(
53-
rtc['simulation']['time limit buffer'])).get_seconds()
54-
)
5553

56-
rtc['simulation']['simulated run length'] = sleep_sec
57-
rtc['submission retry delays'] = [1]
54+
def configure_rtc_sim_mode(rtc, dummy_mode):
55+
"""Change a task proxy's runtime config to simulation mode settings.
56+
"""
57+
sleep_sec = get_simulated_run_len(rtc)
5858

59+
rtc['execution time limit'] = (
60+
sleep_sec + DurationParser().parse(str(
61+
rtc['simulation']['time limit buffer'])).get_seconds()
62+
)
63+
64+
rtc['simulation']['simulated run length'] = sleep_sec
65+
rtc['submission retry delays'] = [1]
66+
67+
if dummy_mode:
5968
# Generate dummy scripting.
6069
rtc['init-script'] = ""
6170
rtc['env-script'] = ""
6271
rtc['pre-script'] = ""
6372
rtc['post-script'] = ""
6473
rtc['script'] = build_dummy_script(
6574
rtc, sleep_sec) if dummy_mode else ""
75+
else:
76+
rtc['script'] = ""
6677

67-
disable_platforms(rtc)
78+
disable_platforms(rtc)
6879

69-
# Disable environment, in case it depends on env-script.
70-
rtc['environment'] = {}
80+
rtc['platform'] = 'localhost'
7181

72-
rtc["simulation"][
73-
"fail cycle points"
74-
] = parse_fail_cycle_points(
75-
rtc["simulation"]["fail cycle points"]
76-
)
82+
# Disable environment, in case it depends on env-script.
83+
rtc['environment'] = {}
84+
85+
rtc["simulation"][
86+
"fail cycle points"
87+
] = parse_fail_cycle_points(
88+
rtc["simulation"]["fail cycle points"]
89+
)
7790

7891

7992
def get_simulated_run_len(rtc: Dict[str, Any]) -> int:
8093
"""Get simulated run time.
8194
82-
rtc = run time config
95+
Args:
96+
rtc: run time config
97+
98+
Returns:
99+
Number of seconds to sleep for in sim mode.
83100
"""
101+
# Simulated run length acts as a flag that this is at runtime:
102+
# If durations have already been parsed, trying to parse them
103+
# again will result in failures.
104+
recalc = bool(rtc['simulation'].get('simulated run length', ''))
84105
limit = rtc['execution time limit']
85106
speedup = rtc['simulation']['speedup factor']
86-
if limit and speedup:
87-
sleep_sec = (DurationParser().parse(
88-
str(limit)).get_seconds() / speedup)
107+
108+
if recalc:
109+
if limit and speedup:
110+
sleep_sec = limit / speedup
111+
else:
112+
sleep_sec = rtc['simulation']['default run length']
89113
else:
90-
sleep_sec = DurationParser().parse(
91-
str(rtc['simulation']['default run length'])
92-
).get_seconds()
114+
if limit and speedup:
115+
sleep_sec = (DurationParser().parse(
116+
str(limit)).get_seconds() / speedup)
117+
else:
118+
default_run_len = str(rtc['simulation']['default run length'])
119+
sleep_sec = DurationParser().parse(default_run_len).get_seconds()
93120

94121
return sleep_sec
95122

@@ -147,7 +174,7 @@ def parse_fail_cycle_points(
147174
[]
148175
"""
149176
f_pts: 'Optional[List[PointBase]]'
150-
if 'all' in f_pts_orig:
177+
if f_pts_orig is None or 'all' in f_pts_orig:
151178
f_pts = None
152179
else:
153180
f_pts = []
@@ -156,19 +183,84 @@ def parse_fail_cycle_points(
156183
return f_pts
157184

158185

186+
def unpack_dict(dict_: NestedDict, parent_key: str = '') -> Dict[str, Any]:
187+
"""Unpack a nested dict into a single layer.
188+
189+
Examples:
190+
>>> unpack_dict({'foo': 1, 'bar': {'baz': 2, 'qux':3}})
191+
{'foo': 1, 'bar.baz': 2, 'bar.qux': 3}
192+
>>> unpack_dict({'foo': {'example': 42}, 'bar': {"1":2, "3":4}})
193+
{'foo.example': 42, 'bar.1': 2, 'bar.3': 4}
194+
195+
"""
196+
output = {}
197+
for key, value in dict_.items():
198+
new_key = parent_key + '.' + key if parent_key else key
199+
if isinstance(value, dict):
200+
output.update(unpack_dict(value, new_key))
201+
else:
202+
output[new_key] = value
203+
204+
return output
205+
206+
207+
def nested_dict_path_update(
208+
dict_: NestedDict, path: List[Any], value: Any
209+
) -> NestedDict:
210+
"""Set a value in a nested dict.
211+
212+
Examples:
213+
>>> nested_dict_path_update({'foo': {'bar': 1}}, ['foo', 'bar'], 42)
214+
{'foo': {'bar': 42}}
215+
"""
216+
this = dict_
217+
for i in range(len(path)):
218+
if isinstance(this[path[i]], dict):
219+
this = this[path[i]]
220+
else:
221+
this[path[i]] = value
222+
return dict_
223+
224+
225+
def update_nested_dict(rtc: NestedDict, dict_: NestedDict) -> None:
226+
"""Update one config nested dictionary with the contents of another.
227+
228+
Examples:
229+
>>> x = {'foo': {'bar': 12}, 'qux': 77}
230+
>>> y = {'foo': {'bar': 42}}
231+
>>> update_nested_dict(x, y)
232+
>>> print(x)
233+
{'foo': {'bar': 42}, 'qux': 77}
234+
"""
235+
for keylist, value in unpack_dict(dict_).items():
236+
keys = keylist.split('.')
237+
rtc = nested_dict_path_update(rtc, keys, value)
238+
239+
159240
def sim_time_check(
160-
message_queue: 'Queue[TaskMsg]', itasks: 'List[TaskProxy]'
241+
message_queue: 'Queue[TaskMsg]',
242+
itasks: 'List[TaskProxy]',
243+
broadcast_mgr: Optional[Any] = None
161244
) -> bool:
162245
"""Check if sim tasks have been "running" for as long as required.
163246
164247
If they have change the task state.
248+
If broadcasts are active and they apply to tasks in itasks update
249+
itasks.rtconfig.
165250
166251
Returns:
167252
True if _any_ simulated task state has changed.
168253
"""
254+
169255
sim_task_state_changed = False
170256
now = time()
171257
for itask in itasks:
258+
if broadcast_mgr:
259+
broadcast = broadcast_mgr.get_broadcast(itask.tokens)
260+
if broadcast:
261+
update_nested_dict(
262+
itask.tdef.rtconfig, broadcast)
263+
configure_rtc_sim_mode(itask.tdef.rtconfig, False)
172264
if itask.state.status != TASK_STATUS_RUNNING:
173265
continue
174266
# Started time is not set on restart
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#!/usr/bin/env bash
2+
# THIS FILE IS PART OF THE CYLC WORKFLOW ENGINE.
3+
# Copyright (C) NIWA & British Crown (Met Office) & Contributors.
4+
#
5+
# This program is free software: you can redistribute it and/or modify
6+
# it under the terms of the GNU General Public License as published by
7+
# the Free Software Foundation, either version 3 of the License, or
8+
# (at your option) any later version.
9+
#
10+
# This program is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU General Public License
16+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
18+
# Test that we can broadcast an alteration to simulation mode.
19+
20+
. "$(dirname "$0")/test_header"
21+
set_test_number 3
22+
23+
install_workflow "${TEST_NAME_BASE}" "${TEST_NAME_BASE}"
24+
run_ok "${TEST_NAME_BASE}-validate" cylc validate "${WORKFLOW_NAME}"
25+
workflow_run_ok "${TEST_NAME_BASE}-run" \
26+
cylc play "${WORKFLOW_NAME}" --mode=simulation
27+
SCHD_LOG="${WORKFLOW_RUN_DIR}/log/scheduler/log"
28+
29+
# If we speed up the simulated task we
30+
# can make it finish before workflow timeout:
31+
cylc broadcast "${WORKFLOW_NAME}" -s '[simulation]speedup factor = 600'
32+
33+
# Wait for the workflow to finish (it wasn't run in no-detach mode):
34+
poll_grep "INFO - DONE" "${SCHD_LOG}"
35+
36+
# If we hadn't changed the speedup factor using broadcast
37+
# The workflow timeout would have been hit:
38+
grep_fail "WARNING - Orphaned tasks" "${SCHD_LOG}"
39+
40+
purge
41+
exit
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[scheduler]
2+
[[events]]
3+
workflow timeout = PT30S
4+
5+
[scheduling]
6+
initial cycle point = 2359
7+
[[graph]]
8+
R1 = get_observations
9+
10+
[runtime]
11+
[[get_observations]]
12+
execution retry delays = PT10M
13+
[[[simulation]]]
14+
speedup factor = 1
15+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
23590101T0000Z/get_observations -triggered off [] in flow 1
2+
23590101T0000Z/get_observations -triggered off [] in flow 1

tests/unit/test_simulation.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
param(None, 10, 'PT1H', id='speedup-factor-alone'),
3737
param('PT1H', None, 'PT1H', id='execution-time-limit-alone'),
3838
param('P1D', 24, 'PT1M', id='speed-up-and-execution-tl'),
39+
param(60 * 60 * 24, 24, 'PT1M', id='recalculation'),
40+
param(1, None, 3600, id='recalculation'),
3941
)
4042
)
4143
def test_get_simulated_run_len(
@@ -49,9 +51,11 @@ def test_get_simulated_run_len(
4951
'execution time limit': execution_time_limit,
5052
'simulation': {
5153
'speedup factor': speedup_factor,
52-
'default run length': default_run_length
53-
}
54+
'default run length': default_run_length,
55+
},
5456
}
57+
if isinstance(execution_time_limit, int):
58+
rtc['simulation']['simulated run length'] = 30
5559
assert get_simulated_run_len(rtc) == 3600
5660

5761

0 commit comments

Comments
 (0)