Lucas Brutschy created KAFKA-20748:
--------------------------------------

             Summary: StoredDescriptionTopologyEpoch can diverge from the 
topology-description plugin across failures and races
                 Key: KAFKA-20748
                 URL: https://issues.apache.org/jira/browse/KAFKA-20748
             Project: Kafka
          Issue Type: Sub-task
            Reporter: Lucas Brutschy


The broker tracks which topology a Streams group's plugin entry holds in 
StoredDescriptionTopologyEpoch, and uses that belief to decide whether to 
solicit a fresh push from the client (TopologyDescriptionRequired), whether the 
group's plugin entry still needs deleteTopology, and what to serve from the 
describe API. The plugin call (setTopology/deleteTopology) and the metadata 
write that records the epoch are two separate, non-atomic steps — an external 
side effect followed by a committed group-metadata record. A crash or error 
between the two steps, or a client push that races a concurrent delete, can 
leave the broker's belief and the plugin permanently disagreeing in one of two 
ways.

In the first, a leak: the plugin holds a topology the broker believes is absent 
(for example, setTopology succeeds but the follow-up epoch write fails), so no 
cleanup path ever reclaims it and the entry lingers indefinitely. In the 
second, a loss: the plugin holds nothing but the broker believes a topology is 
stored (for example, a delete removes the plugin entry but the clearing write 
fails, or a delete races a push), so the broker never re-solicits and the 
describe API reports NOT_STORED for a group that should be able to recover.

This affects every operation that mutates the plugin: a client topology push, 
the periodic cleanup that deletes entries for naturally expired empty groups, 
an explicit DeleteGroups, and the conversion of an empty Streams group to a 
classic group on a classic JoinGroup (which tombstones the Streams metadata and 
would orphan the plugin entry with no cleanup at all). The cleanup path 
additionally has no ordering guarantee between deleteTopology and a concurrent 
setTopology, so a member that rejoins and re-pushes while a delete is in flight 
can have its topology silently removed.

*Suggested fix*

A fix was added to the KIP and needs to be implemented as well. The idea is to 
make StoredDescriptionTopologyEpoch three-valued by adding a -2 "uncertain" 
marker alongside the existing real epoch (>= 0, the plugin definitely holds 
that topology) and -1 (the plugin definitely holds nothing). Uncertain means 
the plugin may or may not hold a topology; it is read like -1 for the 
solicitation decision (so the broker re-solicits) and like a real epoch for the 
deletion decision (so the group stays reclaimable). That combination is what 
makes a half-finished operation self-healing rather than stuck: a group left 
uncertain both asks the client to re-push and remains eligible for cleanup.

Every operation that is about to disturb the plugin first commits -2 durably 
and only then calls the plugin; on success it writes the final value (the 
pushed epoch after a push, -1 after a delete). Any failure after the -2 commit 
leaves the group uncertain, which resolves on its own — the client re-pushes on 
its next heartbeat, or the periodic cleanup re-attempts the delete. This 
barrier is applied to all four operations above; for the classic-JoinGroup 
conversion the plugin delete runs before the group is converted, and on plugin 
failure the join is rejected with a retriable error so the group stays a 
Streams group at -2 rather than being converted with an orphaned entry.

The cleanup path also closes the delete-vs-push race with a smart finalize: 
after a successful delete it re-checks the stored epoch; if it is still -2 it 
clears to -1, but if a racing push advanced it to a real epoch it writes -2 
back to force a re-solicit, since the pushed topology may have been removed by 
the delete. The only residual cost is one redundant push when the raced 
topology actually survived.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to