Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

KAFKA-18844: Stale features information in QuorumController#registerBroker #18997

Open
wants to merge 2 commits into
base: trunk
Choose a base branch
from

Conversation

FrankYang0529
Copy link
Member

@FrankYang0529 FrankYang0529 commented Feb 21, 2025

In #16848, we added kraft.version to finalized features and got finalized features outside controller event handling thread. This makes finalized features may be stale when processing registerBroker event. Also, some cases like QuorumControllerTest.testBalancePartitionLeaders become flaky cause of outdated MV. This PR moves finalized features back to controller event handling thread to avoid the error.


Previous PR title: Fix flaky QuorumControllerTest.testBalancePartitionLeaders()
Previous PR description:
When we initialize FeatureControlManager in QuorumController [0], we don't set bootstrapMetadata for it, so it use Metadata.latestProduction() [1]. In the test case, we use IBP_3_7_IV0 as bootstrapMetadata [2]. When initializing QuorumController, it takes time to replay record to overwrite FeatureControlManager#metadataVersion. If we add Thread.sleep(1000L) before metadataVersion.set(mv) [3], we can reproduce the flaky test steadily.

I'm not sure whether setting bootstrapMetadata to FeatureControlManager is correct, so I fix the case by waiting metadataVersion is overwrote.

[0]

this.featureControl = new FeatureControlManager.Builder().
setLogContext(logContext).
setQuorumFeatures(quorumFeatures).
setSnapshotRegistry(snapshotRegistry).
setClusterFeatureSupportDescriber(clusterSupportDescriber).
build();

[1]

private MetadataVersion metadataVersion = MetadataVersion.latestProduction();

[2]

static final BootstrapMetadata SIMPLE_BOOTSTRAP = BootstrapMetadata.
fromVersion(MetadataVersion.IBP_3_7_IV0, "test-provided bootstrap");

[3]

