Skip to content

Add ConsumerConfig support for group.protocol=consumer (KIP-848) - #613

Open
tobiajo wants to merge 7 commits into
evolution-gaming:masterfrom
tobiajo:tj/group-protocol-consumer
Open

Add ConsumerConfig support for group.protocol=consumer (KIP-848)#613
tobiajo wants to merge 7 commits into
evolution-gaming:masterfrom
tobiajo:tj/group-protocol-consumer

Conversation

@tobiajo

@tobiajo tobiajo commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds groupProtocol/groupRemoteAssignor fields to ConsumerConfig for the KIP-848 configs group.protocol and group.remote.assignor; the new GroupProtocol sealed trait is modelled on IsolationLevel.
  • kafka-clients validates protocol/config combinations at construction time: group.protocol=consumer rejects partition.assignment.strategy, session.timeout.ms and heartbeat.interval.ms (assignment and session timing are server-managed under KIP-848), and classic rejects group.remote.assignor — so each is now emitted only under the protocol that accepts it. Tests construct the real org.apache.kafka.clients.consumer.ConsumerConfig under both protocols to exercise this validation.
  • The new fields break binary compatibility of the case class's apply/copy/unapply/constructor (apply/copy stay source-compatible via defaults); on Scala 2.13 the field count also crosses the 22-field cap on synthesized unapply (22 → 24), so tuple-style pattern matching against ConsumerConfig no longer compiles there at all. versionPolicyIntention is set to Compatibility.None: the next release must be a major, after which it should be reset to Compatibility.BinaryCompatible. A mimaBinaryIssueFilters entry was not used instead because those filters match by bare method name, not signature, and would permanently mask future unrelated breaks to the same methods.
  • Also drops a stale mimaBinaryIssueFilters entry for Producer.sendOffsetsToTransaction (unrelated; the method has shipped in every release since v20.2.0).
  • A separate second commit closes two pre-existing coverage gaps in the ConsumerConfigSpec parsing fixtures: retry-backoff was set to exactly its default (100 ms), so a silent parse break of that key would pass, and client-rack was never set at all, leaving ConsumerConfig.clientRack parsing unexercised. Unrelated to the feature; kept separate so it can be reverted independently.

NOTE: the below auto-generated summary is quite bad, ignore it.

Summary by CodeRabbit

New Features

  • Added support for configuring Classic and Consumer group protocols.
  • Added optional remote assignor configuration for Consumer protocol groups.
  • Added support for configuring client rack information.

Bug Fixes

  • Added validation for unsupported group protocol values and incompatible settings.
  • Improved consumer configuration to apply only protocol-compatible options.
  • Preserved Classic protocol behavior as the default.

ConsumerConfig gains groupProtocol and groupRemoteAssignor fields for
the KIP-848 configs group.protocol and group.remote.assignor.
kafka-clients rejects partition.assignment.strategy,
session.timeout.ms and heartbeat.interval.ms at construction time
when group.protocol is set to consumer (assignment and session timing
are server-managed under the new protocol), so these are now only
emitted under the classic protocol; group.remote.assignor is the
mirror image, only emitted under the consumer protocol and rejected
by the client under classic.

The new fields break binary compatibility of the case class's
apply/copy/unapply/constructor for pre-compiled callers; on Scala
2.13 the field count also crosses the 22-field cap on synthesized
unapply (22 -> 24), so tuple-style pattern matching against
ConsumerConfig no longer compiles there at all. versionPolicyIntention
is set to Compatibility.None accordingly: the next release must be a
major version bump, after which the setting should be reset to
Compatibility.BinaryCompatible.

Also drops a stale mimaBinaryIssueFilters entry for
Producer.sendOffsetsToTransaction, unrelated to this change: the
method has shipped in every release since v20.2.0, so the filter no
longer excludes anything.
retry-backoff/retry.backoff.ms were set to 100 in both .conf fixtures,
which is exactly CommonConfig.retryBackoff's default -- so if that
key's parsing broke silently, the parsing tests wouldn't catch it.
Bumped to 123 and added the matching common override to custom.

