-
Notifications
You must be signed in to change notification settings - Fork 13.7k
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-13600: Kafka Streams - Fall back to most caught up client if no caught up clients exist #11760
Conversation
… all other replicas are behind
Thank you for the PR @tim-patterson ! I going to have a look at it tomorrow! |
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.
Thank you for the PR @tim-patterson !
I looked at the production code. I like your approach! I left some comments.
This or next week I will look at the test code.
final Map<UUID, ClientState> clientStates, | ||
final Map<TaskId, SortedSet<UUID>> tasksToCaughtUpClients, | ||
final Map<TaskId, List<UUID>> tasksToClientByLag) { | ||
final List<UUID> taskClients = requireNonNull(tasksToClientByLag.get(task), "uninitialized map"); |
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.
final List<UUID> taskClients = requireNonNull(tasksToClientByLag.get(task), "uninitialized map"); | |
final List<UUID> taskClients = requireNonNull(tasksToClientByLag.get(task), "uninitialized list"); |
if (sourceClient == null) { | ||
sourceClient = caughtUpClientsByTaskLoad.poll(movement.task); | ||
} | ||
|
||
if (sourceClient == null) { | ||
sourceClient = requireNonNull( | ||
mostCaughtUpEligibleClient(tasksToClientByLag, movement.task, movement.destination), | ||
"Tried to move task to more caught-up client but none exist" | ||
); | ||
} | ||
|
||
if (!clientStates.get(sourceClient).hasStandbyTask(movement.task)) { | ||
// there's not a standby available to take over the task, so we'll schedule a warmup instead |
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.
This part of the code is a bit hard to read. After it is clear that there is a client with a standby after line 117 (if sourceClient != null
), you could execute the content of the else branch on line 140. If there is no such client, then you can check for the caught up and most caught up client. Something like:
if (sourceClient != null) {
swapStandbyAndActive(...)
} else {
sourceClient = caughtUpClientsByTaskLoad.poll(movement.task);
if (sourceClient == null) {
sourceClient = requireNonNull(
mostCaughtUpEligibleClient(tasksToClientByLag, movement.task, movement.destination),
"Tried to move task to more caught-up client but none exist"
);
}
moveActiveAndTryToWarmUp(...)
}
caughtUpClientsByTaskLoad.offerAll(asList(sourceClient, movement.destination));
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.
I sort of originally did something like that , but a unit test caused a runtime assertion.
Basically there's ~5 cases.
- Caught up client with standby
- Caught up client without standby
- Not Caught up client with standby
- Not Caught up client without standby
- No client found (runtime assertion).
So the sourceClient
from mostCaughtUpEligibleClient(...)
may actually need to call swapStandbyAndActive(...)
depending on if it's actually got a standby assigned to it.
I guess can do something like
if (sourceClient != null) {
swapStandbyAndActive(...)
} else {
sourceClient = caughtUpClientsByTaskLoad.poll(movement.task);
if (sourceClient != null) {
moveActiveAndTryToWarmUp(...)
} else {
sourceClient = requireNonNull(
mostCaughtUpEligibleClient(tasksToClientByLag, movement.task, movement.destination),
"Tried to move task to more caught-up client but none exist"
);
if (clientStates.get(sourceClient).hasStandbyTask(movement.task)) {
swapStandbyAndActive(...)
} else {
moveActiveAndTryToWarmUp(...)
}
}
}
caughtUpClientsByTaskLoad.offerAll(asList(sourceClient, movement.destination));
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.
Ah, I totally missed case 3.
What about the following:
if (sourceClient != null) {
swapStandbyAndActive(...);
} else {
sourceClient = caughtUpClientsByTaskLoad.poll(movement.task);
if (sourceClient != null) {
moveActiveAndTryToWarmUp(...);
} else {
sourceClient = mostCaughtUpEligibleClient(tasksToClientByLag, movement.task, movement.destination);
if (sourceClient != null) {
if (clientStates.get(sourceClient).hasStandbyTask(movement.task)) {
swapStandbyAndActive(...);
} else {
moveActiveAndTryToWarmUp(...);
}
} else {
throw new IllegalStateException("Tried to move task to more caught-up client but none exist");
}
}
}
caughtUpClientsByTaskLoad.offerAll(asList(sourceClient, movement.destination));
I think it makes the cases more explicit in the code.
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.
We could even make it more explicit by introducing methods like:
private boolean tryToSwapStandbyAndActiveOnCaughtUpClient(...);
private boolean tryToMoveToCaughtUpClientAndTryToWarmUp(...);
private boolean tryToSwapStandbyAndActiveOnMostCaughtUpClient(...);
private boolean tryToMoveToMostCaughtUpClientAndTryToWarmUp(...);
But that is optional and up to you.
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.
I've done it with the nested if/else's but stopped reusing the same sourceClient var to try make it a little clearer.
Let me know what 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.
I think it is fine as you did. However, the method is already over 100 lines long. Maybe the code would benefit from a bit of refactoring. The method was already quite long before you added your part. WDYT?
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.
Sure let me have a bit of a play around.
All these methods being static means that there's a lot of state/method arguments to pass to each method call.
So I'm unsure about how much of a win we're going to get extracting smaller bits of code out into separate methods.
Maybe some closures/local functions might help....
I'll have a bit of a play and get back to you :)
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.
That sounds great! Thanks!
final UUID client, | ||
final Map<UUID, ClientState> clientStates, | ||
final Map<TaskId, SortedSet<UUID>> tasksToCaughtUpClients, | ||
final Map<TaskId, List<UUID>> tasksToClientByLag) { |
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.
Would it be possible to use a SortedSet
instead of a List
here? Would make it clearer that client IDs should only appear once in this list.
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.
Sure I've pushed up a commit that does this.
My only concern is that these SortedSet
's now end up holding a reference to clientStates
rather than just being a plain old list/set etc.
I run the Streams system tests on this branch twice yesterday and got twice the same failures. It might also be the tests, but it requires investigation. To run the system tests locally follow these instructions: |
Thanks @cadonna I've done some digging By doing a git bisect but checking out checking out the dockerfile with the typo fixed it looks like |
Thanks a lot for the investigation! @showuon Are you aware of any issues with your PR mentioned above? |
The most recent system test run on trunk I could find on Jenkins was one on February 9th. This run does not contain the system test failures I mentioned above. @tim-patterson Can you consistently reproduce the failures locally on trunk? |
Yes I seem to be able to, they're all timeout failures so maybe the extra latency of waiting for acks=all is enough to tip it over a timeout locally... Is there anyway to see what commit that system test from trunk actually ran on? So here's a trunk commit from yesterday
|
I wonder if this is the fix https://issues.apache.org/jira/browse/KAFKA-13673 |
@tim-patterson @cadonna , yes, we are aware of the failed system tests because someone reported last week: #11769. And yes, we have a PR (#11788) ready to fix it. Thanks. |
Thank you both for clearing this up! I also run the system tests on trunk and got the same failures. |
Hi @cadonna, I also split up that big method with a few helpers, WDYT? |
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.
@tim-patterson Thank you for the updates!
Here my feedback!
// caught up client. | ||
final boolean moved = tryToSwapStandbyAndActiveOnCaughtUpClient(clientStates, caughtUpClientsByTaskLoad, movement) || | ||
tryToMoveActiveToCaughtUpClientAndTryToWarmUp(clientStates, warmups, remainingWarmupReplicas, caughtUpClientsByTaskLoad, movement) || | ||
tryToMoveActiveToMostCaughtUpClient(tasksToClientByLag, clientStates, warmups, remainingWarmupReplicas, caughtUpClientsByTaskLoad, movement); |
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.
Could you add tests to TaskMovementTest
for case tryToMoveActiveToMostCaughtUpClient()
that you added?
streams/src/main/java/org/apache/kafka/streams/processor/internals/assignment/TaskMovement.java
Outdated
Show resolved
Hide resolved
…nals/assignment/TaskMovement.java Co-authored-by: Bruno Cadonna <cadonna@apache.org>
a35d4bd
to
d3798f5
Compare
@cadonna Hey Bruno, how's this? |
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 @tim-patterson !
LGTM!
I have Streams system tests and benchmarks running on this PR. Once they are done and they do not show any issue, I will merge the PR.
Streams system tests are green. Going to merge. |
… caught up clients exist (#11760) The task assignor is modified to consider the Streams client with the most caught up states if no Streams client exists that is caught up, i.e., the lag of the states on that client is less than the acceptable recovery lag. Unit test for case task assignment where no caught up nodes exist. Existing unit and integration tests to verify no other behaviour has been changed Co-authored-by: Bruno Cadonna <cadonna@apache.org> Reviewer: Bruno Cadonna <cadonna@apache.org>
Cherry-picked to 3.2 |
Unit test for case task assignment where no caught up nodes exist.
Existing unit and integration tests to verify no other behaviour has been changed
Committer Checklist (excluded from commit message)