Failure handling¶
Added in v0.3.1.210.
A feature-engineering chain is a pipeline: level 3 reads what level 2 produced. So the question is not only "did this process fail?" but "what should happen to everything that depends on it?" This page covers how tdfs4ds reports a failed ingestion, how it records one, and how to stop a failure from quietly contaminating the levels above it.
What changed, and what did not¶
Before v0.3.1.210, a process segmented by a
FilterManager absorbed a failed filter: the error was
logged, the loop moved to the next filter, and run() returned normally. An
orchestrator chaining waves saw a success. Separately, a defect made the
FAILED status impossible to write, so the follow-up table showed only
RUNNING and COMPLETED and gave no way to reconstruct what had happened.
Both are fixed. Two things now happen without you opting in:
- a failed filter is recorded in the follow-up table with a
FAILEDstatus carrying the first line of the Teradata error; - an ingestion that stages zero rows is recorded as
COMPLETED,0 rowsrather thanCOMPLETED.
Default control flow is unchanged. on_failure defaults to 'continue',
which behaves exactly as before, and the upstream guard is off by default.
Upgrading does not change how your chains run.
Choosing a failure policy¶
run(), upload_features() and roll_out() accept on_failure:
| Policy | Behaviour |
|---|---|
'continue' (default) |
Every filter is attempted, each failure is recorded, a summary is logged. No exception — the historical behaviour. |
'stop' |
The loop stops at the first failed filter, records it, and raises. |
'raise' |
Every filter is attempted and recorded — so the trace is complete — then a single exception is raised at the end. |
import tdfs4ds
# Make a partial ingestion failure visible to the caller.
tdfs4ds.run(process_id, on_failure='raise')
raise_on_failure=True is accepted as a shorthand for on_failure='raise'. If
you pass both and they disagree, the explicit on_failure wins and a warning is
logged.
The exception is a FeatureIngestionError and carries the detail as data, so an
orchestrator never has to parse a message:
try:
tdfs4ds.run(process_id, on_failure='raise')
except tdfs4ds.FeatureIngestionError as e:
print(e.process_id, e.n_failed, "of", e.n_total, "filters failed")
for f in e.failures:
print(" filter", f["filter_id"], "->", f["error"])
A process without a FilterManager always raises
on_failure governs what happens to the run when some of its filters
failed — a question that only exists when the work is segmented. An
unsegmented ingestion has no next filter to move to, so it propagates its
error regardless of the policy.
Stopping a failure from contaminating the levels above¶
A failed upstream does not make a downstream view fail. The view still compiles
and still returns rows — empty or stale ones. The downstream process ingests
them, succeeds, and records its own COMPLETED, which the skip logic then
treats as final.
check_upstream=True refuses to compute when an upstream process has failed at
the current valid time:
Nothing is computed and no follow-up row is written. The log names the failing
upstream processes and states both ways forward: fix the upstream and re-run,
or proceed deliberately with check_upstream=False.
The test is "no upstream failed", not "every upstream completed" — a weekly
reference process feeding a daily one legitimately has no row for today. Pass
strict_upstream=True to also refuse when an upstream is still RUNNING.
You can ask the same question without running anything:
verdict = tdfs4ds.check_upstream_health(process_id)
print(verdict["ok"], verdict["verified"])
print(verdict["explanation"])
This guard needs the lineage graph¶
It answers by reading the persisted lineage graph, so it is only as good as that
graph. It reports ok and verified separately, because "no problem found" and
"nothing could be checked" are different answers:
| Situation | ok |
verified |
What you are told |
|---|---|---|---|
| Graph empty or unreadable | True |
False |
run rebuild_lineage_graph(all_data_domains=True) |
| Process absent from the graph | True |
False |
run rebuild_lineage_graph(objects=['<view>'], check=True) |
| Process known, no upstream | True |
True |
it reads source objects only — nothing to check |
| Upstream failed | False |
True |
the run is refused, with the failing processes named |
The first two cases do not block — missing information is not evidence of a problem — but they are never silent, because reporting "nothing to check" would read as approval from a check that never ran.
Refresh lineage as part of normal operations
If you rely on check_upstream, treat
rebuild_lineage_graph() as routine
maintenance, alongside documentation. A process registered after the last
refresh is not yet protected.
Orchestrating waves¶
The script produced by
generate_execution_script takes its own
wave-level wave_on_failure:
| Policy | Behaviour |
|---|---|
'skip_descendants' (default) |
Skips only the failed process's downstream closure; unrelated branches still run. |
'continue' |
Runs every wave, reports failures at the end. |
'stop_after_wave' |
Finishes the current wave, then stops. |
'raise' |
Same, ending with a RuntimeError. |
plan = tdfs4ds.analyze_parallel_plan()
path = tdfs4ds.write_execution_script(plan, wave_on_failure='skip_descendants')
The generated worker runs each process with on_failure='raise', since a wave
policy can only react to failures it is told about.
Two settings, two names
on_failure applies to one process (continue / stop / raise);
wave_on_failure applies to a plan of waves (skip_descendants /
continue / stop_after_wave / raise). The value sets overlap without
agreeing, so they are deliberately not the same parameter name.
The script exits non-zero when anything failed, and prints which processes were skipped and why.
Recovering from a contaminated run¶
Re-running a fixed process replays only the filters that never completed —
the ones that succeeded keep their features and their COMPLETED rows.
But re-running does not repair the levels above it. Those processes recorded
a genuine COMPLETED while computing on empty inputs, so they are skipped on
the next pass and keep their wrong values. To correct them, either re-run them
with force_compute=True, or delete their follow-up rows for that valid time.
Processes that ingested nothing are the exception: they carry
COMPLETED,0 rows, which the skip test does not match, so they recompute on
their own.
Reading the follow-up table¶
STATUS |
Meaning |
|---|---|
COMPLETED |
Succeeded and wrote rows. |
COMPLETED,0 rows |
Succeeded but staged nothing; will recompute next pass. |
FAILED,<error> |
Failed, with the first line of the Teradata error. |
RUNNING |
Either executing now, or stopped without recording an outcome. |