Skip to content

KafkaTable reader task dies permanently on broker disconnect (never reconnects/recovers) #33

Description

@ryan-yuuu

Summary

A KafkaTable's reader task dies permanently when the broker connection is lost in a way that raises out of the fetch loop. Once the reader task exits with an exception, the table is latched into a terminal failed / frozen state for the lifetime of the object — it never reconnects or recovers, even after the broker comes back and the topic is fully healthy. The only remedy is to construct a brand-new table.

Mechanism

The reader loop KafkaTable._run (kafka_table.py) is:

while True:
    batches = await consumer.getmany(timeout_ms=self._poll_timeout_ms)
    ...
    # (position()/end_offsets() calls for the catch-up gate and barriers)

There is no reconnect/retry wrapper around the loop. The only in-loop exception that is caught is IllegalStateError from position() (a transient unassignment). Any other exception escaping getmany() / position() / end_offsets() — i.e. the fatal aiokafka errors a broker disconnect can surface — propagates out of the task.

_on_reader_done (the task's done-callback) then latches the failure permanently:

def _on_reader_done(self, task):
    if task.cancelled():
        return
    exc = task.exception()
    if exc is not None:
        self._failure = exc
        self._failed.set()
        logger.error("KafkaTable reader for topic=%s DIED (...); table is FROZEN at last applied state (... status=failed)", ...)

After this:

  • failure stays set forever (there is no code path that clears it or re-spawns the reader task);
  • reads serve the frozen last-applied snapshot;
  • wait_until_caught_up() / barrier() return False (they short-circuit on self._failure);
  • status is permanently "failed".

Nothing restarts the reader when the broker returns, so a transient outage becomes a permanent death.

The in-loop comment states the intent — "transient outages don't raise — getmany returns empty" — but in practice a broker disconnect (broker restart, network partition, host sleep/resume) can raise a fatal error out of the fetch/position calls, and that path is terminal.

Reproduction

  1. Start a broker and create a topic.
  2. Construct a KafkaTable over that topic and await table.start() — let it catch up (is_caught_up / status == "caught_up").
  3. Sever the broker connection in a way that raises out of the loop, not a clean idle: kill/restart the broker process, or drop the network (e.g. a laptop sleep/resume), while the reader is live.
  4. Observe the reader task exit with an exception and _on_reader_done log ... DIED ...; table is FROZEN ... status=failed.
  5. Bring the broker back and let the topic become fully reachable again.

Also observed in the wild: a long-lived reader left running for hours across network-loss / sleep cycles reaches the same terminal state.

Failing case (observed)

  • After step 5, with the broker and topic healthy again, the table stays dead: failure is still set, status == "failed", wait_until_caught_up() / barrier() keep returning False, and reads serve stale (frozen) data indefinitely.
  • A freshly constructed KafkaTable over the same topic and broker catches up and reads correctly — proving the broker, topic, and data are healthy and the fault is confined to the dead reader object.
  • No reconnection or self-recovery ever occurs for the lifetime of the process holding that table.

Downstream impact: consumers that cache a single long-lived reader inherit this permanent death. For example, calfkit's client.mesh (ControlPlaneView over a KafkaTable) surfaces it as a permanent MeshUnavailableError(reason="reader_dead") that only a process restart clears.

Environment

  • ktables 1.1.1
  • aiokafka 0.13.0
  • Python 3.12
  • First observed 2026-07-03.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions