|
38 | 38 | from .models import LoopEvent |
39 | 39 | from .monitor import LoopMonitor |
40 | 40 | from .scheduler import Schedule, validate_cron |
41 | | -from .state.state import StateManager, create_state_manager |
| 41 | +from .state.state import LoopState, StateManager, create_state_manager |
42 | 42 | from .task import TaskManager, TaskResult |
43 | 43 | from .types import BaseConfig, ExecutorType, LoopStatus, RetryPolicy |
44 | 44 | from .utils import get_func_import_path, import_func_from_path, infer_application_path |
@@ -682,7 +682,7 @@ async def _cancel_handler(workflow_run_id: str): |
682 | 682 | ) from e |
683 | 683 |
|
684 | 684 | async def _resume_handler( |
685 | | - workflow_run_id: str, request: dict[str, Any] = {} |
| 685 | + workflow_run_id: str, request: dict[str, Any] | None = None |
686 | 686 | ): |
687 | 687 | try: |
688 | 688 | workflow = await self.state_manager.get_workflow(workflow_run_id) |
@@ -829,6 +829,166 @@ async def has_active_clients(self, loop_id: str) -> bool: |
829 | 829 | client_count = await self.state_manager.get_active_client_count(loop_id) |
830 | 830 | return client_count > 0 |
831 | 831 |
|
| 832 | + async def start_loop( |
| 833 | + self, |
| 834 | + name: str, |
| 835 | + loop_id: str, |
| 836 | + initial_data: dict[str, Any] | None = None, |
| 837 | + ) -> LoopState: |
| 838 | + """Start a named loop with a specific loop_id. |
| 839 | +
|
| 840 | + Args: |
| 841 | + name: The registered loop name (from @app.loop decorator) |
| 842 | + loop_id: The unique identifier for this loop instance |
| 843 | + initial_data: Optional initial context data for the loop |
| 844 | +
|
| 845 | + Returns: |
| 846 | + The LoopState for the started loop |
| 847 | +
|
| 848 | + Raises: |
| 849 | + LoopNotFoundError: If the loop name is not registered |
| 850 | + """ |
| 851 | + if name not in self._loop_metadata: |
| 852 | + raise LoopNotFoundError(f"Loop '{name}' is not registered") |
| 853 | + |
| 854 | + metadata = self._loop_metadata[name] |
| 855 | + func = metadata["func"] |
| 856 | + |
| 857 | + loop, _created = await self.state_manager.get_or_create_loop( |
| 858 | + loop_name=name, |
| 859 | + loop_id=loop_id, |
| 860 | + current_function_path=get_func_import_path(func), |
| 861 | + create_with_id=True, |
| 862 | + ) |
| 863 | + |
| 864 | + if loop.status == LoopStatus.STOPPED: |
| 865 | + logger.warning( |
| 866 | + "Loop is stopped, not starting", |
| 867 | + extra={"loop_id": loop_id, "loop_name": name}, |
| 868 | + ) |
| 869 | + return loop |
| 870 | + |
| 871 | + context = LoopContext( |
| 872 | + loop_id=loop.loop_id, |
| 873 | + initial_event=None, |
| 874 | + state_manager=self.state_manager, |
| 875 | + integrations=metadata.get("integrations", []), |
| 876 | + ) |
| 877 | + |
| 878 | + if initial_data: |
| 879 | + for key, value in initial_data.items(): |
| 880 | + await context.set(key, value) |
| 881 | + |
| 882 | + await context.setup_integrations() |
| 883 | + |
| 884 | + loop_instance: Loop | None = metadata.get("loop_instance") |
| 885 | + if loop_instance: |
| 886 | + loop_instance.ctx = context |
| 887 | + func = loop_instance.loop |
| 888 | + |
| 889 | + started = await self.loop_manager.start( |
| 890 | + func=func, |
| 891 | + loop_start_func=metadata.get("on_start"), |
| 892 | + loop_stop_func=metadata.get("on_stop"), |
| 893 | + context=context, |
| 894 | + loop=loop, |
| 895 | + loop_delay=metadata["loop_delay"], |
| 896 | + stop_after_idle_seconds=metadata.get("stop_after_idle_seconds"), |
| 897 | + pause_after_idle_seconds=metadata.get("pause_after_idle_seconds"), |
| 898 | + ) |
| 899 | + |
| 900 | + if started: |
| 901 | + logger.info( |
| 902 | + "Loop started", |
| 903 | + extra={"loop_id": loop.loop_id, "loop_name": name}, |
| 904 | + ) |
| 905 | + |
| 906 | + return await self.state_manager.get_loop(loop.loop_id) |
| 907 | + |
| 908 | + async def stop_loop(self, name: str, loop_id: str) -> bool: |
| 909 | + """Stop a specific loop instance. |
| 910 | +
|
| 911 | + Args: |
| 912 | + name: The registered loop name |
| 913 | + loop_id: The unique identifier for this loop instance |
| 914 | +
|
| 915 | + Returns: |
| 916 | + True if the loop was stopped, False if it wasn't running |
| 917 | + """ |
| 918 | + if name not in self._loop_metadata: |
| 919 | + raise LoopNotFoundError(f"Loop '{name}' is not registered") |
| 920 | + |
| 921 | + try: |
| 922 | + loop = await self.state_manager.get_loop(loop_id) |
| 923 | + if loop.loop_name != name: |
| 924 | + logger.warning( |
| 925 | + "Loop name mismatch", |
| 926 | + extra={ |
| 927 | + "expected": name, |
| 928 | + "actual": loop.loop_name, |
| 929 | + "loop_id": loop_id, |
| 930 | + }, |
| 931 | + ) |
| 932 | + return False |
| 933 | + |
| 934 | + await self.state_manager.update_loop_status(loop_id, LoopStatus.STOPPED) |
| 935 | + stopped = await self.loop_manager.stop(loop_id) |
| 936 | + |
| 937 | + if stopped: |
| 938 | + logger.info( |
| 939 | + "Loop stopped", |
| 940 | + extra={"loop_id": loop_id, "loop_name": name}, |
| 941 | + ) |
| 942 | + |
| 943 | + return stopped |
| 944 | + |
| 945 | + except LoopNotFoundError: |
| 946 | + return False |
| 947 | + |
| 948 | + async def loop_exists(self, name: str, loop_id: str) -> bool: |
| 949 | + """Check if a loop instance exists and is active (running or idle). |
| 950 | +
|
| 951 | + Args: |
| 952 | + name: The registered loop name |
| 953 | + loop_id: The unique identifier for this loop instance |
| 954 | +
|
| 955 | + Returns: |
| 956 | + True if the loop exists and is running/idle, False otherwise |
| 957 | + """ |
| 958 | + if name not in self._loop_metadata: |
| 959 | + return False |
| 960 | + |
| 961 | + try: |
| 962 | + loop = await self.state_manager.get_loop(loop_id) |
| 963 | + if loop.loop_name != name: |
| 964 | + return False |
| 965 | + return loop.status in ( |
| 966 | + LoopStatus.RUNNING, |
| 967 | + LoopStatus.IDLE, |
| 968 | + LoopStatus.PENDING, |
| 969 | + ) |
| 970 | + except LoopNotFoundError: |
| 971 | + return False |
| 972 | + |
| 973 | + async def list_loops(self, name: str) -> list[str]: |
| 974 | + """List all active loop IDs for a given loop name. |
| 975 | +
|
| 976 | + Args: |
| 977 | + name: The registered loop name |
| 978 | +
|
| 979 | + Returns: |
| 980 | + List of loop_ids that are currently active (running or idle) |
| 981 | + """ |
| 982 | + if name not in self._loop_metadata: |
| 983 | + raise LoopNotFoundError(f"Loop '{name}' is not registered") |
| 984 | + |
| 985 | + loops = await self.state_manager.get_loops_by_name(name) |
| 986 | + return [ |
| 987 | + loop.loop_id |
| 988 | + for loop in loops |
| 989 | + if loop.status in (LoopStatus.RUNNING, LoopStatus.IDLE, LoopStatus.PENDING) |
| 990 | + ] |
| 991 | + |
832 | 992 | async def restart_workflow(self, workflow_run_id: str) -> bool: |
833 | 993 | """Restart a workflow from its persisted state.""" |
834 | 994 | try: |
|
0 commit comments