Skip to content

Failure classification

What counts as a failure is a separate concern from when to trip (thresholds, in Config). It is decided by a FailureClassifier.

Default policy

By default, a call is a failure exactly when it raises, and any returned value is a success:

from interlock import CircuitBreaker

breaker = CircuitBreaker(name='svc')   # DefaultFailureClassifier

This is right for code that signals errors by raising. It is not enough when failure is encoded in a return value — for example an HTTP response object whose 503 status means the dependency is unhealthy.

Classify by result

A classifier implements one method. The result/exception pair is mutually exclusive: when exception is not None the call raised; otherwise result holds the return value.

from interlock import CircuitBreaker

class StatusClassifier:
    def is_failure(self, *, result: object, exception: BaseException | None) -> bool:
        if exception is not None:
            return True
        return getattr(result, 'status_code', 200) >= 500

breaker = CircuitBreaker(name='api', classifier=StatusClassifier())
result = breaker.call(client.get, url)   # a 503 response now counts as a failure

Result-based classification needs the return value, so it works with the decorator and call, but not the context manager (which only sees exceptions and duration).

Ignore expected errors

Business errors — a 404, a validation failure — should not open the circuit. Encode that by treating only the exceptions you care about as failures:

class IgnoreNotFound:
    def is_failure(self, *, result: object, exception: BaseException | None) -> bool:
        if isinstance(exception, NotFoundError):
            return False          # expected, not a dependency problem
        return exception is not None

HTTP out of the box

For httpx2, you do not need to write this yourself — the httpx2 integration ships HttpStatusClassifier, which treats transport exceptions and the canonical retryable statuses (429, 500, 502, 503, 504) as failures.