Skip to content

Conversation

@mannoopj
Copy link
Contributor

FeaturesPublisher's default finalizedFeatures assumes a metadata version of 7 when there is no finalized level for metadata.version. Instead of defaulting to 7, it should not report a value or report -1 indicating it is unknown.

See https://lists.apache.org/thread/ytmo7b9vmy46n6p47l1sxx6ftvdhht5b for the context as part of the KIP-1170 discussion.

Implemented by changing finalizedFeatures to Optional in FeaturesPublisher

@github-actions github-actions bot added triage PRs from the community core Kafka Broker performance kraft small Small PRs labels Dec 10, 2025
Copy link
Contributor

@kevin-wu24 kevin-wu24 left a comment

Choose a reason for hiding this comment

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

Thanks for the changes @mannoopj. Left a review of src/main. Can we also add a FeaturesPublisher unit test to verify this new behavior?

@Override
public FinalizedFeatures features() {
return featuresProvider.get();
return featuresProvider.get().orElse(null);
Copy link
Contributor

Choose a reason for hiding this comment

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

We should avoid ever returning null, because this is not safe. Instead, we can change the ApiVersionManager interface's features() method to return an Optional<FinalizedFeatures>. The implementors can return the wrapped optional, and check things based on its presence.

For example, ControllerApis#handleDescribeCluster can now return a more useful error message if the optional is not present. We can also add a test for that in ControllerApisTest.

() => featuresPublisher.features().setFinalizedLevel(
KRaftVersion.FEATURE_NAME,
raftManager.client.kraftVersion().featureLevel())
() => featuresPublisher.features().map(f =>
Copy link
Contributor

@kevin-wu24 kevin-wu24 Dec 10, 2025

Choose a reason for hiding this comment

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

There is a subtlety here with kraft.version. Previously, featuresPublisher.features was always "present", so we can always set the "finalized" kraft version known by the local KafkaRaftClient. However, now the featurePublisher.features() might be an empty optional, in which case the function in the .map call won't be applied until after featuresPublisher.features().isPresent().

This means if a controller node has kraft.version=1, but has not committed the bootstrap metadata version record (i.e. featuresPublisher.features().isEmpty()), the controller will not show that it is kraft.version=1 in the ApiVersionsResponse it sends, even though it previously would have.

I think the intention is, for better or for worse, that the "finalizedFeatures" section of the ApiVersionsResponse to show the local node's kraft.version level, even if it may be uncommitted. Is that correct @jsancio?

Copy link
Contributor

@kevin-wu24 kevin-wu24 Dec 10, 2025

Choose a reason for hiding this comment

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

If so, one way to fix this is to add a NOT_FINALIZED enum value to MetadataVersion. This option makes the most sense to me, but it is weird that FinalizedFeatures has a NOT_FINALIZED value for metadata version...

Or we can make FinalizedFeatures#metadataVersion field an optional, and keep it so that FinalizedFeatures does not have to be an optional (although this seems confusing when the object is called "FinalizedFeatures").

Copy link
Contributor

Choose a reason for hiding this comment

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

Also it is weird we have FinalizedFeatures and FinalizedControllerFeatures records.

Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like this behavior is not intentional: KAFKA-18865

.setSupportedFeatures(brokerFeatures)
.setFinalizedFeatures(currentFeatures.finalizedFeatures())
.setFinalizedFeaturesEpoch(currentFeatures.finalizedFeaturesEpoch())
.setFinalizedFeatures(currentFeatures.map(FinalizedFeatures::finalizedFeatures).orElse(Map.of()))
Copy link
Contributor

@kevin-wu24 kevin-wu24 Dec 10, 2025

Choose a reason for hiding this comment

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

Actually, if we pass the kraft.version information here as a Supplier, we can set the finalized features field with the kraft.version here whenever currentFeatures.isEmpty() (i.e. FeaturesPublisher#finalizedFeatures.isEmpty()).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

val apiVersionManager = new SimpleApiVersionManager(
        ListenerType.CONTROLLER,
        config.unstableApiVersionsEnabled,
        () => featuresPublisher.features().setFinalizedLevel(
          KRaftVersion.FEATURE_NAME,
          raftManager.client.kraftVersion().featureLevel())
      )

This code in ControllerServer.scala already adds the kraft.version. It might not appear in the output if kraft.version is 0 and hence gets filtered out.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the only other places SimpleApiVersionManager gets called are from tests and KRaftMetadataRequestBenchmark so changing the default to FinalizedFeatures.UNKNOWN_FINALIZED_FEATURES shouldn't change any behaviors here

Copy link
Contributor

Choose a reason for hiding this comment

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

I think if we want to expose the latest committed kraft version, we should do it in a separate PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

This code in ControllerServer.scala already adds the kraft.version. It might not appear in the output if kraft.version is 0 and hence gets filtered out.

Yes but the current code will expose kraft.version=1 in the APIVersionsResponse even if it has not been committed yet. We can slightly improve on this by not adding the kraft.version if the metadata.version does not exist, but the feature's value in the API versions may still be uncommitted when doing upgrade.

@github-actions github-actions bot removed the triage PRs from the community label Dec 11, 2025
Copy link
Contributor

@kevin-wu24 kevin-wu24 left a comment

Choose a reason for hiding this comment

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

Thanks for the changes @mannoopj . Left another review of src/main.

Can we add the suggestion from: #21122 (comment)? That way we can call setFinalizedFeatureLevel for KRaft in the APIVersionsResponse only when the FinalizedFeatures is is not "empty."

private final Logger log;
private final FaultHandler faultHandler;
private volatile FinalizedFeatures finalizedFeatures = FinalizedFeatures.fromKRaftVersion(MINIMUM_VERSION);
private volatile FinalizedFeatures finalizedFeatures = new FinalizedFeatures(
Copy link
Contributor

Choose a reason for hiding this comment

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

We should make new FinalizedFeatures(Optional.empty()...) a static constant in FinalizedFeatures. Maybe something like UNKNOWN_FINALIZED_FEATURES.

// Nearly all RPCs should check MetadataVersion inside the QuorumController. However, this
// RPC is consulting a cache which lives outside the QC. So we check MetadataVersion here.
if (!apiVersionManager.features.metadataVersion().isControllerRegistrationSupported) {
if (!apiVersionManager.features.metadataVersion().map(_.isControllerRegistrationSupported).orElse(false)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be better if we could return a more accurate error message in the case where metadataVersion().isEmpty(). Returning a message like "There is no finalized MetadataVersion, so direct-to-controller communication is not supported" is more accurate in this case.

"supported with the current MetadataVersion.")
if (!apiVersionManager.features.metadataVersion().map(_.isControllerRegistrationSupported).orElse(false)) {
throw new UnsupportedVersionException("There is no finalized MetadataVersion, so " +
"direct-to-controller communication is not supported.")
Copy link
Contributor

Choose a reason for hiding this comment

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

Please make a separate if/else if clause for if the metadata version is not finalized. We should not change this existing check.

.setSupportedFeatures(brokerFeatures)
.setFinalizedFeatures(currentFeatures.finalizedFeatures())
.setFinalizedFeaturesEpoch(currentFeatures.finalizedFeaturesEpoch())
.setFinalizedFeatures(currentFeatures.map(FinalizedFeatures::finalizedFeatures).orElse(Map.of()))
Copy link
Contributor

Choose a reason for hiding this comment

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

I think if we want to expose the latest committed kraft version, we should do it in a separate PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants