DUE TO SPAM, SIGN-UP IS DISABLED. Goto Selfserve wiki signup and request an account.
...
| Code Block | ||||
|---|---|---|---|---|
| ||||
package org.apache.kafka.coordinator.group.api.streams;
public class StreamsGroupTopologyDescription {
public Collection<Subtopology> subtopologies();
public Collection<GlobalStore> globalStores();
public static class Subtopology {
public String id();
public Collection<Node> nodes();
}
/**
* A processing node in the topology. Predecessor nodes can be inferred from successor relation.
*/
public interface Node {
String name();
Set<String> successors();
}
public static class Source implements Node {
public Set<String> topics();
}
public static class Processor implements Node {
public Set<String> stores();
}
public static class Sink implements Node {
public Optional<String> topic();
}
public static class GlobalStore {
public Source source();
public Processor processor();
}
} |
Broker-Side Persistence
Two new tagged fields are added to the persisted StreamsGroupMetadataValue record (a broker-internal record written to __consumer_offsets, not a wire-level change visible to clients):
...
Exit code. The command exits 0 for AVAILABLE. It exits 1 for NOT_STORED, for ERROR, and when the DescribedGroup.ErrorCode is non-zero (authorization failure, group not found, coordinator unavailable, etc.). NOT_REQUESTED does not occur for this command because it always sets IncludeTopologyDescription=true.
Proposed Changes
End-to-end Flow
The sequence below traces the four user-visible interactions — a successful push, a describe, an explicit DeleteGroups, and broker-driven cleanup of a naturally-expired group. Plugin calls return CompletableFutures; the broker awaits them asynchronously.
Broker Side
- After a successful
StreamsGroupHeartbeat, the broker decides whether to setTopologyDescriptionRequired=truepurely from the group's persisted state — no plugin RPC is involved.STALE_TOPOLOGYmembers are skipped. For all other members, the broker sets the flag iff:StoredTopologyEpoch != currentTopologyEpochANDLastFailedTopologyEpoch != currentTopologyEpochAND no per-group transient-failure back-off is currently in its window. The back-off is in-memory state on the service (keyed bygroupId, carryingtopologyEpoch+nextAttemptMs), armed when a transientsetTopologyfailure is observed and doubled per consecutive failure starting at 30 s and capped at 1 h; it is cleared on a successful push, on a permanent failure (becauseLastFailedTopologyEpochratchets the same epoch), and implicitly on any topology-epoch advance (a stale entry for an older epoch is ignored). A service-side in-flight tracker (pergroupId, default 30 s) is additionally used to prevent multiple concurrent heartbeats from each setting the flag. - On
UpdateStreamsGroupTopologyDescription, the broker checks theREADACL on the group and that a plugin is configured. The broker validates theMemberIdagainst the streams group: an emptyMemberIdis rejected withINVALID_REQUEST, aMemberIdthat does not match any current member of the group is rejected withUNKNOWN_MEMBER_ID, and a request whose group ID does not name an existing streams group is also rejected withUNKNOWN_MEMBER_ID(the group-deleted case is observationally identical to a member fence from the client's point of view, and is handled by the same rejoin path). The broker does not enforce a size limit on the topology description — it is the plugin's responsibility to decide what size it is willing to store. The broker callssetTopologyon the plugin. On success the broker writes a metadata record settingStoredTopologyEpoch = pushedEpoch, and the response carriesNONE. OnInvalidRequestException(mapped toINVALID_REQUEST) orTopologyDescriptionTooLargeException(mapped toTOPOLOGY_DESCRIPTION_TOO_LARGE), the broker writes a metadata record settingLastFailedTopologyEpoch = pushedEpochso subsequent heartbeats at the same topology epoch do not re-solicit. Any other exception maps toTOPOLOGY_DESCRIPTION_UPDATE_FAILED, is logged at WARN, and is treated as transient — no metadata record is written, and the next heartbeat re-solicits. If the plugin call succeeds but the subsequent metadata-record write fails, the broker accepts the drift: the next heartbeat seesStoredTopologyEpoch < currentTopologyEpoch, re-solicits, the client re-pushes the identical payload, the plugin's idempotentsetTopologyis invoked again, and the metadata-record write is retried — closing the gap. - On
DeleteGroups, the broker callsdeleteTopologyon the plugin before writing the group tombstone, for each requested streams group that hasStoredTopologyEpoch != -1.deleteTopologyfailures are logged but do not affect the deletion response. The group is then tombstoned regardless of the per-group plugin outcome. This ordering matches the broker-driven natural-expiration cleanup (next bullet), where the plugin is also called before the group is tombstoned. - On
StreamsGroupDescribewithIncludeTopologyDescription=true, the broker callsgetTopologyon the plugin only whenStoredTopologyEpoch == currentTopologyEpochfor that group; otherwise it reportsNOT_STOREDwithout making a plugin call. Calls across groups run in parallel. Authorization is unchanged — the existingDESCRIBEACL on the GROUP resource covers the topology description.DescribedGroup.ErrorCodeis never modified by topology-related outcomes; theTopologyDescriptionStatusfield carries the reason whenTopologyDescriptionis null (NOT_REQUESTED,NOT_STORED, orERROR; the last is also logged at WARN). When no plugin is configured, the broker returnsNOT_STORED. IfgetTopologyreturnsnullwhile the broker believed the description was stored (plugin data-loss), the broker schedules a fire-and-forget write resettingStoredTopologyEpoch = -1so the next heartbeat re-solicits a fresh push. - The When a plugin is configured, the broker runs a periodic topology-description cleanup every every
offsets.retention.check.interval.ms. Each fire fans out a read-only query across the broker's hosted__consumer_offsetspartitions to identify streams groups eligible for cleanup:isEmpty && allOffsetsExpired && StoredTopologyEpoch != -1. This is the same eligibility predicate the shard's offset-expiration sweep uses to delete consumer/share groups, with the additionalStoredTopologyEpoch != -1filter. For each eligible group, the broker callsplugin.deleteTopology(groupId)and, on success, writes a metadata record settingStoredTopologyEpoch = -1. Plugin failures leave the field set; the same group is retried on the next cycle. OnceStoredTopologyEpoch = -1, the shard's offset-expiration sweep tombstones the (now flag-cleared) group on a subsequent cycle.
Client Side
- When no plugin is configured, the periodic cleanup does not run and the shard's offset-expiration sweep expires streams groups normally, ignoring
StoredTopologyEpoch; operators that disable a previously-configured plugin are responsible for cleaning up plugin-side state out-of-band.
Client Side
- At startup, if
topology.description.push.enabled=true, the Streams client converts the The Streams client records the
TopologyDescriptionRequiredflag from each heartbeat response.- At startup, if
topology.description.push.enabled=true, the Streams client converts the topology returned byTopology#describe()to the wire format and stores it internally. The mapping is one-to-one with the RPC schema; predecessor edges are not sent on the wire and the read side reconstructs them by inverting each node's successor list. Whentopology.description.push.enabled=false, no description is stored and the feature is disabled on this client. - The Streams client records the
TopologyDescriptionRequiredflag from each heartbeat response. - On each consumer background-thread poll, the client sends sends
UpdateStreamsGroupTopologyDescriptionto the coordinator when a coordinator is known, the flag is set, a stored topology description is available, the client has a non-empty member ID assigned by the coordinator, and no prior request is in flight. The member ID populated on the request is the same one carried onStreamsGroupHeartbeat. Completion handling is described in Error Handling and Retries. The push runs on the consumer background thread and never blocks user-facing Kafka Streams APIs
...
Topology Translation
TopologyDescription from Kafka Streams maps one-to-one to the wire types in the RPC schema. Predecessor edges are not sent on the wire; the read side reconstructs them by inverting each node's successor list.
Error Handling and Retries
- ; the push is best-effort
...
- .
- Completion handling on the push response is keyed on the error code.
NOT_COORDINATORandCOORDINATOR_NOT_AVAILABLEtrigger coordinator rediscovery
...
- and leave the flag
...
- set.
COORDINATOR_LOAD_IN_PROGRESSand network exceptions leave the flag set for retry on the next poll.
...
UNKNOWN_MEMBER_IDmeans the broker no longer recognizes this member (group deleted, or member dropped): the client clears the flag and relies on the existing membership-management path to trigger a clean rejoin on the next heartbeat. All other errors (TOPOLOGY_DESCRIPTION_TOO_LARGE,TOPOLOGY_DESCRIPTION_UPDATE_FAILED,INVALID_REQUEST,UNSUPPORTED_VERSION,GROUP_ID_NOT_FOUND,GROUP_AUTHORIZATION_FAILED) clear the
...
- flag and log at WARN
...
- ; the client does not retry on its own
...
- , and a re-
...
- attempt happens only when the broker re-sets the flag via a subsequent heartbeat.
...
- A non-zero
ThrottleTimeMson the response delays the next push attempt by that amount, as with other request managers.
Plugin Implementation Guidelines
...
A correct plugin implementation should:
- Treat
setTopology(on(groupId, topologyEpoch)) anddeleteTopology(on(groupId)) as idempotent; the broker may re-issue an identical call when an earlier call's bookkeeping write failed. - Be thread-safe under concurrent invocation:
setTopologymay be called by multiple members of the same group in the same heartbeat cycle, and the periodic-cleanup path may invokedeleteTopologywhile a member is mid-push. - Reject payloads the plugin will not accept by completing the
setTopologyfuture withTopologyDescriptionTooLargeExceptionorInvalidRequestException; the broker persists the rejection at the epoch level viaLastFailedTopologyEpochand stops re-soliciting at the same epoch. - Signal transient storage-layer failures by completing the
setTopologyfuture with any other exception; the broker's transient-failure back-off (30 s → 1 h, exponential) throttles re-solicitation. - Return
nullfromgetTopologywhen the plugin has lost its data; the broker clearsStoredTopologyEpochon the next describe so a fresh push is re-solicited. - Avoid blocking coordinator threads (plugin methods may be invoked on them) and complete futures within seconds, not minutes; the broker applies no wall-clock deadline to plugin calls, so the coordinator's responsiveness is bounded by what the plugin does. Bound plugin-side state explicitly — the plugin shares the broker heap
UNKNOWN_MEMBER_ID means the broker no longer recognizes this member — either the group has been deleted or the member has been dropped. The client clears its topologyDescriptionRequired flag and relies on the existing membership-management path: the next StreamsGroupHeartbeat returns the same fence error, which already triggers a clean rejoin. Clearing the flag here prevents another push at the (now-fenced) member ID before the heartbeat round-trips.
A non-zero ThrottleTimeMs on the response delays the next push attempt by that amount, as with other request managers.
Plugin Implementation Guidelines
A correct plugin implementation should:
- Treat
setTopologyas idempotent on(groupId, topologyEpoch): the broker may re-issue an identical call when an earlier call's bookkeeping write failed. Overwriting with identical data is safe and expected. - Treat
deleteTopologyas idempotent on(groupId): the broker may call it again when an earlier call's flag-clearing write failed, and may call it for a group with nothing currently stored. Both must succeed. - Decide and enforce a maximum stored description size, and reject pushes that exceed it by completing the
setTopologyfuture withTopologyDescriptionTooLargeException. The broker persists the rejection at the epoch level viaLastFailedTopologyEpoch, so subsequent heartbeats at the same epoch do not re-solicit. The plugin does not need to maintain its own "disabled" tracking. - Reject payloads with semantic problems (malformed graph, missing fields the plugin requires for its own indexing, etc.) by completing the
setTopologyfuture withInvalidRequestException. The broker's permanent-failure treatment matchesTopologyDescriptionTooLargeException. - Surface storage-layer failures by completing the
setTopologyfuture with any other exception. The broker treats it as transient: the metadata-record write is skipped, and the next heartbeat re-solicits. No special back-off is required on the plugin side. - Detect plugin-side data loss by returning
nullfromgetTopologywhen the broker asks for an epoch the plugin no longer has. The broker self-heals by clearingStoredTopologyEpochon the next describe.
Operational expectations
Plugin code runs inside the broker JVM and shares its heap and threads. The broker applies no wall-clock deadline to plugin calls — matching the convention established by Authorizer and ClientMetricsReceiver — so the operational behaviour of the broker is bounded by what the plugin does, not by a defensive timer. The expectations below are the contract that makes that arrangement safe; plugins that violate them can degrade or stall the coordinator.
- Latency. Plugin futures should settle in seconds, not minutes. A
setTopology,getTopology, ordeleteTopologyfuture that hangs holds the corresponding broker bookkeeping (response future, in-flight tracker entry, retained payload) for as long as it remains incomplete. If the backing storage is slow or unresponsive, complete the future exceptionally rather than holding it open — the broker's transient-failure back-off (exponential, 30 s → 1 h) will throttle re-solicitation appropriately. - No blocking on coordinator threads. Plugin methods may be invoked on coordinator threads. Synchronous I/O against the plugin's backing store, locks held across
await(), or long-running computation insidesetTopology/getTopology/deleteTopologywill block coordinator processing for other groups on the same shard. Use the async I/O surface of the chosen backend. - Bounded memory. The plugin shares the broker heap. Plugin-side state should be bounded to roughly one topology per active group at the current topology epoch; anything larger (caches of superseded epochs, retained payloads after
deleteTopology) needs an explicit eviction policy. - Thread hygiene.
setTopologymay be called concurrently by multiple members of the same group in the same heartbeat cycle, and the periodic-cleanup path may invokedeleteTopologywhile a member is mid-push. All three methods must be safe under concurrent invocation. Avoid spawning unbounded thread pools or background tasks; bound any internal executors to a fixed size. - Failure mode visibility. Surface plugin-side errors (storage outages, retries exhausted, deserialization failures) through the future's exceptional completion rather than logging-and-returning-success. The broker's error path is the only mechanism that distinguishes a successful push from a silent loss.
Security Considerations
The topology description contains user-defined processor names, state-store names, and topic names. This KIP does not treat these as inherently sensitive and does not introduce any client-side redaction. Operators who consider node, store, or topic names sensitive should scope DESCRIBE ACLs on the GROUP resource accordingly: the same ACL that guards StreamsGroupDescribe guards the TopologyDescription field on the response. The UpdateStreamsGroupTopologyDescription path is guarded by READ on the GROUP, identical to the existing heartbeat ACL.
...
The following broker-side sensors metrics are added on GroupCoordinatorMetrics, each exposed as a rate + count meter under the existing group-coordinator-metrics group (JMX type kafka.server:type=group-coordinator-metrics). Each sensor produces both a rate and a cumulative count.
| MBean | Type | Description |
|---|---|---|
kafka.server:type=group-coordinator-metrics,name= | ||
| Sensor | Description | |
topology-description-plugin-set-success-{rate,count} | Meter | Successful plugin.setTopology calls. |
kafka.server:type=group-coordinator-metrics,name=topology-description-plugin-set-error-{rate,count} | Meter | Failed plugin.setTopology calls (covers TooLarge, InvalidRequest, and other exceptions; an . An error increments the same this sensor regardless of the kind).whether it was TopologyDescriptionTooLargeException, InvalidRequestException, or any other exception. |
kafka.server:type=group-coordinator-metrics,name=topology-description-plugin-delete-success-{rate,count} | Meter | Successful plugin.deleteTopology calls (covers both the explicit - DeleteGroups and periodic-cleanup paths). |
kafka.server:type=group-coordinator-metrics,name=topology-description-plugin-delete-error-{rate,count} | Meter | Failed plugin.deleteTopology calls. |
kafka.server:type=group-coordinator-metrics,name=topology-description-plugin-get-success-{rate,count} | Meter | Successful plugin.getTopology calls. |
kafka.server:type=group-coordinator-metrics,name=topology-description-plugin-get-error-{rate,count} | Meter | Failed plugin.getTopology calls. |
kafka.server:type=group-coordinator-metrics,name=topology-description-cleanup-cycle-{rate,count} | Meter | Periodic topology-description cleanup cycles that actually ran. |
kafka.server:type=group-coordinator-metrics,name=topology-description-cleanup-skipped-{rate,count} | Meter | Cycles skipped by the single-flight guard because a prior cycle was still in flight. |
kafka.server:type=group-coordinator-metrics,name=topology-description-cleanup-eligible-{rate,count} | Meter | Streams group IDs identified as eligible for topology-description cleanup, summed across partitions. |
...
Multi-version describe. The describe response surfaces only the topology under the current group's current topologyEpoch. During a rolling topology upgrade, the previous epoch's description may is still stored by the plugin but is no longer reachable via describe. A natural extension is a descriptions[] array on the response, tagged by epoch, allowing operators to view both the previous and the in-flight new topology while the rollout completes.
...
