|
| 1 | +# Signals |
| 2 | + |
| 3 | +!!! beta "Beta Feature" |
| 4 | + Signals are currently available in beta. The API and functionality may change in future releases. |
| 5 | + |
| 6 | +Signals are a mechanism in Exosphere for controlling workflow execution flow and state management. They allow nodes to communicate with the state manager to perform specific actions like pruning states or requeuing them after a delay. |
| 7 | + |
| 8 | +## Overview |
| 9 | + |
| 10 | +Signals are implemented as exceptions that should be raised from within node execution. When a signal is raised, the runtime automatically handles the communication with the state manager to perform the requested action. |
| 11 | + |
| 12 | +## Available Signals |
| 13 | + |
| 14 | +### PruneSignal |
| 15 | + |
| 16 | +The `PruneSignal` is used to permanently remove a state from the workflow execution. This is typically used when a node determines that the current execution path should be terminated. |
| 17 | + |
| 18 | +#### Usage |
| 19 | + |
| 20 | +```python |
| 21 | +from exospherehost import PruneSignal |
| 22 | + |
| 23 | +class MyNode(BaseNode): |
| 24 | + class Inputs(BaseModel): |
| 25 | + data: str |
| 26 | + |
| 27 | + class Outputs(BaseModel): |
| 28 | + result: str |
| 29 | + |
| 30 | + async def execute(self, inputs: Inputs) -> Outputs: |
| 31 | + if inputs.data == "invalid": |
| 32 | + # Prune the state with optional data |
| 33 | + raise PruneSignal({"reason": "invalid_data", "error": "Data validation failed"}) |
| 34 | + |
| 35 | + return self.Outputs(result="processed") |
| 36 | +``` |
| 37 | + |
| 38 | +#### Parameters |
| 39 | + |
| 40 | +- `data` (dict[str, Any], optional): Additional data to include with the prune operation. Defaults to an empty dictionary. |
| 41 | + |
| 42 | +### ReQueueAfterSignal |
| 43 | + |
| 44 | +The `ReQueueAfterSignal` is used to requeue a state for execution after a specified time delay. This is useful for implementing retry logic, scheduled tasks, or rate limiting. |
| 45 | + |
| 46 | +#### Usage |
| 47 | + |
| 48 | +```python |
| 49 | +from exospherehost import ReQueueAfterSignal |
| 50 | +from datetime import timedelta |
| 51 | + |
| 52 | +class RetryNode(BaseNode): |
| 53 | + class Inputs(BaseModel): |
| 54 | + retry_count: int |
| 55 | + data: str |
| 56 | + |
| 57 | + class Outputs(BaseModel): |
| 58 | + result: str |
| 59 | + |
| 60 | + async def execute(self, inputs: Inputs) -> Outputs: |
| 61 | + if inputs.retry_count < 3: |
| 62 | + # Requeue after 5 minutes |
| 63 | + raise ReQueueAfterSignal(timedelta(minutes=5)) |
| 64 | + |
| 65 | + return self.Outputs(result="completed") |
| 66 | +``` |
| 67 | + |
| 68 | +#### Parameters |
| 69 | + |
| 70 | +- `delay` (timedelta): The amount of time to wait before requeuing the state. Must be greater than 0. |
| 71 | + |
| 72 | +## Important Notes |
| 73 | + |
| 74 | +1. **Do not catch signals**: Signals are designed to bubble up to the runtime for handling. Do not catch these exceptions in your node code. |
| 75 | + |
| 76 | +2. **Automatic handling**: The runtime automatically sends signals to the state manager when they are raised. |
| 77 | + |
| 78 | +3. **State lifecycle**: Signals affect the state's lifecycle in the state manager: |
| 79 | + - `PruneSignal`: Sets state status to `PRUNED` |
| 80 | + - `ReQueueAfterSignal`: Sets state status to `CREATED` and schedules requeue |
| 81 | + |
| 82 | +## Error Handling |
| 83 | + |
| 84 | +If signal sending fails (e.g., network issues), the runtime will log the error and continue processing other states. The failed signal will not be retried automatically. |
| 85 | + |
| 86 | +## Examples |
| 87 | + |
| 88 | +### Conditional Pruning |
| 89 | + |
| 90 | +```python |
| 91 | +class ValidationNode(BaseNode): |
| 92 | + class Inputs(BaseModel): |
| 93 | + user_id: str |
| 94 | + data: dict |
| 95 | + |
| 96 | + async def execute(self, inputs: Inputs) -> Outputs: |
| 97 | + if not self._validate_user(inputs.user_id): |
| 98 | + raise PruneSignal({ |
| 99 | + "reason": "invalid_user", |
| 100 | + "user_id": inputs.user_id, |
| 101 | + "timestamp": datetime.now().isoformat() |
| 102 | + }) |
| 103 | + |
| 104 | + return self.Outputs(validated=True) |
| 105 | +``` |
| 106 | + |
| 107 | +### Polling |
| 108 | + |
| 109 | +```python |
| 110 | +class PollingNode(BaseNode): |
| 111 | + class Inputs(BaseModel): |
| 112 | + job_id: str |
| 113 | + |
| 114 | + async def execute(self, inputs: Inputs) -> Outputs: |
| 115 | + # Check if the job is complete |
| 116 | + job_status = await self._check_job_status(inputs.job_id) |
| 117 | + |
| 118 | + if job_status == "completed": |
| 119 | + result = await self._get_job_result(inputs.job_id) |
| 120 | + return self.Outputs(result=result) |
| 121 | + elif job_status == "failed": |
| 122 | + # Job failed, prune the state |
| 123 | + raise PruneSignal({ |
| 124 | + "reason": "job_failed", |
| 125 | + "job_id": inputs.job_id, |
| 126 | + "poll_count": inputs.poll_count |
| 127 | + }) |
| 128 | + else: |
| 129 | + # Job still running, poll again in 30 seconds |
| 130 | + raise ReQueueAfterSignal(timedelta(seconds=30)) |
| 131 | +``` |
0 commit comments