Error Handling
Scalar UDFs can also set @geneva.udf(timeout=...) to enforce a per-row timeout in
seconds. Timeout failures surface as ordinary TimeoutError, so existing
on_error, retry, and skip_on_error() behavior applies unchanged.
This timeout support is scalar-only and uses Unix SIGALRM / setitimer
semantics at runtime. It also requires execution on the worker process main
thread. It works inside MultiProcessBatchApplier worker subprocesses because
each child executes batches on its own main thread, but it still uses
process-global signal state. UDFs or libraries that install their own
SIGALRM handlers/timers, or that depend on main-thread signal behavior while
coordinating work in background threads, may be incompatible.
Fatal Worker Errors
UDF backfill also classifies fatal worker loss into public Geneva exception types:
FatalWorkerTransientErrorFatalWorkerOOMErrorFatalWorkerCrashErrorFatalWorkerExitError
By default, fatal worker errors use a driver-side retry-then-skip policy. The current unfinished task is attempted up to 3 times in total. If those attempts are exhausted, Geneva bisects the task until it isolates each failing row, writes NULL only for those rows, and continues the job. The bisected children retain the exhausted attempt count rather than starting a new retry budget. When error logging is enabled, Geneva records one error per failed row, and filtered backfills preserve successful selected values as well as existing values outside the filter. This driver-only default does not change the fail-fast behavior of ordinary UDF exceptions.
The retry rule matches FatalWorkerError, so newly introduced fatal-worker
subclasses receive the default recovery behavior without an allowlist update.
The exceptions are: FatalWorkerCrashError skips retry but still uses row
isolation; FatalWorkerOOMError keeps the separate shrinking recovery described
below; and deterministic integrity failures (ShortFragmentWriteError,
CorruptCheckpointError, CheckpointCoverageError, and
MergeFallbackTargetError) remain job-fatal by default.
FatalWorkerOOMError is also recovered by default: the unfinished part of the
failing task is split into balanced tasks and resubmitted, so each retry runs
with a smaller peak memory footprint. The target fanout follows the current
ActorPool submission capacity, with a minimum of 2 and an unfinished-row cap:
2 below 1,000 rows, 10 from 1,000 through 999,999 rows, and 100 from 1,000,000
rows onward. A saturated pool therefore starts with a binary split, while idle
actors can consume a wider long-tail split without creating unnecessarily tiny
tasks. The pipeline refills all available actor slots after a split, while
keeping at most one task queued behind the actors. Retries that strictly shrink
every unfinished window are progress and proceed freely. A non-shrinking
replacement for a multi-row task is rejected immediately, while a retry with
no uncovered replacement is charged against the bounded retry budget. Once a
task reaches a single-row window and can no longer shrink, each retry is also
charged against the job-level budget (default: 10 such retries per job, 3 for
the same exact range), and exhausting it fails the job fast with an "OOM
recovery budget exceeded" error. The budget is configured through
geneva_oom_recovery_budget settings (enabled,
max_total_oom_recoveries, max_same_range_oom_recoveries, and the optional
fixed target_split_fanout override). Setting enabled=false restores the
previous fail-fast behavior for unhandled OOMs.
If the user provides on_error and does not explicitly match a fatal
worker-loss type, Geneva still applies the corresponding default behavior. If
the user does explicitly match one, the user configuration wins for that
error type; for example, Fail(...) restores immediate job failure. Explicit
fail-fast policies such as fail_fast() or an empty matcher list also restore
immediate job failure for every exception.
Examples:
from geneva import Retry, Fail, udf
from geneva.errors import FatalWorkerTransientError
@udf(
data_type=pa.int64(),
on_error=[Retry(FatalWorkerTransientError, max_attempts=5)],
)
def retry_infra_failures(x: int) -> int:
return x * 2
@udf(
data_type=pa.int64(),
on_error=[Fail(FatalWorkerTransientError)],
)
def fail_on_infra_failures(x: int) -> int:
return x * 2
Exception Matchers
geneva.debug.error_store.Retry
Bases: ExceptionMatcher
Retry on matching exceptions with backoff
Parameters:
-
*exceptions(type[Exception], default:()) –Exception types to match
-
match(str, default:None) –Regex pattern to match in exception message. Simple strings work as substring matches (e.g., "rate limit"). Use (?i) for case-insensitive matching.
-
max_attempts(int, default:3) –Maximum number of attempts (default: 3)
-
backoff(str, default:'exponential') –Backoff strategy: "exponential" (default), "fixed", or "linear"
Examples:
Retry(ConnectionError, TimeoutError, max_attempts=3)
Retry(ValueError, match="rate limit", max_attempts=5)
Retry(APIError, match=r"429|rate.?limit")
Retry(APIError, match=r"(?i)rate limit") # case-insensitive
geneva.debug.error_store.Skip
Bases: ExceptionMatcher
Skip row (return None) on matching exceptions
Parameters:
-
*exceptions(type[Exception], default:()) –Exception types to match
-
match(str, default:None) –Regex pattern to match in exception message
-
max_skip_count(int, default:None) –Maximum number of rows that can be skipped before failing the job. Only used when passed via
skip_on_error(). -
max_skip_fraction(float, default:None) –Maximum fraction of rows (0.0-1.0) that can be skipped before failing the job. Only used when passed via
skip_on_error().
Examples:
Skip(ValueError, KeyError)
Skip(ValueError, match="invalid input")
geneva.debug.error_store.Fail
Bases: ExceptionMatcher
Fail job immediately on matching exceptions
Parameters:
-
*exceptions(type[Exception], default:()) –Exception types to match
-
match(str, default:None) –Regex pattern to match in exception message
Examples:
Fail(AuthError)
Fail(ValueError, match="fatal")
Helper Functions
geneva.debug.error_store.retry_transient
Retry transient network errors (ConnectionError, TimeoutError, OSError).
Parameters:
-
max_attempts(int, default:3) –Maximum number of attempts (default: 3)
-
backoff(str, default:'exponential') –Backoff strategy: "exponential" (default), "fixed", or "linear"
Returns:
-
list[ExceptionMatcher]–Matcher list for use with on_error parameter
Examples:
@udf(data_type=pa.int32(), on_error=retry_transient())
@udf(data_type=pa.int32(), on_error=retry_transient(max_attempts=5))
geneva.debug.error_store.retry_all
Retry any exception.
Parameters:
-
max_attempts(int, default:3) –Maximum number of attempts (default: 3)
-
backoff(str, default:'exponential') –Backoff strategy: "exponential" (default), "fixed", or "linear"
Returns:
-
list[ExceptionMatcher]–Matcher list for use with on_error parameter
Examples:
@udf(data_type=pa.int32(), on_error=retry_all())
@udf(data_type=pa.int32(), on_error=retry_all(max_attempts=5))
geneva.debug.error_store.skip_on_error
skip_on_error(
max_skip_count: int | None = None,
max_skip_fraction: float | None = None,
) -> list[ExceptionMatcher]
Skip (return None) for any exception.
Parameters:
-
max_skip_count(int, default:None) –Maximum number of rows that can be skipped before the job fails. If both
max_skip_countandmax_skip_fractionare set, whichever threshold is hit first triggers failure. -
max_skip_fraction(float, default:None) –Maximum fraction of rows (0.0-1.0) that can be skipped before the job fails. The fraction is computed as
skipped / total_processed_so_far.
Returns:
-
list[ExceptionMatcher]–Matcher list for use with on_error parameter
Examples:
@udf(data_type=pa.int32(), on_error=skip_on_error())
@udf(data_type=pa.int32(), on_error=skip_on_error(max_skip_count=100))
@udf(data_type=pa.int32(), on_error=skip_on_error(max_skip_fraction=0.05))
@udf(
data_type=pa.int32(),
on_error=skip_on_error(max_skip_count=100, max_skip_fraction=0.05),
)
geneva.debug.error_store.fail_fast
Fail immediately on any exception (default behavior).
Returns:
-
list[ExceptionMatcher]–Empty matcher list (no special handling)
Examples:
@udf(data_type=pa.int32(), on_error=fail_fast())
Configuration
geneva.debug.error_store.ErrorHandlingConfig
Configuration for UDF error handling behavior
geneva.debug.error_store.UDFRetryConfig
Retry configuration for UDF execution using tenacity semantics
retry_transient
retry_transient(max_attempts: int = 3) -> UDFRetryConfig
Retry common transient errors (network, timeouts)
Parameters:
-
max_attempts(int, default:3) –Maximum number of attempts including the initial try
geneva.debug.error_store.ErrorRecord
UDF execution error record, stored in geneva_errors table
input_columns
output_columns
timestamp
timestamp: datetime = field(
factory=dt_now_utc,
metadata={"pa_type": timestamp("us", tz="UTC")},
)