Skip to content

Message Brokers

A broker is a data-engineering boundary like a database or an object store, and it fails in ways specific to it: a publish is rejected, a consumer cannot reach its group, an offset is never committed.

Mapping those onto ServiceConnectionError and OperationTimeoutError catches the failure but loses what a pipeline needs in order to act on it — which operation failed, and the topic, partition, offset and consumer group it failed at.

MessageBrokerError
├── BrokerConnectionError
├── BrokerTimeoutError
├── MessagePublishError
├── MessageConsumeError
└── MessageAcknowledgementError

The hierarchy is about the operation, not the product. Kafka, RabbitMQ, Pulsar and NATS disagree about almost everything else, but all four connect, publish, consume and acknowledge — so application code catches these without naming a client library, and DataExcept depends on none of them.

Publishing

from dataexcept import MessagePublishError

try:
    producer.produce(topic="orders", value=payload)
    producer.flush()
except KafkaException as exc:
    raise MessagePublishError(
        "orders",
        broker=bootstrap_servers,
        partition=partition,
        cause=exc,
    ) from exc

Renders as Failed to publish to topic 'orders', partition 3 on broker 'kafka:9092': Local: Broker transport failure.

Consuming

from dataexcept import MessageConsumeError

record = consumer.poll(timeout=1.0)
if record is not None and record.error():
    raise MessageConsumeError(
        record.topic(),
        broker=bootstrap_servers,
        partition=record.partition(),
        offset=record.offset(),
        consumer_group=group_id,
        cause=KafkaException(record.error()),
    )

The coordinates are what make the failure actionable: the same topic failing at one partition is a very different incident from the same topic failing at all of them.

Acknowledging

from dataexcept import MessageAcknowledgementError

try:
    consumer.commit(message=record, asynchronous=False)
except KafkaException as exc:
    raise MessageAcknowledgementError(
        record.topic(),
        partition=record.partition(),
        offset=record.offset(),
        consumer_group=group_id,
        cause=exc,
    ) from exc

This is deliberately not a MessageConsumeError. The message was read and processed; it is the record of that which did not stick, so the broker will deliver it again. A handler that retries a failed consume and one that reasons about duplicate delivery are not the same handler.

Connecting and timing out

from dataexcept import BrokerConnectionError, BrokerTimeoutError

raise BrokerConnectionError(bootstrap_servers, cause=exc)
raise BrokerTimeoutError(bootstrap_servers, "publish", 30.0, cause=exc)

A bootstrap address is a URL often enough to matter, so broker is redacted when it is one and left alone when it is a plain host:port:

BrokerConnectionError("kafka://svc:hunter2@broker.internal:9092")
# broker == "kafka://***:***@broker.internal:9092"

Retryability is not guessed

Every one of these leaves failure_kind at unknown and retryable at None. A broker refusing a publish may be a leader election that resolves in a second, or a topic that does not exist — and the exception cannot tell which.

An integration that does know, because it read the broker's own error code, says so:

from dataexcept import FailureMetadata, MessagePublishError

raise MessagePublishError("orders", cause=exc).with_failure_metadata(
    FailureMetadata(failure_kind="transient", retryable=True, retry_after_seconds=5)
)

That travels through the envelope and the Pino projection like any other failure metadata, so a consumer downstream can back off on the broker's own advice rather than on a guess made three services away. See Failure Metadata for how those values are chosen.

Catching

from dataexcept import DataExceptError, MessageBrokerError, MessageConsumeError

try:
    run_consumer()
except MessageConsumeError:
    ...              # exactly this failure
except MessageBrokerError:
    ...              # any other broker failure -- not a database or HTTP one
except DataExceptError:
    ...              # anything else this library raises

MessageBrokerError is a domain root in its own right, so it catches broker failures and nothing else. New broker exceptions may appear under it in a minor release, as the stability policy describes.