DUE TO SPAM, SIGN-UP IS DISABLED. Goto Selfserve wiki signup and request an account.
...
| Code Block | ||||
|---|---|---|---|---|
| ||||
{ "name": "ErrorMessage", "type": "string", "versions": "3+", "nullableVersions": "3+", "ignorable": true, "default": "null",
"about": "AThe description of the deletion errorerror message, or null if the deletion succeeded orthere was no description is availableerror." } |
A new generic error code is added to the per-group ErrorCode slot:
...
| Code Block | ||||
|---|---|---|---|---|
| ||||
/**
* A broker-side plugin that stores, forwards, or exposes topology descriptions pushed
* by Kafka Streams clients.
*
* <p>Implementations must be thread-safe. {@link #setTopology} may be called
* concurrently by multiple members of the same group; calls with the same
* {@code (groupId, topologyEpoch)} carry identical data and must be idempotent.
* {@link #deleteTopology} must also be idempotent — it may be called more than once
* for the same {@code groupId}, including when nothing is stored.
*/
public interface StreamsGroupTopologyDescriptionPlugin extends Configurable, AutoCloseable {
/**
* Store the topology description for a streams group.
*
* <p>The returned future completes when the topology has been persisted or forwarded.
* Failures must be signalled by completing the future exceptionally — implementations
* must not throw synchronously. The completion exception drives broker-side behaviour:
*
* <ul>
* <li>{@link PluginPermanentFailureExceptionStreamsTopologyDescriptionPermanentFailureException} — the description will never be accepted
* at this topology epoch (e.g. too large, semantically rejected). The broker
* ratchets {@code LastFailedTopologyEpoch} and stops re-soliciting until the
* epoch advances.</li>
* <li>{@link PluginTransientFailureExceptionStreamsTopologyDescriptionTransientFailureException} or any other exception — treated as
* transient. The broker arms or extends the per-group back-off (30 s → 1 h,
* exponential) and re-solicits on a later heartbeat.</li>
* </ul>
*
* In both cases the caller receives error code
* {@code STREAMS_TOPOLOGY_DESCRIPTION_UPDATE_FAILED} with the exception's message in
* {@code ErrorMessage}; the permanent-vs-transient split is broker-internal state.
*/
CompletableFuture<Void> setTopology(String groupId, int topologyEpoch,
StreamsGroupTopologyDescription description);
/**
* Remove any topology description stored for this group. Called when the group is
* deleted or expires. A failure (future completed exceptionally) is reported to the
* caller of {@code DeleteGroups} as {@code DELETE_FAILED} with the exception message
* in the per-group {@code ErrorMessage}, and the broker does not tombstone the group;
* a retry of {@code DeleteGroups} re-invokes this method idempotently. The
* periodic-cleanup path treats a failure identically — the group's tombstone is
* deferred to a future cycle.
*/
CompletableFuture<Void> deleteTopology(String groupId);
/**
* Return the stored topology description for {@code (groupId, topologyEpoch)}, or
* {@code null} if the plugin no longer has the data (e.g. backend wipe). If the future
* completes exceptionally, the broker reports a read error for the group.
*/
CompletableFuture<StreamsGroupTopologyDescription> getTopology(String groupId, int topologyEpoch);
} |
...
| 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();
}
} |
Two new exception classes in the same package let the plugin signal the permanent-vs-transient distinction. Plugins that throw any other exception are treated as transient:
| Code Block | ||||
|---|---|---|---|---|
| ||||
package org.apache.kafka.coordinator.group.api.streams; import org.apache.kafka.common.errors.ApiException; /** Signals that the topology description for the current epoch will never be accepted (e.g. too large, semantically rejected). */ public class PluginPermanentFailureExceptionStreamsTopologyDescriptionPermanentFailureException extends ApiException { public PluginPermanentFailureExceptionStreamsTopologyDescriptionPermanentFailureException(String message) { super(message); } public PluginPermanentFailureExceptionStreamsTopologyDescriptionPermanentFailureException(String message, Throwable cause) { super(message, cause); } } /** Signals a transient backend failure; the broker re-solicits on a later heartbeat. Plugins that throw any other exception are treated identically. */ public class PluginTransientFailureExceptionStreamsTopologyDescriptionTransientFailureException extends ApiException { public PluginTransientFailureExceptionStreamsTopologyDescriptionTransientFailureException(String message) { super(message); } public PluginTransientFailureExceptionStreamsTopologyDescriptionTransientFailureException(String message, Throwable cause) { super(message, cause); } } |
...
The plugin is instantiated at broker startup if
group.streams.topology.description.plugin.classis configured. A broker without a plugin returnsUNSUPPORTED_VERSIONforStreamsGroupTopologyDescriptionUpdateand never setsTopologyDescriptionRequired, so the RPC is only sent against a plugin-configured broker.After a successful
StreamsGroupHeartbeat, the broker decides whether to setTopologyDescriptionRequired=truepurely from the group's persisted state — no plugin RPC is involved. Members withSTALE_TOPOLOGYstatus are skipped. For all other members the broker sets the flag iffStoredTopologyEpoch != currentTopologyEpochANDLastFailedTopologyEpoch != currentTopologyEpochAND no per-group back-off is in its window. The back-off is in-memory state (keyed bygroupId, carryingtopologyEpoch+nextAttemptMs) that arms or extends every time the flag is set and additionally on a transientsetTopologyfailure; consecutive arms double the window from 30 s up to 1 h. It clears on a successful push, on a permanent failure (whereLastFailedTopologyEpochratchets), and implicitly on any topology-epoch advance. The same mechanism covers unresponsive plugins and clients that never push (for example, withtopology.description.push.enabled=false).On
StreamsGroupTopologyDescriptionUpdate, the broker checks theREADACL on the group and that a plugin is configured, then validates theMemberId: an emptyMemberIdis rejected withINVALID_REQUEST, a non-existing streams group withGROUP_ID_NOT_FOUND, and aMemberIdnot matching any current member withUNKNOWN_MEMBER_ID. The broker enforces no size limit; the plugin decides what it is willing to store. The broker then callssetTopologyon the plugin. On success it writes a metadata record settingStoredTopologyEpoch = pushedEpochand the response carriesNONE. OnPluginPermanentFailureExceptionStreamsTopologyDescriptionPermanentFailureExceptionit writesLastFailedTopologyEpoch = pushedEpochso subsequent heartbeats at the same epoch do not re-solicit. OnPluginTransientFailureExceptionStreamsTopologyDescriptionTransientFailureExceptionor any other exception it writes no metadata record, arms the per-group back-off, and the next heartbeat re-solicits once the window elapses. In both failure cases the response carriesSTREAMS_TOPOLOGY_DESCRIPTION_UPDATE_FAILEDwith the plugin's exception message inErrorMessage; the permanent-vs-transient split is broker-internal state. If the plugin call succeeds but the metadata-record write fails, the next heartbeat sees the drift, re-solicits, and the idempotent re-push closes the gap.On
DeleteGroups, the broker callsdeleteTopologyon the plugin before writing the group tombstone, for each requested streams group withStoredTopologyEpoch != -1. On plugin success the group is tombstoned and the per-groupErrorCodeisNONE. On plugin failure the group is not tombstoned and the per-groupErrorCodeis set toDELETE_FAILEDwith the plugin's exception message inErrorMessage(also logged at WARN); the operator retries the request once the plugin recovers, or unsets the plugin config to bypass it. Other groups in the same batch are unaffected — the failure is reported per group. This ordering matches the natural-expiration cleanup below, which also defers tombstoning untilplugin.deleteTopologysucceeds.- 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). With no plugin configured the broker returnsNOT_STORED. AgetTopologycall returningnull(plugin-side data loss) surfaces asNOT_STOREDand is logged at WARN; subsequent describes keep returningNOT_STOREDuntil the topology epoch advances or an operator clears plugin state. - When a plugin is configured, the broker runs a periodic topology-description cleanup 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. When no plugin is configured, the periodic cleanup does not run and the shard's offset-expiration sweep expires streams groups normally, ignoringStoredTopologyEpoch; operators that disable a previously-configured plugin are responsible for cleaning up plugin-side state out-of-band.
...
- 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 withPluginPermanentFailureExceptionStreamsTopologyDescriptionPermanentFailureException; the broker persists the rejection at the epoch level viaLastFailedTopologyEpochand stops re-soliciting at the same epoch. The exception message reaches the client inErrorMessage. - Signal transient storage-layer failures by completing the
setTopologyfuture withPluginTransientFailureExceptionStreamsTopologyDescriptionTransientFailureException(or any other exception); the broker's per-group back-off (30 s → 1 h, exponential) throttles re-solicitation. - Return
nullfromgetTopologywhen the plugin has lost the description; the broker reportsNOT_STOREDon the describe response. Exceptions are reserved for transient backend failures and surface asERROR. - 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.
...
| MBean | Type | Description |
|---|---|---|
kafka.server:type=group-coordinator-metrics,name=streams-group-topology-description-set-success-{rate,count} | Meter | Successful plugin.setTopology calls. |
kafka.server:type=group-coordinator-metrics,name=streams-group-topology-description-set-error-{rate,count} | Meter | Failed |
kafka.server:type=group-coordinator-metrics,name=streams-group-topology-description-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=streams-group-topology-description-delete-error-{rate,count} | Meter | Failed plugin.deleteTopology calls. |
kafka.server:type=group-coordinator-metrics,name=streams-group-topology-description-get-success-{rate,count} | Meter | Successful plugin.getTopology calls. |
kafka.server:type=group-coordinator-metrics,name=streams-group-topology-description-get-error-{rate,count} | Meter | Failed plugin.getTopology calls. |
kafka.server:type=group-coordinator-metrics,name=streams-group-topology-description-cleanup-cycle-{rate,count} | Meter | Periodic topology-description cleanup cycles that actually ran. |
kafka.server:type=group-coordinator-metrics,name=streams-group-topology-description-cleanup-eligible-{rate,count} | Meter | Streams group IDs identified as eligible for topology-description cleanup, summed across partitions. |
...
StreamsGroupTopologyDescriptionUpdateRequestTest(new) — push happy path;PluginPermanentFailureExceptionStreamsTopologyDescriptionPermanentFailureException(ratchetsLastFailedTopologyEpoch) andPluginTransientFailureExceptionStreamsTopologyDescriptionTransientFailureException(arms back-off) both surface asSTREAMS_TOPOLOGY_DESCRIPTION_UPDATE_FAILEDwith the cause inErrorMessage; heartbeat-flag gating; member/group/MemberIdfencing; explicitDeleteGroupsplugin-success path (tombstone written) and plugin-failure path (DELETE_FAILEDreturned withErrorMessage, group not tombstoned, retry converges after plugin recovery).StreamsGroupTopologyDescriptionUpdateNoPluginRequestTest(new) — broker without plugin: pushes rejected, flag never set, describe returnsNOT_STORED.AuthorizerIntegrationTest—READACL on the GROUP resource for the new RPC.
...