squah-confluent commented on code in PR #23121:
URL: https://github.com/apache/kafka/pull/23121#discussion_r3858660840
##########
core/src/main/scala/kafka/server/ConfigAdminManager.scala:
##########
@@ -64,15 +65,19 @@ import scala.jdk.CollectionConverters._
*
* Configuration processing is split into two parts.
* - The first step, called "preprocessing," handles setting KIP-412 log
levels, validating
- * BROKER configurations. We also filter out some other things here like
UNKNOWN resource
- * types, etc.
+ * BROKER configurations, and performing the full GROUP configuration
validation (see
+ * [[org.apache.kafka.coordinator.group.GroupConfig#validateOnBroker]]). We
also filter out
+ * some other things here like UNKNOWN resource types, etc.
* - The second step is "persistence," and handles storing the configurations
durably to our
* metadata store.
*
* The active controller performs its own configuration validation step in
* [[kafka.server.ControllerConfigurationValidator]]. This is mainly important
for
* TOPIC resources, since we already validated changes to BROKER resources on
the
- * forwarding broker. The controller is also responsible for enforcing the
configured
+ * forwarding broker. GROUP resources are validated on the controller too, so
that the
+ * cluster stays protected while it may still contain brokers that predate the
broker-side
+ * check; see [[kafka.server.ControllerConfigurationValidator]] for the
upgrade-safety
+ * gating. The controller is also responsible for enforcing the configured
* [[org.apache.kafka.server.policy.AlterConfigPolicy]].
Review Comment:
nit: I'm assuming these javadoc updates are Claude-written and will push to
cut them down since Claude loves to cram information that's true but not
immediately helpful to the reader into them.
* "protected" is not quite the right concept.
* We already point to `ControllerConfigurationValidator` for controller
validation above.
```
* TOPIC resources, since we already validated changes to BROKER and GROUP
resources on the
* forwarding broker. When the cluster may contain brokers that predate the
broker-side GROUP
* validation, GROUP resources are validated on the controller too. The
controller is also
* responsible for enforcing the configured
[[org.apache.kafka.server.policy.AlterConfigPolicy]].
* ```
##########
core/src/main/scala/kafka/server/ConfigAdminManager.scala:
##########
@@ -201,6 +208,28 @@ class ConfigAdminManager(nodeId: Int,
}
}
+ /**
+ * Perform the full GROUP config validation on the forwarding broker, before
the change is
+ * sent to the controller. See
[[kafka.server.ControllerConfigurationValidator]] for why the
+ * controller also performs this validation while a cluster may still
contain brokers that
+ * predate this broker-side check.
+ */
+ private def validateGroupConfigChangeOnBroker(resource:
IAlterConfigsResource): Unit = {
Review Comment:
nit: We don't suffix the other methods in this class with "OnBroker". All
the validation in this class is on the broker.
##########
core/src/main/scala/kafka/server/ConfigAdminManager.scala:
##########
@@ -201,6 +208,28 @@ class ConfigAdminManager(nodeId: Int,
}
}
+ /**
+ * Perform the full GROUP config validation on the forwarding broker, before
the change is
+ * sent to the controller. See
[[kafka.server.ControllerConfigurationValidator]] for why the
+ * controller also performs this validation while a cluster may still
contain brokers that
+ * predate this broker-side check.
+ */
Review Comment:
nit: I'm assuming these javadoc updates are Claude-written and will push to
cut them down since Claude loves to cram information that's true but not
immediately helpful to the reader into them.
* There's no concept of a non-full GROUP validation before or after this PR.
* All the validation methods in this file validate before the change is sent
to the controller.
* "See [[kafka.server.ControllerConfigurationValidator]] for why the
controller also performs this validation" answers a question the reader never
asked.
```suggestion
```
##########
core/src/main/scala/kafka/server/ControllerConfigurationValidator.scala:
##########
@@ -39,6 +40,14 @@ import scala.collection.mutable
* the controller. Therefore, the validation here is just a kind of sanity
check, which
* should never fail under normal conditions.
*
+ * GROUP resources are validated here too, via {@link
GroupConfig#validateOnController}, even
+ * though the forwarding broker already performs the same full validation in
+ * {@link kafka.server.ConfigAdminManager#preprocess()} via {@link
GroupConfig#validateOnBroker}.
+ * This is kept, gated on {@code metadataVersion}, so that the cluster is
never left without GROUP
+ * config validation while it may still contain brokers that predate the
broker-side check
+ * (KAFKA-20790). It can be dropped once upgrading directly from a pre-{@link
MetadataVersion#IBP_4_5_IV0}
+ * cluster is no longer supported.
Review Comment:
* We're expressing the same information as the previous paragraph, but for
GROUPs. Copying the sentence structure makes it more obvious.
* We can leave the implementation detailsĀ and removal considerations for
comments in the validation code.
```
* For changes to GROUP resources, the forwarding broker performs validation
in
* {@link kafka.server.ConfigAdminManager#preprocess()} before sending the
change to
* the controller. The validation here is only run when the cluster may
contain brokers
* that predate the broker-side validation.
```
##########
group-coordinator/src/test/java/org/apache/kafka/coordinator/group/GroupConfigTest.java:
##########
@@ -384,59 +384,34 @@ public void
testStreamsRackAwareAssignmentTagsValidation() {
@Test
public void testStreamsAssignorNameValidation() {
- // A registered assignor name is accepted.
- Map<String, String> props = createValidGroupConfig();
- props.put(GroupConfig.STREAMS_ASSIGNOR_NAME_CONFIG, "sticky");
- doTestValidProps(props);
-
- // An unknown assignor name is rejected with INVALID_CONFIG.
- props = createValidGroupConfig();
- props.put(GroupConfig.STREAMS_ASSIGNOR_NAME_CONFIG, "does-not-exist");
- doTestInvalidProps(props, InvalidConfigurationException.class);
- }
-
- @Test
- public void testStreamsAssignorNameSelectsCustomAssignor() {
Review Comment:
Why did we remove this test?
##########
core/src/main/scala/kafka/server/ConfigAdminManager.scala:
##########
@@ -64,15 +65,19 @@ import scala.jdk.CollectionConverters._
*
* Configuration processing is split into two parts.
* - The first step, called "preprocessing," handles setting KIP-412 log
levels, validating
- * BROKER configurations. We also filter out some other things here like
UNKNOWN resource
- * types, etc.
+ * BROKER configurations, and performing the full GROUP configuration
validation (see
+ * [[org.apache.kafka.coordinator.group.GroupConfig#validateOnBroker]]). We
also filter out
+ * some other things here like UNKNOWN resource types, etc.
Review Comment:
nit: I'm assuming these javadoc updates are Claude-written and will push to
cut them down since Claude loves to cram information that's true but not
immediately helpful to the reader into them.
* There's no concept of a non-full GROUP validation before or after this PR.
* We don't link to the validation method for broker validation.
```
* - The first step, called "preprocessing," handles setting KIP-412 log
levels, and validating
* BROKER and GROUP configurations. We also filter out some other things
here like UNKNOWN
* resource types, etc.
```
##########
core/src/main/scala/kafka/server/ConfigAdminManager.scala:
##########
@@ -201,6 +208,28 @@ class ConfigAdminManager(nodeId: Int,
}
}
+ /**
+ * Perform the full GROUP config validation on the forwarding broker, before
the change is
+ * sent to the controller. See
[[kafka.server.ControllerConfigurationValidator]] for why the
+ * controller also performs this validation while a cluster may still
contain brokers that
+ * predate this broker-side check.
+ */
+ private def validateGroupConfigChangeOnBroker(resource:
IAlterConfigsResource): Unit = {
+ val configResource = new ConfigResource(GROUP, resource.resourceName())
Review Comment:
We pass `configResource` as a parameter in `validateBrokerConfigChange`
instead of re-creating it.
##########
core/src/test/scala/unit/kafka/server/ControllerConfigurationValidatorTest.scala:
##########
@@ -219,6 +224,25 @@ class ControllerConfigurationValidatorTest {
config.put("foobar", "abc")
assertEquals("Unknown group config name: foobar",
assertThrows(classOf[InvalidConfigurationException], () =>
validator.validate(
- new ConfigResource(GROUP, "group"), config, emptyMap())).getMessage)
+ new ConfigResource(GROUP, "group"), config, emptyMap(),
preGroupBrokerValidationMv)).getMessage)
+ }
+
+ @Test
+ def testInvalidGroupConfigStillRejectedBelowGatedMetadataVersion(): Unit = {
+ val config = new util.TreeMap[String, String]()
+ config.put("foobar", "abc")
+ assertEquals("Unknown group config name: foobar",
+ assertThrows(classOf[InvalidConfigurationException], () =>
validator.validate(
+ new ConfigResource(GROUP, "group"), config, emptyMap(),
MetadataVersion.IBP_4_4_IV2)).getMessage)
+ }
+
+ @Test
+ def
testInvalidGroupConfigSkippedOnceMetadataVersionGuaranteesBrokerValidation():
Unit = {
+ // Once every broker in the cluster is guaranteed to already validate this
on the forwarding
+ // broker (see ConfigAdminManager#validateGroupConfigChangeOnBroker), the
controller no longer
+ // needs to, and an otherwise-invalid config is let through here.
+ val config = new util.TreeMap[String, String]()
+ config.put("foobar", "abc")
+ validator.validate(new ConfigResource(GROUP, "group"), config, emptyMap(),
MetadataVersion.IBP_4_5_IV0)
}
Review Comment:
Claude loves spamming new tests without regard for existing tests or overall
test strategy.
There is an existing `testInvalidGroupConfig` test - how does the coverage
overlap with the new tests?
##########
core/src/main/scala/kafka/server/ConfigAdminManager.scala:
##########
@@ -145,7 +150,9 @@ class ConfigAdminManager(nodeId: Int,
validateResourceNameIsCurrentNodeId(resource.resourceName())
}
validateBrokerConfigChange(resource, configResource)
- case TOPIC | CLIENT_METRICS | GROUP =>
+ case GROUP =>
+ validateGroupConfigChangeOnBroker(resource)
Review Comment:
Do we need to update the `preprocess(AlterConfigsRequestData, ...)` path too?
##########
core/src/test/scala/unit/kafka/server/ConfigAdminManagerTest.scala:
##########
@@ -417,6 +422,60 @@ class ConfigAdminManagerTest {
(_, _) => true))
}
+ def groupIncremental(configName: String, value: String, opType: OpType):
IAlterConfigsResource =
+ new IAlterConfigsResource().
+ setResourceName("group").
+ setResourceType(GROUP.id).
+ setConfigs(new IAlterableConfigCollection(
+ util.Arrays.asList(new IAlterableConfig().setName(configName).
+ setValue(value).
+ setConfigOperation(opType.id()))))
+
+ @Test
+ def testPreprocessIncrementalWithStreamsAssignorName(): Unit = {
+ // A built-in assignor and a custom one, registered by short name and by
class name respectively.
+ val manager = newConfigAdminManager(1,
+ Map(GroupCoordinatorConfig.STREAMS_GROUP_ASSIGNORS_CONFIG ->
+ s"sticky,${classOf[CustomStreamsTaskAssignor].getName}"))
+
+ // Both are selectable by the name the assignor reports, and neither
resource is preprocessed,
+ // so both requests are forwarded to the controller.
+ Seq("sticky", CustomStreamsTaskAssignor.NAME).foreach { name =>
+ val group = groupIncremental(GroupConfig.STREAMS_ASSIGNOR_NAME_CONFIG,
name, OpType.SET)
+ assertEquals(Collections.emptyMap(),
+ manager.preprocess(new IncrementalAlterConfigsRequestData().
+ setResources(new IAlterConfigsResourceCollection(util.Arrays.asList(
+ group))),
+ (_, _) => true))
+ }
+
+ // A name that no registered assignor reports is rejected before the
request is forwarded.
+ val unknown = groupIncremental(GroupConfig.STREAMS_ASSIGNOR_NAME_CONFIG,
"does-not-exist", OpType.SET)
+ assertEquals(Collections.singletonMap(unknown,
+ new ApiError(Errors.INVALID_CONFIG, "streams.assignor.name
'does-not-exist' is not a " +
+ "registered task assignor. Registered assignors are: [sticky,
custom].")),
+ manager.preprocess(new IncrementalAlterConfigsRequestData().
+ setResources(new IAlterConfigsResourceCollection(util.Arrays.asList(
+ unknown))),
+ (_, _) => true))
+ }
+
+ @Test
+ def testPreprocessIncrementalWithUnregisteredBuiltinStreamsAssignorName():
Unit = {
+ // Only the custom assignor is registered. A built-in is not implicitly
available, so it can no
+ // longer be selected either.
+ val manager = newConfigAdminManager(1,
+ Map(GroupCoordinatorConfig.STREAMS_GROUP_ASSIGNORS_CONFIG ->
classOf[CustomStreamsTaskAssignor].getName))
+ val sticky = groupIncremental(GroupConfig.STREAMS_ASSIGNOR_NAME_CONFIG,
"sticky", OpType.SET)
+ assertEquals(Collections.singletonMap(sticky,
+ new ApiError(Errors.INVALID_CONFIG, "streams.assignor.name 'sticky' is
not a " +
+ "registered task assignor. Registered assignors are: [custom].")),
+ manager.preprocess(new IncrementalAlterConfigsRequestData().
+ setResources(new IAlterConfigsResourceCollection(util.Arrays.asList(
+ sticky))),
+ (_, _) => true))
+ }
Review Comment:
These tests no longer look appropriate. We would like to test that group
validation is done on the broker (the equivalent of the first part of
`StreamsGroupHeartbeatRequestTest.testAlterStreamsAssignorNameGroupConfigIsValidatedOnTheBroker`),
not the particulars of streams assignor validation.
##########
core/src/main/scala/kafka/server/ControllerConfigurationValidator.scala:
##########
@@ -134,8 +144,12 @@ class ControllerConfigurationValidator(kafkaConfig:
KafkaConfig) extends Configu
ClientMetricsConfigs.validate(resource.name(), filteredConfigs)
case GROUP =>
validateGroupName(resource.name())
- val filteredConfigs = filterAndValidateNullConfigs(newConfigs, "group")
- GroupConfig.validate(filteredConfigs,
kafkaConfig.groupCoordinatorConfig, kafkaConfig.shareGroupConfig)
+ // Skip once every broker in the cluster is guaranteed to already
validate this on the
+ // forwarding broker. See the class-level doc for why this can't just
be removed outright.
Review Comment:
> See the class-level doc for why this can't just be removed outright.
Can we inline the condition for removal?
```
// Validate on the controller when the cluster may contain
forwarding brokers that do
// not validate. Can be removed once upgrading directly from a
// pre-{@link MetadataVersion#IBP_4_5_IV0} cluster is no longer
supported.
```
##########
core/src/test/scala/unit/kafka/server/ControllerConfigurationValidatorTest.scala:
##########
@@ -36,26 +37,30 @@ class ControllerConfigurationValidatorTest {
val config = new KafkaConfig(TestUtils.createDummyBrokerConfig())
val validator = new ControllerConfigurationValidator(config)
+ // The metadata.version in effect prior to this feature shipping
(KAFKA-20790), used for all
+ // tests that are not specifically about the upgrade-safety gate below.
+ val preGroupBrokerValidationMv: MetadataVersion =
MetadataVersion.LATEST_PRODUCTION
Review Comment:
This comment is going to be inaccurate within two Kafka releases when
`LATEST_PRODUCTION` shifts under us. The exact metadata version also doesn't
matter for almost all tests. Can we inline this?
##########
group-coordinator/src/main/java/org/apache/kafka/coordinator/group/GroupConfig.java:
##########
@@ -477,6 +477,38 @@ public static void validateNames(Map<String, String>
newGroupConfig) {
}
}
+ /**
+ * Validate a GROUP config change on the forwarding broker, before it is
sent to the
+ * controller. Runs the full set of checks in {@link #validate}.
+ *
+ * @param newGroupConfig The new unparsed group config overrides.
+ * @param groupCoordinatorConfig The group coordinator config.
+ * @param shareGroupConfig The share group config.
+ */
+ public static void validateOnBroker(
+ Map<String, String> newGroupConfig,
+ GroupCoordinatorConfig groupCoordinatorConfig,
+ ShareGroupConfig shareGroupConfig
+ ) {
+ validate(newGroupConfig, groupCoordinatorConfig, shareGroupConfig);
+ }
+
+ /**
+ * Validate a GROUP config change on the controller. Runs the full set of
checks in
+ * {@link #validate}.
Review Comment:
Here the reader will wonder why we are validating both on the broker and
controller. This is a good place to explain that the controller validation is
not run once \<blah blah blah\>.
(Please do not call it an upgrade-safety gate. I'm tired of reading
Claude-speak.)
##########
core/src/test/scala/unit/kafka/server/ControllerConfigurationValidatorTest.scala:
##########
@@ -219,6 +224,25 @@ class ControllerConfigurationValidatorTest {
config.put("foobar", "abc")
assertEquals("Unknown group config name: foobar",
assertThrows(classOf[InvalidConfigurationException], () =>
validator.validate(
- new ConfigResource(GROUP, "group"), config, emptyMap())).getMessage)
+ new ConfigResource(GROUP, "group"), config, emptyMap(),
preGroupBrokerValidationMv)).getMessage)
+ }
+
+ @Test
+ def testInvalidGroupConfigStillRejectedBelowGatedMetadataVersion(): Unit = {
Review Comment:
* "Still" implies a before-and-after, but it doesn't mean anything to the
reader.
```suggestion
def testInvalidGroupConfigRejectedBelowMetadataVersion4_5(): Unit = {
```
##########
core/src/test/scala/unit/kafka/server/ControllerConfigurationValidatorTest.scala:
##########
@@ -219,6 +224,25 @@ class ControllerConfigurationValidatorTest {
config.put("foobar", "abc")
assertEquals("Unknown group config name: foobar",
assertThrows(classOf[InvalidConfigurationException], () =>
validator.validate(
- new ConfigResource(GROUP, "group"), config, emptyMap())).getMessage)
+ new ConfigResource(GROUP, "group"), config, emptyMap(),
preGroupBrokerValidationMv)).getMessage)
+ }
+
+ @Test
+ def testInvalidGroupConfigStillRejectedBelowGatedMetadataVersion(): Unit = {
+ val config = new util.TreeMap[String, String]()
+ config.put("foobar", "abc")
+ assertEquals("Unknown group config name: foobar",
+ assertThrows(classOf[InvalidConfigurationException], () =>
validator.validate(
+ new ConfigResource(GROUP, "group"), config, emptyMap(),
MetadataVersion.IBP_4_4_IV2)).getMessage)
+ }
+
+ @Test
+ def
testInvalidGroupConfigSkippedOnceMetadataVersionGuaranteesBrokerValidation():
Unit = {
Review Comment:
```suggestion
def testInvalidGroupConfigNotValidatedAfterMetadataVersion4_5(): Unit = {
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]