-
Notifications
You must be signed in to change notification settings - Fork 15.4k
MINOR: Fix flaky BrokerLifecycleManagerTest #14836
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -201,7 +201,7 @@ class BrokerLifecycleManagerTest { | |
| while (!future.isDone || context.mockClient.hasInFlightRequests) { | ||
| context.poll() | ||
| manager.eventQueue.wakeup() | ||
| context.time.sleep(100) | ||
| context.time.sleep(5) | ||
| } | ||
| future.get | ||
| } | ||
|
|
@@ -214,28 +214,20 @@ class BrokerLifecycleManagerTest { | |
| ctx.controllerNodeProvider.node.set(controllerNode) | ||
|
|
||
| val registration = prepareResponse(ctx, new BrokerRegistrationResponse(new BrokerRegistrationResponseData().setBrokerEpoch(1000))) | ||
| val heartbeats = Seq.fill(6)(prepareResponse[BrokerHeartbeatRequest](ctx, new BrokerHeartbeatResponse(new BrokerHeartbeatResponseData()))) | ||
|
|
||
| manager.start(() => ctx.highestMetadataOffset.get(), | ||
| ctx.mockChannelManager, ctx.clusterId, ctx.advertisedListeners, | ||
| Collections.emptyMap(), OptionalLong.empty()) | ||
| poll(ctx, manager, registration) | ||
|
|
||
| manager.propagateDirectoryFailure(Uuid.fromString("h3sC4Yk-Q9-fd0ntJTocCA")) | ||
| poll(ctx, manager, heartbeats(0)).data() | ||
| val dirs1 = poll(ctx, manager, heartbeats(1)).data().offlineLogDirs() | ||
|
|
||
| manager.propagateDirectoryFailure(Uuid.fromString("ej8Q9_d2Ri6FXNiTxKFiow")) | ||
| poll(ctx, manager, heartbeats(2)).data() | ||
| val dirs2 = poll(ctx, manager, heartbeats(3)).data().offlineLogDirs() | ||
|
|
||
| manager.propagateDirectoryFailure(Uuid.fromString("1iF76HVNRPqC7Y4r6647eg")) | ||
| poll(ctx, manager, heartbeats(4)).data() | ||
| val dirs3 = poll(ctx, manager, heartbeats(5)).data().offlineLogDirs() | ||
|
|
||
| assertEquals(Set("h3sC4Yk-Q9-fd0ntJTocCA").map(Uuid.fromString), dirs1.asScala.toSet) | ||
| assertEquals(Set("h3sC4Yk-Q9-fd0ntJTocCA", "ej8Q9_d2Ri6FXNiTxKFiow").map(Uuid.fromString), dirs2.asScala.toSet) | ||
| assertEquals(Set("h3sC4Yk-Q9-fd0ntJTocCA", "ej8Q9_d2Ri6FXNiTxKFiow", "1iF76HVNRPqC7Y4r6647eg").map(Uuid.fromString), dirs3.asScala.toSet) | ||
| val latestHeartbeat = Seq.fill(10)( | ||
| prepareResponse[BrokerHeartbeatRequest](ctx, new BrokerHeartbeatResponse(new BrokerHeartbeatResponseData())) | ||
| ).map(poll(ctx, manager, _)).last | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, KIP-858 says "The UUIDs for the newly failed log directories are included in the BrokerHeartbeat request until the broker receives a successful response.". How do we guarantee that only the 10th HeartbeatRequest picks up the failed log dirs?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I need to update the KIP. Last week in a disucssion with @cmccabe and @pprovenzano, we realized that because of overload mode for heartbeats, it will be easier to handle failed log directories if the broker always sends the accumulated list. Hence #14770 |
||
| assertEquals( | ||
| Set("h3sC4Yk-Q9-fd0ntJTocCA", "ej8Q9_d2Ri6FXNiTxKFiow", "1iF76HVNRPqC7Y4r6647eg").map(Uuid.fromString), | ||
| latestHeartbeat.data().offlineLogDirs().asScala.toSet) | ||
| manager.close() | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's causing the following failure before? Does this change fix the issue?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No. This can mitigate it, but it cannot prevent the race condition entirely.
AssignmentsManager has its own event loop thread that is batching and sending the accumulated failed directories. It's a bit tricky to predict the content of each request, so instead I opted to only assert after a few heartbeats.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the explanation, @soarez. There is a heartbeat request after the initial registration. Each
manager.propagateDirectoryFailurecould trigger a separate heartbeat request. So, is it true that after the 4th heartbeat, each heart request is guaranteed to include all three failed dirs?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's true in theory, but in this unit test, it is not.
I just tried with 6 heartbeats, and in 1,000 repetitions of the test, one of the runs was missing the last directory. On my laptop, with 7 heartbeats, 10,000 test runs had no failures.
I think this happens because of how
poll()works: it's rapidly advancing the clock and notifying three separate systems - BrokerLifecycleManager, MockClient and MockChannelManager — signaling them usingObject.notify(), which does not guarantee each of those threads will run straightaway, it's still up to the OS to schedule them onto the CPU.In practice, outside of unit tests, the delays in scheduling the BrokerLifecycleManager thread should be insignificant compared to the heartbeat interval. So I don't expect failures to be delayed for more than a heartbeat.
If the test proves to still be flaky even with 10 heartbeats, I think we can just increase the number.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@soarez : Thanks for the explanation. Currently, when processing a successful
HeartBeatResponse,BrokerLifecycleManagerautomatically schedules a newHeartBeatRequest, regardless whether there is a pending HeartBeatRequest inKafkaEventQueue. Normally, only BrokerRegistration triggers the initial HeartBeatRequest. So, there is only one HeartBeatRequest per heartbeat interval. However, eachmanager.propagateDirectoryFailurenow independently triggers a separateHeartBeatRequest. This means everymanager.propagateDirectoryFailurecall adds one moreHeartBeatRequestper heartbeat interval forever during the lifetime of a broker. This could unnecessarily overwhelm the controller. So, this seems to be a real issue?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for raising this @junrao
manager.propagateDirectoryFailureis called fromreplicaManager.handleLogDirFailurewhich handles directory failure only once for each directory. In most cases, the number of configured directories per broker should be under 10. Even if we consider 30, there can be 29 extraHeartBeatRequests (the last one shuts down the broker). Maybe I'm missing something, but I don't expect these to overwhelm the controller.That said, if you still think this may be an issue, we can avoid triggering a separate
HearBeatRequestand just wait for the next one. Otherwise, I think it makes sense not to delay updating leadership and ISR as necessary after any directory failure. What do you think?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@soarez : Just to be clear. In your example, there will be 29 extra
HeartBeatRequests continuously every heartbeat interval, right? It may not overwhelm the broker. But it's unnecessary work and makes debugging much harder.We don't necessarily need to delay the propagation of failed disks. For example, when adding HeartBeatRequest to the queue, if the schedule is earlier than a pending
HeartBeatRequestin the queue, we could cancel the pending request.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, I think it's just once. After receiving a response to a
HeartBeatRequest, aBrokerHeartbeatResponseEventruns which ends up callingscheduleNextCommunication. This queues a CommunicationEvent (which sends the next heartbeat) to the KafkaEventQueue using a tag. Because the tag is always the same, any previously queued event with the same tag is cancelled. So even if we receive 2HeartBeatRequestresponses in quick succession, we only send a single following heartbeat request.Taking another look at the code, I'm wrong about the extra requests,
propagateDirectoryFailuretriggers the "extra heartbeat" also usingscheduleNextCommunication— so the pendingHeartBeatRequestis cancelled indeed!There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@soarez : Thanks for the explanation. You are right that
KafkaEventQueuedoes de-duping and only allows one outstandingCommunicationEventin the queue. But it seems that duplicatedHeartbeatRequests could still be generated, which is causing the original transient failure.CommunicationEventcallssendBrokerHeartbeatthat calls the following._channelManager.sendRequest(new BrokerHeartbeatRequest.Builder(data), handler)The problem is that we have another queue in
NodeToControllerChannelManagerImplthat doesn't do the de-duping. Once aCommunicationEventis dequeued fromKafkaEventQueue, aHeartbeatRequestwill be queued inNodeToControllerChannelManagerImpl. At this point, anotherCommunicationEventcould be enqueued inKafkaEventQueue. When it's processed, anotherHeartbeatRequestwill be queued inNodeToControllerChannelManagerImpl.This probably won't introduce long lasting duplicated
HeartbeatRequestin practice sinceCommunicationEventis typically queued inKafkaEventQueuefor heartbeat interval. By that time, other pendingHeartbeatRequests will be processed and de-duped when enqueuing toKafkaEventQueue. But, maybe we could file a jira to track it.For the test, could we add a comment to explain why we need to wait for 10
HeartbeatRequests?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@junrao : That's a good point, you're right. In between
HeartbeatRequestbeing sent and the response being handled,propagateDirectoryFailurecould be called, immediately scheduling aHeartbeatRequest, causing an extra request.It looks like this was already the case with
setReadyToUnfence()andbeginControlledShutdown(), which can also cause an extra request in the same way.We can avoid the extra requests by checking - in
OfflineDirEvent.run,SetReadyToUnfenceEvent.runandBeginControlledShutdownEvent.run- whether a request is inflight, and delaying callingscheduleNextCommunicationImmediatelyuntil after the response is received.Please see #14874 about there explaining comment for the test.
I also see you've filed KAFKA-15950. Thanks, I'll have a look.