@@ -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
@@ -1834,13 +1844,20 @@ def spawn_task(
18341844 if (
18351845 prev_status is not None
18361846 and not itask .state .outputs .get_completed_outputs ()
1847+ and not self .config .experimental .expire_triggers
18371848 ):
1838- # If itask has any history in this flow but no completed outputs
1839- # we can infer it has just been deliberately removed (N.B. not
1840- # by `cylc remove`), so don't immediately respawn it.
1841- # TODO (follow-up work):
1842- # - this logic fails if task removed after some outputs completed
1843- LOG .info (f"Not respawning { point } /{ name } - task was removed" )
1849+ # If itask has history but no completed outputs, it was removed
1850+ # by suicide trigger (not by `cylc remove` which erases history).
1851+ # This logic fails if suicided after completion of outputs!
1852+
1853+ # NOTE: redoing suicide triggers as expire triggers fixed this.
1854+ # TODO: remove this code once that's no longer experimental.
1855+ # Until then, this code also prevents double-spawning of waiting
1856+ # parentless tasks at restart if experimental expire triggers are
1857+ # off (but that is also handled properly without this block - by
1858+ # not adding tasks to the pool if already added (see comments in
1859+ # the add_to_pool method).
1860+ LOG .debug (f"Not respawning { point } /{ name } " )
18441861 return None
18451862
18461863 if prev_status in TASK_STATUSES_FINAL :
0 commit comments