Skip to content

fix: reader survives raising apply-observer callbacks and transient catch-up unassignment#34

Merged
ryan-yuuu merged 3 commits into
mainfrom
fix/reader-resilience
Jul 4, 2026
Merged

fix: reader survives raising apply-observer callbacks and transient catch-up unassignment#34
ryan-yuuu merged 3 commits into
mainfrom
fix/reader-resilience

Conversation

@ryan-yuuu

Copy link
Copy Markdown
Owner

Summary

Follow-up to the #33 investigation. Empirically, a broker disconnect does not kill the reader — getmany() returns empty and aiokafka retries transport failures (verified against a real broker on a confirmed-silent port and against aiokafka 0.14.0 source). But the underlying "any exception out of the reader loop → permanent failed, no recovery" fragility is real. This PR closes the two realistic in-loop exception sources.

Fix #1 — a raising apply-observer callback no longer kills the reader

on_set/on_delete were invoked unguarded in _apply, so any exception a callback threw latched the table failed for its whole lifetime. Now a raise is logged (with the record key + traceback) and counted via the new ViewStats.callback_errors; the base table still applies the record.

  • Reader liveness is KafkaTable's contract; a derived view's integrity is the view's own responsibility (compute-then-commit — which GroupedKafkaTable's hooks already do, so this is purely an external-callback backstop).
  • except Exception only — BaseException/cancellation still propagates.

Fix #3 — catch-up gate tolerates a transient IllegalStateError

Under group_id=None, a metadata blip (e.g. LeaderNotAvailable during a leader election) can momentarily empty the assignment, making position() raise IllegalStateError. The barrier() sweep already guards this; the catch-up gate did not, so a self-healing blip mid-catch-up was fatal. Now it defers the gate (mirrors barrier()) and latches only on a complete successful read.

  • A non-IllegalStateError still propagates — a real fault must die, not spin (pinned by a boundary test).
  • A permanent unassignment (e.g. topic deleted) defers → catchup_timeoutdegraded (recoverable) rather than failed — same trade-off barrier() already makes.

Out of scope

  • Reader supervisor / restart() (would recover from any death, incl. non-retriable fetch errors like auth).
  • Log-rate throttling (the decoder guards share the same unbounded shape; a separate cross-cutting change).

Tests

TDD throughout. Full suite 252 passed (integration included), 100% branch coverage on kafka_table.py and grouped_table.py. New coverage: on_set/on_delete survival + counter + key-in-log + BaseException boundary; catch-up transient-blip survival, no-premature-latch, and the non-IllegalStateError boundary.

Refs #33.

…atch-up unassignment

Removes two independent permanent-death paths in KafkaTable._run that are not
broker disconnects (disconnects are retried and survived — verified empirically):

- A raising on_set/on_delete apply-observer callback is now logged (with the
  record key + traceback) and counted via ViewStats.callback_errors instead of
  killing the reader forever; the base table still applies the record. Reader
  liveness is the table's contract; observer integrity stays the observer's
  (compute-then-commit, as GroupedKafkaTable's own hooks already do).

- The catch-up gate's position() read now tolerates a transient IllegalStateError
  from a metadata-blip assignment shrink under group_id=None, mirroring the
  barrier sweep's existing guard. A self-healing blip defers the gate rather than
  latching the reader as failed; a non-IllegalStateError still propagates, and a
  permanent unassignment degrades (via catchup_timeout) rather than failing.

Also syncs uv.lock's ktables version to 1.1.1 (stale since the 1.1.1 release).

100% branch coverage retained. Refs #33.
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  ktables
  grouped_table.py
  kafka_table.py
Project Total  

This report was generated by python-coverage-comment-action

ryan-yuuu added 2 commits July 3, 2026 18:35
…up blip

Round-2 review follow-ups (no code-behavior change; tests + comment polish):

- Add test_permanent_position_unavailability_degrades_not_fails: a permanent
  catch-up IllegalStateError (e.g. topic deleted mid-catch-up) defers to
  catchup_timeout -> degraded, never failed. The decision was documented but
  untested; guards against a future narrowing that would fail or hot-spin.

- Add test_catchup_defers_whole_gate_on_single_partition_blip: locks the
  all-or-nothing catch-up gate semantic (one partition blipping defers the whole
  gate), deliberately distinct from barrier()'s per-tp skip. Adds a blip_partition
  param to the _fake_consumer helper.

- Comment polish: retarget the ViewStats :meth: cross-ref to _notify_observer
  (where the logged/counted logic lives); drop gitignored spec-section refs from
  test comments; pin the timing invariant on the no-latch test.

Full suite 254 passed, 100% branch coverage retained.
@ryan-yuuu ryan-yuuu merged commit cf3b040 into main Jul 4, 2026
7 checks passed
@ryan-yuuu ryan-yuuu deleted the fix/reader-resilience branch July 4, 2026 01:48
ryan-yuuu pushed a commit that referenced this pull request Jul 4, 2026
🤖 I have created a release *beep* *boop*
---


## [1.1.2](v1.1.1...v1.1.2)
(2026-07-04)


### Bug Fixes

* reader survives raising apply-observer callbacks and transient
catch-up unassignment
([#34](#34))
([cf3b040](cf3b040))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant