@@ -266,12 +266,22 @@ def db_add_new_flow_rows(self, itask: TaskProxy) -> None:
266266 self .workflow_db_mgr .put_insert_task_outputs (itask )
267267
268268 def add_to_pool (self , itask ) -> None :
269- """Add a task to the pool."""
269+ """Add a task to the pool, if not already added ."""
270270
271271 self .active_tasks .setdefault (itask .point , {})
272+ if itask .identity in self .active_tasks [itask .point ]:
273+ # This is normal on restart with waiting parentless tasks that
274+ # auto-spawned to the runahead limit before shutdown (e.g. if
275+ # you shut down immediately after starting up paused). Each
276+ # will be resurrected from the DB again *and* try to auto-spawn
277+ # again because we don't record that parentless tasks spawned.
278+ # (But debug-log it anyway for any unexpected cases).
279+ LOG .debug (f"{ itask .identity } not added to n=0: already exists" )
280+ return None
281+
272282 self .active_tasks [itask .point ][itask .identity ] = itask
273283 self .active_tasks_changed = True
274- LOG .debug (f"[{ itask } ] added to the n=0 window" )
284+ LOG .info (f"[{ itask } ] added to the n=0 window" )
275285
276286 self .create_data_store_elements (itask )
277287
@@ -1835,13 +1845,20 @@ def spawn_task(
18351845 if (
18361846 prev_status is not None
18371847 and not itask .state .outputs .get_completed_outputs ()
1848+ and not self .config .experimental .expire_triggers
18381849 ):
1839- # If itask has any history in this flow but no completed outputs
1840- # we can infer it has just been deliberately removed (N.B. not
1841- # by `cylc remove`), so don't immediately respawn it.
1842- # TODO (follow-up work):
1843- # - this logic fails if task removed after some outputs completed
1844- LOG .info (f"Not respawning { point } /{ name } - task was removed" )
1850+ # If itask has history but no completed outputs, it was removed
1851+ # by suicide trigger (not by `cylc remove` which erases history).
1852+ # This logic fails if suicided after completion of outputs!
1853+
1854+ # NOTE: redoing suicide triggers as expire triggers fixed this.
1855+ # TODO: remove this code once that's no longer experimental.
1856+ # Until then, this code also prevents double-spawning of waiting
1857+ # parentless tasks at restart if experimental expire triggers are
1858+ # off (but that is also handled properly without this block - by
1859+ # not adding tasks to the pool if already added (see comments in
1860+ # the add_to_pool method).
1861+ LOG .debug (f"Not respawning { point } /{ name } " )
18451862 return None
18461863
18471864 if prev_status in TASK_STATUSES_FINAL :
0 commit comments