-
Notifications
You must be signed in to change notification settings - Fork 15.4k
KAFKA-13600: Kafka Streams - Fall back to most caught up client if no caught up clients exist #11760
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
Changes from 2 commits
382ae5a
6cec06c
5284d29
7b1e879
26149ab
67d5ba0
ec7e24c
2dfb79d
067897e
d3798f5
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 | ||||
|---|---|---|---|---|---|---|
|
|
@@ -20,6 +20,7 @@ | |||||
|
|
||||||
| import java.util.Collections; | ||||||
| import java.util.Comparator; | ||||||
| import java.util.List; | ||||||
| import java.util.Map; | ||||||
| import java.util.PriorityQueue; | ||||||
| import java.util.Queue; | ||||||
|
|
@@ -29,6 +30,7 @@ | |||||
| import java.util.UUID; | ||||||
| import java.util.concurrent.atomic.AtomicInteger; | ||||||
| import java.util.function.BiFunction; | ||||||
| import java.util.function.Function; | ||||||
|
|
||||||
| import static java.util.Arrays.asList; | ||||||
| import static java.util.Objects.requireNonNull; | ||||||
|
|
@@ -42,10 +44,6 @@ private TaskMovement(final TaskId task, final UUID destination, final SortedSet< | |||||
| this.task = task; | ||||||
| this.destination = destination; | ||||||
| this.caughtUpClients = caughtUpClients; | ||||||
|
|
||||||
| if (caughtUpClients == null || caughtUpClients.isEmpty()) { | ||||||
| throw new IllegalStateException("Should not attempt to move a task if no caught up clients exist"); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| private TaskId task() { | ||||||
|
|
@@ -56,25 +54,34 @@ private int numCaughtUpClients() { | |||||
| return caughtUpClients.size(); | ||||||
| } | ||||||
|
|
||||||
| private static boolean taskIsNotCaughtUpOnClientAndOtherCaughtUpClientsExist(final TaskId task, | ||||||
| final UUID client, | ||||||
| final Map<TaskId, SortedSet<UUID>> tasksToCaughtUpClients) { | ||||||
| return !taskIsCaughtUpOnClientOrNoCaughtUpClientsExist(task, client, tasksToCaughtUpClients); | ||||||
| private static boolean taskIsNotCaughtUpOnClientAndOtherMoreCaughtUpClientsExist(final TaskId task, | ||||||
| final UUID client, | ||||||
| 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"); | ||||||
|
Member
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.
Suggested change
|
||||||
| if (taskIsCaughtUpOnClient(task, client, tasksToCaughtUpClients)) { | ||||||
| return false; | ||||||
| } | ||||||
| final long mostCaughtUpLag = clientStates.get(taskClients.get(0)).lagFor(task); | ||||||
| final long clientLag = clientStates.get(client).lagFor(task); | ||||||
| return mostCaughtUpLag < clientLag; | ||||||
| } | ||||||
|
|
||||||
| private static boolean taskIsCaughtUpOnClientOrNoCaughtUpClientsExist(final TaskId task, | ||||||
| final UUID client, | ||||||
| final Map<TaskId, SortedSet<UUID>> tasksToCaughtUpClients) { | ||||||
| private static boolean taskIsCaughtUpOnClient(final TaskId task, | ||||||
| final UUID client, | ||||||
| final Map<TaskId, SortedSet<UUID>> tasksToCaughtUpClients) { | ||||||
| final Set<UUID> caughtUpClients = requireNonNull(tasksToCaughtUpClients.get(task), "uninitialized set"); | ||||||
| return caughtUpClients.isEmpty() || caughtUpClients.contains(client); | ||||||
| return caughtUpClients.contains(client); | ||||||
| } | ||||||
|
|
||||||
| static int assignActiveTaskMovements(final Map<TaskId, SortedSet<UUID>> tasksToCaughtUpClients, | ||||||
| final Map<TaskId, List<UUID>> tasksToClientByLag, | ||||||
| final Map<UUID, ClientState> clientStates, | ||||||
| final Map<UUID, Set<TaskId>> warmups, | ||||||
| final AtomicInteger remainingWarmupReplicas) { | ||||||
| final BiFunction<UUID, TaskId, Boolean> caughtUpPredicate = | ||||||
| (client, task) -> taskIsCaughtUpOnClientOrNoCaughtUpClientsExist(task, client, tasksToCaughtUpClients); | ||||||
| (client, task) -> taskIsCaughtUpOnClient(task, client, tasksToCaughtUpClients); | ||||||
|
|
||||||
| final ConstrainedPrioritySet caughtUpClientsByTaskLoad = new ConstrainedPrioritySet( | ||||||
| caughtUpPredicate, | ||||||
|
|
@@ -89,10 +96,10 @@ static int assignActiveTaskMovements(final Map<TaskId, SortedSet<UUID>> tasksToC | |||||
| final UUID client = clientStateEntry.getKey(); | ||||||
| final ClientState state = clientStateEntry.getValue(); | ||||||
| for (final TaskId task : state.activeTasks()) { | ||||||
| // if the desired client is not caught up, and there is another client that _is_ caught up, then | ||||||
| // we schedule a movement, so we can move the active task to the caught-up client. We'll try to | ||||||
| // if the desired client is not caught up, and there is another client that _is_ more caught up, then | ||||||
| // we schedule a movement, so we can move the active task to a more caught-up client. We'll try to | ||||||
| // assign a warm-up to the desired client so that we can move it later on. | ||||||
| if (taskIsNotCaughtUpOnClientAndOtherCaughtUpClientsExist(task, client, tasksToCaughtUpClients)) { | ||||||
| if (taskIsNotCaughtUpOnClientAndOtherMoreCaughtUpClientsExist(task, client, clientStates, tasksToCaughtUpClients, tasksToClientByLag)) { | ||||||
| taskMovements.add(new TaskMovement(task, client, tasksToCaughtUpClients.get(task))); | ||||||
| } | ||||||
| } | ||||||
|
|
@@ -102,17 +109,26 @@ static int assignActiveTaskMovements(final Map<TaskId, SortedSet<UUID>> tasksToC | |||||
| final int movementsNeeded = taskMovements.size(); | ||||||
|
|
||||||
| for (final TaskMovement movement : taskMovements) { | ||||||
| final UUID standbySourceClient = caughtUpClientsByTaskLoad.poll( | ||||||
| // Attempt to find a caught up standby, otherwise find any caught up client, failing that use the most | ||||||
| // caught up client. | ||||||
| UUID sourceClient = caughtUpClientsByTaskLoad.poll( | ||||||
| movement.task, | ||||||
| c -> clientStates.get(c).hasStandbyTask(movement.task) | ||||||
| ); | ||||||
| if (standbySourceClient == null) { | ||||||
| // there's not a caught-up standby available to take over the task, so we'll schedule a warmup instead | ||||||
| final UUID sourceClient = requireNonNull( | ||||||
| caughtUpClientsByTaskLoad.poll(movement.task), | ||||||
| "Tried to move task to caught-up client but none exist" | ||||||
|
|
||||||
| 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 | ||||||
|
Member
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. 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 (
Contributor
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 sort of originally did something like that , but a unit test caused a runtime assertion. Basically there's ~5 cases.
So the I guess can do something like
Member
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. Ah, I totally missed case 3. What about the following: I think it makes the cases more explicit in the code.
Member
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. We could even make it more explicit by introducing methods like: But that is optional and up to you.
Contributor
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've done it with the nested if/else's but stopped reusing the same sourceClient var to try make it a little clearer.
Member
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 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?
Contributor
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. Sure let me have a bit of a play around.
Member
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. That sounds great! Thanks! |
||||||
| moveActiveAndTryToWarmUp( | ||||||
| remainingWarmupReplicas, | ||||||
| movement.task, | ||||||
|
|
@@ -125,22 +141,23 @@ static int assignActiveTaskMovements(final Map<TaskId, SortedSet<UUID>> tasksToC | |||||
| // we found a candidate to trade standby/active state with our destination, so we don't need a warmup | ||||||
| swapStandbyAndActive( | ||||||
| movement.task, | ||||||
| clientStates.get(standbySourceClient), | ||||||
| clientStates.get(sourceClient), | ||||||
| clientStates.get(movement.destination) | ||||||
| ); | ||||||
| caughtUpClientsByTaskLoad.offerAll(asList(standbySourceClient, movement.destination)); | ||||||
| caughtUpClientsByTaskLoad.offerAll(asList(sourceClient, movement.destination)); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| return movementsNeeded; | ||||||
| } | ||||||
|
|
||||||
| static int assignStandbyTaskMovements(final Map<TaskId, SortedSet<UUID>> tasksToCaughtUpClients, | ||||||
| final Map<TaskId, List<UUID>> tasksToClientByLag, | ||||||
| final Map<UUID, ClientState> clientStates, | ||||||
| final AtomicInteger remainingWarmupReplicas, | ||||||
| final Map<UUID, Set<TaskId>> warmups) { | ||||||
| final BiFunction<UUID, TaskId, Boolean> caughtUpPredicate = | ||||||
| (client, task) -> taskIsCaughtUpOnClientOrNoCaughtUpClientsExist(task, client, tasksToCaughtUpClients); | ||||||
| (client, task) -> taskIsCaughtUpOnClient(task, client, tasksToCaughtUpClients); | ||||||
|
|
||||||
| final ConstrainedPrioritySet caughtUpClientsByTaskLoad = new ConstrainedPrioritySet( | ||||||
| caughtUpPredicate, | ||||||
|
|
@@ -157,8 +174,8 @@ static int assignStandbyTaskMovements(final Map<TaskId, SortedSet<UUID>> tasksTo | |||||
| for (final TaskId task : state.standbyTasks()) { | ||||||
| if (warmups.getOrDefault(destination, Collections.emptySet()).contains(task)) { | ||||||
| // this is a warmup, so we won't move it. | ||||||
| } else if (taskIsNotCaughtUpOnClientAndOtherCaughtUpClientsExist(task, destination, tasksToCaughtUpClients)) { | ||||||
| // if the desired client is not caught up, and there is another client that _is_ caught up, then | ||||||
| } else if (taskIsNotCaughtUpOnClientAndOtherMoreCaughtUpClientsExist(task, destination, clientStates, tasksToCaughtUpClients, tasksToClientByLag)) { | ||||||
| // if the desired client is not caught up, and there is another client that _is_ more caught up, then | ||||||
| // we schedule a movement, so we can move the active task to the caught-up client. We'll try to | ||||||
| // assign a warm-up to the desired client so that we can move it later on. | ||||||
| taskMovements.add(new TaskMovement(task, destination, tasksToCaughtUpClients.get(task))); | ||||||
|
|
@@ -170,11 +187,17 @@ static int assignStandbyTaskMovements(final Map<TaskId, SortedSet<UUID>> tasksTo | |||||
| int movementsNeeded = 0; | ||||||
|
|
||||||
| for (final TaskMovement movement : taskMovements) { | ||||||
| final UUID sourceClient = caughtUpClientsByTaskLoad.poll( | ||||||
| final Function<UUID, Boolean> eligibleClientPredicate = | ||||||
| clientId -> !clientStates.get(clientId).hasAssignedTask(movement.task); | ||||||
| UUID sourceClient = caughtUpClientsByTaskLoad.poll( | ||||||
| movement.task, | ||||||
| clientId -> !clientStates.get(clientId).hasAssignedTask(movement.task) | ||||||
| eligibleClientPredicate | ||||||
| ); | ||||||
|
|
||||||
| if (sourceClient == null) { | ||||||
| sourceClient = mostCaughtUpEligibleClient(tasksToClientByLag, eligibleClientPredicate, movement.task, movement.destination); | ||||||
| } | ||||||
|
|
||||||
| if (sourceClient == null) { | ||||||
| // then there's no caught-up client that doesn't already have a copy of this task, so there's | ||||||
| // nowhere to move it. | ||||||
|
|
@@ -235,4 +258,24 @@ private static void swapStandbyAndActive(final TaskId task, | |||||
| destinationClientState.assignStandby(task); | ||||||
| } | ||||||
|
|
||||||
| private static UUID mostCaughtUpEligibleClient(final Map<TaskId, List<UUID>> tasksToClientByLag, | ||||||
| final TaskId task, | ||||||
| final UUID destinationClient) { | ||||||
| return mostCaughtUpEligibleClient(tasksToClientByLag, client -> true, task, destinationClient); | ||||||
| } | ||||||
|
|
||||||
| private static UUID mostCaughtUpEligibleClient(final Map<TaskId, List<UUID>> tasksToClientByLag, | ||||||
| final Function<UUID, Boolean> constraint, | ||||||
| final TaskId task, | ||||||
| final UUID destinationClient) { | ||||||
| for (final UUID client : tasksToClientByLag.get(task)) { | ||||||
| if (destinationClient.equals(client)) { | ||||||
| break; | ||||||
| } else if (constraint.apply(client)) { | ||||||
| return client; | ||||||
| } | ||||||
| } | ||||||
| return null; | ||||||
| } | ||||||
|
|
||||||
| } | ||||||
Uh oh!
There was an error while loading. Please reload this page.
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
SortedSetinstead of aListhere? 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 toclientStatesrather than just being a plain old list/set etc.