619
_tasks: SchedulerTaskList
620
_printer: PrintStyle
621
_instance = None
622
+ _running_deferred_tasks: Dict[str, DeferredTask]
623
+ _running_tasks_lock: threading.RLock
624
625
@classmethod
626
def get(cls) -> "TaskScheduler":
633
if not hasattr(self, '_initialized'):
634
self._tasks = SchedulerTaskList.get()
635
self._printer = PrintStyle(italic=True, font_color="green", padding=False)
636
+ self._running_deferred_tasks = {}
637
+ self._running_tasks_lock = threading.RLock()
638
self._initialized = True
639
640
+ def _register_running_task(self, task_uuid: str, deferred_task: DeferredTask) -> None:
641
+ with self._running_tasks_lock:
642
+ self._running_deferred_tasks[task_uuid] = deferred_task
643
+
644
+ def _unregister_running_task(self, task_uuid: str) -> None:
645
+ with self._running_tasks_lock:
646
+ self._running_deferred_tasks.pop(task_uuid, None)
647
+
648
+ def cancel_running_task(self, task_uuid: str, terminate_thread: bool = False) -> bool:
649
+ with self._running_tasks_lock:
650
+ deferred_task = self._running_deferred_tasks.get(task_uuid)
651
+ if not deferred_task:
652
+ return False
653
+ self._printer.print(f"Scheduler cancelling task {task_uuid}")
654
+ deferred_task.kill(terminate_thread=terminate_thread)
655
+ return True
656
+
657
+ def cancel_tasks_by_context(self, context_id: str, terminate_thread: bool = False) -> bool:
658
+ cancelled_any = False
659
+ with self._running_tasks_lock:
660
+ running_tasks = list(self._running_deferred_tasks.keys())
661
+ for task_uuid in running_tasks:
662
+ task = self.get_task_by_uuid(task_uuid)
663
+ if task and task.context_id == context_id:
664
+ if self.cancel_running_task(task_uuid, terminate_thread=terminate_thread):
665
+ cancelled_any = True
666
+ return cancelled_any
667
+
668
async def reload(self):
669
await self._tasks.reload()
670
806
task_snapshot: Union[ScheduledTask, AdHocTask, PlannedTask] | None = self.get_task_by_uuid(task_uuid)
807
if task_snapshot is None:
808
self._printer.print(f"Scheduler Task with UUID '{task_uuid}' not found")
809
+ self._unregister_running_task(task_uuid)
810
return
811
if task_snapshot.state == TaskState.RUNNING:
812
self._printer.print(f"Scheduler Task '{task_snapshot.name}' already running, skipping")
813
+ self._unregister_running_task(task_uuid)
814
return
815
816
# Atomically fetch and check the task's current state
817
current_task = await self.update_task_checked(task_uuid, lambda task: task.state != TaskState.RUNNING, state=TaskState.RUNNING)
818
if not current_task:
819
self._printer.print(f"Scheduler Task with UUID '{task_uuid}' not found or updated by another process")
820
+ self._unregister_running_task(task_uuid)
821
return
822
if current_task.state != TaskState.RUNNING:
823
# This means the update failed due to state conflict
824
self._printer.print(f"Scheduler Task '{current_task.name}' state is '{current_task.state}', skipping")
825
+ self._unregister_running_task(task_uuid)
826
return
827
828
await current_task.on_run()
904
self._printer.print(f"Fixing task state consistency: '{current_task.name}' state is not IDLE after success")
905
await self.update_task(task_uuid, state=TaskState.IDLE)
906
907
+ except asyncio.CancelledError:
908
+ self._printer.print(f"Scheduler Task '{current_task.name}' cancelled by user")
909
+ try:
910
+ await asyncio.shield(self.update_task(task_uuid, state=TaskState.IDLE))
911
+ except Exception:
912
+ pass
913
+ raise
914
except Exception as e:
915
# Error
916
self._printer.print(f"Scheduler Task '{current_task.name}' failed: {e}")
927
agent.handle_critical_exception(e)
928
finally:
929
# Call on_finish for task-specific cleanup
887
- await current_task.on_finish()
930
+ try:
931
+ await asyncio.shield(current_task.on_finish())
932
+ except asyncio.CancelledError:
933
+ pass
934
+ except Exception:
935
+ pass
936
937
# Make one final save to ensure all states are persisted
890
- await self._tasks.save()
938
+ try:
939
+ await asyncio.shield(self._tasks.save())
940
+ except asyncio.CancelledError:
941
+ pass
942
+ except Exception:
943
+ pass
944
+
945
+ self._unregister_running_task(task_uuid)
946
947
deferred_task = DeferredTask(thread_name=self.__class__.__name__)
948
+ self._register_running_task(task.uuid, deferred_task)
949
deferred_task.start_task(_run_task_wrapper, task.uuid, task_context)
950
895
- # Ensure background execution doesn't exit immediately on async await, especially in script contexts
896
- # This helps prevent premature exits when running from non-event-loop contexts
897
- asyncio.create_task(asyncio.sleep(0.1))
951
+ # Ensure background execution doesn't exit immediately on async await, especially in script contexts.
952
+ # Yielding briefly keeps callers like CLI scripts alive long enough for the DeferredTask thread to spin up
953
+ # without leaving stray pending tasks that trigger \"Task was destroyed\" warnings when the loop shuts down.
954
+ await asyncio.sleep(0.1)
955
956
def serialize_all_tasks(self) -> list[Dict[str, Any]]:
957
"""