client-rack/client.rack were never set by either fixture, leaving
ConsumerConfig.clientRack's parsing entirely unexercised -- and unlike
CommonConfig's fields, which CommonConfigSpec covers independently, it
has no other test coverage. Added a fixture value for it too.

Both gaps pre-date this branch and are unrelated to the
group.protocol=consumer feature; kept as a separate commit so it can
be reverted independently.
@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

Next included review available in 19 minutes.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7ee32972-dde3-431b-9f74-a3242aece687

📥 Commits

Reviewing files that changed from the base of the PR and between 9716644 and d06b4ee.

📒 Files selected for processing (3)
  • skafka/src/main/scala/com/evolutiongaming/skafka/ConfigHelpers.scala
  • skafka/src/main/scala/com/evolutiongaming/skafka/consumer/ConsumerConfig.scala
  • skafka/src/test/scala/com/evolutiongaming/skafka/consumer/ConsumerConfigSpec.scala

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 23d4377c-7560-45af-a0f7-a16c440f5fe1

📥 Commits

Reviewing files that changed from the base of the PR and between 3bb2227 and 9716644.

📒 Files selected for processing (1)
  • build.sbt

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

ConsumerConfig now supports classic and consumer group protocols, remote assignors, protocol-specific Kafka bindings, and Kafka client validation tests. Binary compatibility enforcement is disabled for the public configuration change.

Changes

Consumer group protocol support