public void replay(FeatureLevelRecord record) {
VersionRange range = quorumFeatures.localSupportedFeature(record.name());
if (!range.contains(record.featureLevel())) {
throw new RuntimeException("Tried to apply FeatureLevelRecord " + record + ", but this controller only " +
"supports versions " + range);
}
if (record.name().equals(MetadataVersion.FEATURE_NAME)) {
MetadataVersion mv = MetadataVersion.fromFeatureLevel(record.featureLevel());
metadataVersion.set(mv);

Committer Checklist (excluded from commit message)

  • Verify design and implementation
  • Verify test coverage and CI build status
  • Verify documentation (including upgrade notes)

@github-actions github-actions bot added tests Test fixes (including flaky tests) kraft small Small PRs labels Feb 21, 2025
@ijuma
Copy link
Member

ijuma commented Feb 21, 2025

This is related to #18845, thanks for the quick fix!

@ijuma
Copy link
Member

ijuma commented Feb 21, 2025

in QuorumController [0], we don't set bootstrapMetadata for it, so it use Metadata.latestProduction()

We tried to remove these defaults as part of #18845 - looks like I missed this one. Can you try removing the default? Is that an easy change or does it cause a lot of issues?

@FrankYang0529
Copy link
Member Author

FrankYang0529 commented Feb 21, 2025

Can you try removing the default?

I think we can't remove the default one. Each variable get default value if it's null. Or do you mean that trying to set bootstrapMetadata when initialize FeatureControlManager?

if (logContext == null) logContext = new LogContext();
if (snapshotRegistry == null) snapshotRegistry = new SnapshotRegistry(logContext);
if (quorumFeatures == null) {
Map<String, VersionRange> localSupportedFeatures = new HashMap<>();
localSupportedFeatures.put(MetadataVersion.FEATURE_NAME, VersionRange.of(
MetadataVersion.MINIMUM_VERSION.featureLevel(),
MetadataVersion.latestProduction().featureLevel()));
quorumFeatures = new QuorumFeatures(0, localSupportedFeatures, Collections.singletonList(0));
}
return new FeatureControlManager(
logContext,
quorumFeatures,
snapshotRegistry,
metadataVersion,
clusterSupportDescriber
);

@ijuma
Copy link
Member

ijuma commented Feb 21, 2025

Yes, if it's mandatory, then we should set it in the constructor. Any issues doing that?

Copy link
Contributor

@junrao junrao left a comment

Choose a reason for hiding this comment

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

@FrankYang0529 : Thanks for the PR.

Regarding FeatureControlManager.metadataVersion, it's bootstrapped from the bootstrap file or the log. Any test that depends on it should wait for the bootstrap to complete. So, it does seem reasonable to initialize it to null. @cmccabe : What do you think?

@@ -753,6 +753,10 @@ public void testBalancePartitionLeaders() throws Throwable {
QuorumController active = controlEnv.activeController();
Map<Integer, Long> brokerEpochs = new HashMap<>();

TestUtils.waitForCondition(() ->
active.featureControl().finalizedFeatures(Long.MAX_VALUE).featureMap().get(MetadataVersion.FEATURE_NAME) == SIMPLE_BOOTSTRAP.metadataVersion().featureLevel(),
Copy link
Contributor

Choose a reason for hiding this comment

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

Since we need to wait for QuorumController to fully bootstrap before doing the remaining test, it seems that we should just call controlEnv.activeController(true) here.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks for the suggestion. I think we can remove QuorumControllerTestEnv#activeController(boolean) function, because it's better for all cases get a real active controller. Other test cases like testUncleanShutdownBrokerElrEnabled also has similar error.

Screenshot 2025-02-22 at 10 37 39 AM

@ijuma
Copy link
Member

ijuma commented Feb 22, 2025

So, it does seem reasonable to initialize it to null. @cmccabe : What do you think?

It looks like this would basically always be null outside of tests and it would fail in the following line of the FeatureControlManager constructor:

this.metadataVersion = new TimelineObject<>(snapshotRegistry, metadataVersion);

It seems to me that we should actually remove the metadata version from FeatureControlManager.Builder and rely on FeatureControlManager.replay to set the metadata version.

@FrankYang0529
Copy link
Member Author

@ijuma, it looks like TimelineObject initial value cannot be null, so we can't remove metadata version from FeatureControlManager.Builder.

public TimelineObject(SnapshotRegistry snapshotRegistry, T initialValue) {
Objects.requireNonNull(initialValue);

@ijuma
Copy link
Member

ijuma commented Feb 23, 2025

@FrankYang0529 Yes, I know it cannot be null. My point is that you'd have to use TimelineObject<Optional<MetadataVersion>> or something long those lines.

@chia7712
Copy link
Member

It seems to me that we should actually remove the metadata version from FeatureControlManager.Builder and rely on FeatureControlManager.replay to set the metadata version.

How about initializing it to the bootstrapped metadata? If the role is quorum leader, it will eventually be initialized to the bootstrapped metadata. It is still valid as the "current" metadata version if the role is a standby controller.

@ijuma
Copy link
Member

ijuma commented Feb 24, 2025

@chia7712 Is the bootstrapped metadata available when FeatureControlManager is first initialized?

Copy link
Contributor

@junrao junrao left a comment

Choose a reason for hiding this comment

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

@FrankYang0529 : Thanks for the updated PR. A couple of more comments.

How about initializing it to the bootstrapped metadata? If the role is quorum leader, it will eventually be initialized to the bootstrapped metadata. It is still valid as the "current" metadata version if the role is a standby controller.

Chatted with Colin offline a bit. The bootstrap MV (from the bootstrap file) in QC can be higher or lower than the MV in the log. So, it should only be used if the log is empty. We could use MINIMUM_VERSION for initialization and the correct MV will be set after replaying the log. If the log is empty, in production, we prepend the bootstrap MV in QC. In testing, any test depending on the correct MV needs to explicitly ensure that the correct MV is populated.

return activeController(false);
}

QuorumController activeController(boolean waitForActivation) throws InterruptedException {
Copy link
Contributor

Choose a reason for hiding this comment

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

controlEnv.activeController() is called multiple times in some of the tests. It's a bit wasteful to have to append/wait for a new event in every call. Perhaps we could cache the returned QuorumController and reuse it in those tests.

@ijuma
Copy link
Member

ijuma commented Feb 24, 2025

We could use MINIMUM_VERSION for initialization and the correct MV will be set after replaying the log.

This is error-prone though. While working on #18845, we found an existing race condition that was hard to detect because we silently used the wrong metadata version in a number of situations. I fear this approach may lead to similar problems.

I think it's cleanest to leave the metadata version unset until we actually read it from the log.

@junrao
Copy link
Contributor

junrao commented Feb 24, 2025

I think it's cleanest to leave the metadata version unset until we actually read it from the log.

That's safer. The only question is what to do in FeatureControlManager.metadataVersion() when MV is not set yet. Returning an option requires every caller to check it. Perhaps we could throw an exception since the caller is not expected to use this method when MV is not set yet.

@ijuma
Copy link
Member

ijuma commented Feb 24, 2025

Yes, that's the approach we took with FeaturesImage. Most callers use metadataVersionOrThrow. The rare use cases that may call it before it's set call the version that returns Optional and keep retrying until it's set (which is the desired behavior - it's not safe for them to proceed until then).

@chia7712
Copy link
Member

that's the approach we took with FeaturesImage. Most callers use metadataVersionOrThrow. The rare use cases that may call it before it's set call the version that returns Optional and keep retrying until it's set (which is the desired behavior - it's not safe for them to proceed until then).

I think it includes following changes.

  1. remove metadataVersion from FeatureControlManager constructor, since the TimelineObject<MetadataVersion> will be changed to TimelineObject<Optional<MetadataVersion>>. And it is initialized with Optional.empty
  2. FeatureControlManager#metadataVersion should be changed to metadataVersionOrThrow, similar to FeaturesImage
  3. CompleteActivationEvent should NOT use FeatureControlManager#metadataVersionOrThrowif the log is empty, since the FeatureControlManager has not yet set the MV.

If above list is correct, maybe we can address it in follow-up?

@chia7712
Copy link
Member

If above list is correct, maybe we can address it in follow-up?

open https://issues.apache.org/jira/browse/KAFKA-18858

@junrao
Copy link
Contributor

junrao commented Feb 24, 2025

  1. CompleteActivationEvent should NOT use FeatureControlManager#metadataVersionOrThrow if the log is empty, since the FeatureControlManager has not yet set the MV.

We could change FeatureControlManager.metadataVersion to return an option and use it in CompleteActivationEvent. With this, we could remove the isEmpty param in ActivationRecordsGenerator.generate() since it's the same as metadataVersion being empty.

We could do 1-3 in a followup jira.

@@ -373,7 +373,7 @@ public void testElrEnabledByDefault() throws Throwable {
)).
build()
) {
controlEnv.activeController(true);
controlEnv.activeController();
Copy link
Contributor

Choose a reason for hiding this comment

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

Thinking a bit more. It seems that this test exposes a real bug introduced in https://github.com/apache/kafka/pull/16848/files#diff-77dc2adb187fd078084644613cff2b53021c8a5fbcdcfa116515734609d1332a.

In the following code, we pick up the finalized feature version (including MV) first and pass them to the registerBroker event. It's possible that the passed in MV is outdated, but when the registerBroker event is processed, the MV becomes update to date. This will fail a registerBroker request that shouldn't be failed. It seems that we should pick up the finalized feature version when the registerBroker event is processed.

public CompletableFuture<BrokerRegistrationReply> registerBroker(
    ControllerRequestContext context,
    BrokerRegistrationRequestData request
) {
    // populate finalized features map with latest known kraft version for validation
    Map<String, Short> controllerFeatures = new HashMap<>(featureControl.finalizedFeatures(Long.MAX_VALUE).featureMap());
    controllerFeatures.put(KRaftVersion.FEATURE_NAME, raftClient.kraftVersion().featureLevel());
    return appendWriteEvent("registerBroker", context.deadlineNs(),
        () -> clusterControl.
            registerBroker(request, offsetControl.nextWriteOffset(),
                new FinalizedControllerFeatures(controllerFeatures, Long.MAX_VALUE),
                context.requestHeader().requestApiVersion() >= 3),
        EnumSet.noneOf(ControllerOperationFlag.class));
}

Colin confirmed that this is an issue. In general, we should not be accessing featureControl.finalizedFeatures outside of the event queue thread since it introduces potential concurrency issues.

Copy link
Member Author

Choose a reason for hiding this comment

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

I try to change the function as following and the test can pass without active controller check.

    @Override
    public CompletableFuture<BrokerRegistrationReply> registerBroker(
        ControllerRequestContext context,
        BrokerRegistrationRequestData request
    ) {
        return appendWriteEvent("registerBroker", context.deadlineNs(),
            () -> {
                // populate finalized features map with latest known kraft version for validation
                Map<String, Short> controllerFeatures = new HashMap<>(featureControl.finalizedFeatures(Long.MAX_VALUE).featureMap());
                controllerFeatures.put(KRaftVersion.FEATURE_NAME, raftClient.kraftVersion().featureLevel());
                return clusterControl.
                    registerBroker(request, offsetControl.nextWriteOffset(),
                        new FinalizedControllerFeatures(controllerFeatures, Long.MAX_VALUE),
                        context.requestHeader().requestApiVersion() >= 3);
            },
            EnumSet.noneOf(ControllerOperationFlag.class));
    }

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, exactly. If we fix the registerBroker code, just waiting for the QC to be the leader should be enough.

Copy link
Member

Choose a reason for hiding this comment

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

Nice catch @junrao!

@ijuma
Copy link
Member

ijuma commented Feb 25, 2025

Once we merge this, we should also backport it to 4.0 so the build is stable there (and the bug is fixed).

Copy link
Contributor

@junrao junrao left a comment

Choose a reason for hiding this comment

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

@FrankYang0529 : Thanks for the updated PR. Just a minor comment. Also, could we change the description of the jira and the PR since this is not just a flaky test fix?

@ahuang98 and @jsancio : Should we cherry pick this to 3.9 since the issue was introduced in https://github.com/apache/kafka/pull/16848/files#diff-77dc2adb187fd078084644613cff2b53021c8a5fbcdcfa116515734609d1332a?

context.requestHeader().requestApiVersion() >= 3),
() -> {
// Populate finalized features map with latest known kraft version for validation.
// Get the finalized features map in controller operation to avoid outdated features.
Copy link
Contributor

Choose a reason for hiding this comment

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

in controller operation => in the controller event handling thread

Copy link
Member

@ijuma ijuma Feb 25, 2025

Choose a reason for hiding this comment

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

Isn't the important point that both reads and writes have to happen within the controller event handling thread (versus the more specific finalized features map part)? Overly specific comments tend to go stale more easily.

Signed-off-by: PoAn Yang <[email protected]>
Copy link
Contributor

@junrao junrao left a comment

Choose a reason for hiding this comment

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

@FrankYang0529 : Thanks for the updated PR. LGTM too assuming the tests pass. Also, could you change the title/description of the jira and the PR?

@FrankYang0529 FrankYang0529 changed the title KAFKA-18844: Fix flaky QuorumControllerTest.testBalancePartitionLeaders() KAFKA-18844: Stale features information in QuorumController#registerBroker Feb 26, 2025
@FrankYang0529
Copy link
Member Author

Hi @junrao, thanks for the review. Updated title and description on Jira and the PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
kraft small Small PRs tests Test fixes (including flaky tests)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants