Spaces:
Sleeping
Sleeping
File size: 7,236 Bytes
116524e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 | # Error Handling
The pipeline engine guarantees that every sample produces a `SampleResult` β nothing is dropped silently. One failing sample never blocks others. Retry logic is the responsibility of individual steps, not the pipeline.
---
## SampleResult
Every sample that enters `run()` produces exactly one `SampleResult`:
```python
@dataclass
class SampleResult:
sample: Any # the original input
output: StepContext | None # final context (None if failed)
error: Exception | None # the exception (None if succeeded)
failed_at: str | None # step class name where error occurred
cause: Exception | None = None # inner exception for BranchError
```
| Field | On success | On failure |
|-------|-----------|------------|
| `sample` | original input | original input |
| `output` | final `StepContext` | `None` |
| `error` | `None` | the exception |
| `failed_at` | `None` | class name of the failing step (e.g. `"Tokenize"`) |
| `cause` | `None` | inner exception when `failed_at == "Branch"` |
!!! note "Background steps"
For steps after an `async_boundary`, `output` and `error` may still be `None` when `run()` returns. Call `pipe.wait_for_background()` to block until all background work completes and results are finalized.
---
## Construction-time errors
These are caught **before any data flows** β they surface immediately when you build the pipeline.
### PipelineOrderError
Raised when a step requires a field that is produced by a **later** step in the pipeline:
```python
from pipeline import Pipeline
from pipeline.errors import PipelineOrderError
class Uppercase:
requires = frozenset({"tokens"})
provides = frozenset({"upper_tokens"})
def __call__(self, ctx): ...
class Tokenize:
requires = frozenset()
provides = frozenset({"tokens"})
def __call__(self, ctx): ...
try:
Pipeline().then(Uppercase()).then(Tokenize())
except PipelineOrderError as e:
print(e)
# Uppercase requires {"tokens"} but it is provided by a later step
```
!!! tip
`PipelineOrderError` is always a bug β reorder your steps. Fields not produced by **any** step in the pipeline are treated as external inputs and do not trigger this error.
### PipelineConfigError
Raised for invalid pipeline wiring:
**Multiple async boundaries:**
```python
from pipeline.errors import PipelineConfigError
class StepA:
requires = frozenset()
provides = frozenset({"a"})
async_boundary = True
def __call__(self, ctx): ...
class StepB:
requires = frozenset({"a"})
provides = frozenset({"b"})
async_boundary = True
def __call__(self, ctx): ...
try:
Pipeline().then(StepA()).then(StepB())
except PipelineConfigError:
print("Only one async_boundary per pipeline is allowed")
```
**Async boundary inside a Branch child:**
```python
try:
Pipeline().branch(
Pipeline().then(StepA()), # async_boundary = True inside branch
)
except PipelineConfigError:
print("async_boundary inside Branch children is not allowed")
```
---
## Runtime errors
### Foreground failures
When a step before the `async_boundary` (or in a pipeline with no boundary) raises an exception, the pipeline catches it per-sample and records it in the `SampleResult`:
```python
class Boom:
requires = frozenset()
provides = frozenset()
def __call__(self, ctx):
raise RuntimeError(f"Failed on {ctx.sample!r}")
pipe = Pipeline().then(Tokenize()).then(Boom())
results = pipe.run([
StepContext(sample="good"),
StepContext(sample="also good"),
])
for r in results:
if r.error:
print(f"Sample '{r.sample}' failed at {r.failed_at}: {r.error}")
else:
print(f"Sample '{r.sample}' succeeded")
```
```
Sample 'good' failed at Boom: Failed on 'good'
Sample 'also good' failed at Boom: Failed on 'also good'
```
Each sample is processed independently β one failure does not prevent others from running.
### Background failures
When a step **after** the `async_boundary` raises, the caller has already moved on. The exception is captured and attached to the `SampleResult` in-place:
```python
pipe = Pipeline().then(Tokenize()).then(BrokenBackgroundStep())
results = pipe.run(samples)
# results returned immediately β background still running
pipe.wait_for_background(timeout=10.0)
# Now check for background failures
for r in results:
if r.error:
print(f"Background failure at {r.failed_at}: {r.error}")
```
### PipelineCancelled
When a `cancel_token` is triggered, remaining steps and samples are cancelled with `PipelineCancelled`:
```python
from pipeline import CancellationToken, PipelineCancelled
token = CancellationToken()
# ... later, from another thread or endpoint:
token.cancel()
results = pipe.run(contexts, cancel_token=token)
for r in results:
if isinstance(r.error, PipelineCancelled):
print(f"Sample '{r.sample}' was cancelled before {r.failed_at}")
elif r.error:
print(f"Sample '{r.sample}' failed at {r.failed_at}: {r.error}")
```
Cancellation is checked **between** steps β a running step always completes. `PipelineCancelled` follows the same `SampleResult` pattern as step errors: it is caught per-sample, not propagated.
### BranchError
When one or more branch pipelines fail, a `BranchError` is raised with the full list of failures:
```python
from pipeline.errors import BranchError
results = pipe.run(contexts)
for r in results:
if isinstance(r.error, BranchError):
print(f"{len(r.error.failures)} branch(es) failed:")
for f in r.error.failures:
print(f" {type(f).__name__}: {f}")
elif r.error:
print(f"Step failure at {r.failed_at}: {r.error}")
```
All branches run to completion before `BranchError` is raised β no branch is cancelled when another fails. The `SampleResult.cause` field carries the inner exception from the failing branch.
---
## Inspecting results
The standard pattern after `run()`:
```python
results = pipe.run(contexts)
pipe.wait_for_background() # if using async_boundary
succeeded = [r for r in results if r.error is None]
failed = [r for r in results if r.error is not None]
print(f"{len(succeeded)} succeeded, {len(failed)} failed")
for r in failed:
print(f" Sample: {r.sample}")
print(f" Failed at: {r.failed_at}")
print(f" Error: {r.error}")
```
---
## Background monitoring
### `wait_for_background()`
Blocks until all background tasks complete:
```python
# Wait indefinitely
pipe.wait_for_background()
# Wait with timeout β raises TimeoutError if not done
pipe.wait_for_background(timeout=30.0)
```
Completed threads are removed from the tracking list after this call.
### `background_stats()`
Returns a snapshot of background task progress. Thread-safe β can be called from any thread while the pipeline is running:
```python
stats = pipe.background_stats()
print(stats)
# {'active': 2, 'completed': 8}
```
|