Layer / File(s) Summary
Protocol contract and compatibility settings
skafka/src/main/scala/com/evolutiongaming/skafka/consumer/GroupProtocol.scala, skafka/src/main/scala/com/evolutiongaming/skafka/consumer/ConsumerConfig.scala, build.sbt
Adds the GroupProtocol hierarchy and new ConsumerConfig fields. Disables binary compatibility enforcement and removes the Producer.sendOffsetsToTransaction exception.
Protocol parsing and Kafka bindings
skafka/src/main/scala/com/evolutiongaming/skafka/consumer/ConsumerConfig.scala
Parses classic and consumer protocols case-insensitively. Generates protocol-specific Kafka properties and supports an optional remote assignor.
Binding and client validation coverage
skafka/src/test/scala/com/evolutiongaming/skafka/consumer/ConsumerConfigSpec.scala, skafka/src/test/resources/com/evolutiongaming/skafka/consumer/*.conf
Tests default classic bindings, consumer protocol bindings, remote assignor handling, incompatible property rejection, and Kafka client construction.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 97166

This change adds consumer group protocol configuration and related validation while documenting the intentional compatibility boundary; no actionable merge-blocking risk remains beyond normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant Application
  participant ConsumerConfig
  participant GroupProtocolFromConf
  participant KafkaConsumerConfig

  Application->>ConsumerConfig: load group protocol settings
  ConsumerConfig->>GroupProtocolFromConf: parse protocol name
  GroupProtocolFromConf-->>ConsumerConfig: return protocol value
  ConsumerConfig->>ConsumerConfig: generate protocol-specific bindings
  ConsumerConfig->>KafkaConsumerConfig: validate generated properties
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding ConsumerConfig support for the consumer group protocol defined by KIP-848.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0 files. (1 skipped: 1 unsupported.)

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@tobiajo
tobiajo force-pushed the tj/group-protocol-consumer branch from 3bb2227 to 87767f4 Compare August 17, 2026 14:33
// `group.protocol=consumer` rejects `partition.assignment.strategy`, `session.timeout.ms` and
// `heartbeat.interval.ms` (assignment and session timing are server-managed under the new
// protocol); `group.protocol=classic` rejects `group.remote.assignor`.
val protocolSpecificMap = groupProtocol match {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If someone sets group-protocol=consumer together with an explicit session-timeout or partition-assignment-strategy, the values parse fine and then get dropped here, so failure detection timing quietly falls back to broker defaults while the operator thinks their tuning is active. Raw kafka-clients fails fast on that combination. The config parser can tell explicit values from defaults, so it could warn or reject there instead of silently ignoring them.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that the existing configurations (sessionTimeout and heartbeatInterval) are not optional in skafka, there is no possibility to reject. The alternative would be an ADT of configuration for different protocols, moving protocol specific params into it, which would be a larger change.

I chose this as I felt it followed existing pattern as there are other cases too, where redundant config is ignored, not sure if comparable to this though as kafka-client not rejecting on those cases.

Would it be preferable with the following?

case class Classic(sessionTimeout: FiniteDuration, heartbeatInterval: FiniteDuration)

case class Consumer(remoteAssignor: Option[String])

Just reacting on configurations that diverge from default for the existing configurations I think would be meaningless, adding false security.

sslSupport: SslSupportConfig = SslSupportConfig.Default,
clientRack: Option[String] = None,
groupProtocol: GroupProtocol = GroupProtocol.Classic,
groupRemoteAssignor: Option[String] = None, // only emitted under GroupProtocol.Consumer

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting group-remote-assignor without group-protocol=consumer parses fine and the assignor just gets dropped, which you only notice from runtime behaviour. Since GroupProtocol is new in this PR, the assignor could live inside the Consumer case instead, e.g. case class Consumer(remoteAssignor: Option[String] = None). That makes the invalid combination unrepresentable and saves a case class field.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread build.sbt Outdated
// On Scala 2.13 this also crosses the 22-field cap on synthesized unapply (22 -> 24), so
// tuple-style pattern matching against ConsumerConfig no longer compiles there at all.
// TODO reset to Compatibility.BinaryCompatible after that release
ThisBuild / versionPolicyIntention := Compatibility.None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is ThisBuild scoped, so bincompat checking is off for every module and the whole API until the TODO is reverted, and nothing enforces that the next tag is actually a major. An unrelated accidental binary break merged before that release would pass versionPolicyCheck silently. Scoping it to the skafka project, or filtering just the ConsumerConfig break, keeps the window smaller.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in: 9716644

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping open so whoever merges this does not miss it.

}
}

private implicit val GroupProtocolFromConf: FromConf[GroupProtocol] = FromConf[GroupProtocol] { (conf, path) =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the third copy of the same find-by-name-or-throw block (AutoOffsetReset and IsolationLevel above), and the next enum config field would copy it a fourth time. Might be worth a small shared helper, something like enumFromConf[T](values: Set[T], label: String)(name: T => String): FromConf[T].

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that work well, added (in 97c4779):

ConfigHelpers.enumFromConf[T](values: Set[T], label: String)(name: T => String): FromConf[T]

Plus a bonus commit (d06b4ee), using it also for implicit val KeystoreTypeFromConfig.

Compatibility.None was set at ThisBuild scope, disabling bincompat
checking for every module until the TODO is reverted -- an unrelated
accidental binary break in play-json/metrics/metrics-prometheus-v1
would have passed versionPolicyCheck silently. Only skafka itself
breaks compatibility (the new ConsumerConfig fields), so the override
now lives in that project's settings and the other modules keep being
checked against the last release.
toUpperCase without a locale uses the JVM default, so on a Turkish
default locale "classic" uppercases to CLASSİC (dotted İ), which never
matches what kafka-clients emits and fails the test spuriously.
GroupProtocolFromConf was the third copy of the find-by-name-or-throw
block in ConsumerConfig; the next enum config field would copy it a
fourth time. The helper is private[skafka] to keep it out of the
published API. The remaining copies (CommonConfig, ProducerConfig,
KeystoreType) predate this PR and are left for a follow-up.
It sits directly below the helper and is the same find-by-name-or-throw
block. The error message loses its quotes around the value; nothing
asserts it.
@sonarqubecloud

Copy link
Copy Markdown

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.

2 participants