Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,10 @@ subprojects {
def logStreams = new HashMap<String, FileOutputStream>()
beforeTest { TestDescriptor td ->
def tid = testId(td)
// truncate the file name if it's too long
def logFile = new File(
"${projectDir}/build/reports/testOutput/${tid}.test.stdout")
"${projectDir}/build/reports/testOutput/${tid.substring(0, Math.min(tid.size(),240))}.test.stdout"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Necessary because the test name that JUnit generates for the parameterized StreamsPartitionAssignorTest is slightly too long. I have no way to shorten it because the thing that pushes it over is the fact that there are two package names in the parameterized method name, and there's no control over the format of the test name itself. So, I decided just to truncate the file name instead, which is almost certainly still unique for pretty much any test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The only alternative I can think of is to parameterize the "short name" of the TaskAssignor, which seems kind of wacky.

Also, worth noting the impact of truncation is nothing if the file name is still unique. If the name is shared between two tests, then the impact is still nothing if both tests pass. The only observable effect is that if one or both tests fail, their logs would get combined. It seems like we can afford just to defer this problem until it happens, if ever.

)
logFile.parentFile.mkdirs()
logFiles.put(tid, logFile)
logStreams.put(tid, new FileOutputStream(logFile))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1146,4 +1146,13 @@ public Set<Characteristics> characteristics() {
}
};
}

@SafeVarargs
public static <E> Set<E> union(final Supplier<Set<E>> constructor, final Set<E>... set) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I've been wanting this for a while, so I just decided to add it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

req: Please add unit tests for this method

final Set<E> result = constructor.get();
for (final Set<E> s : set) {
result.addAll(s);
}
return result;
}
}
15 changes: 15 additions & 0 deletions clients/src/test/java/org/apache/kafka/common/utils/UtilsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import java.util.Properties;
import java.util.Random;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

Expand All @@ -47,7 +48,11 @@
import static org.apache.kafka.common.utils.Utils.getPort;
import static org.apache.kafka.common.utils.Utils.mkSet;
import static org.apache.kafka.common.utils.Utils.murmur2;
import static org.apache.kafka.common.utils.Utils.union;
import static org.apache.kafka.common.utils.Utils.validHostPattern;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
Expand Down Expand Up @@ -582,4 +587,14 @@ public void testConvertTo32BitField() {
} catch (IllegalArgumentException e) {
}
}

@Test
public void testUnion() {
final Set<String> oneSet = mkSet("a", "b", "c");
final Set<String> anotherSet = mkSet("c", "d", "e");
final Set<String> union = union(TreeSet::new, oneSet, anotherSet);

assertThat(union, is(mkSet("a", "b", "c", "d", "e")));
assertThat(union.getClass(), equalTo(TreeSet.class));
}
}
4 changes: 2 additions & 2 deletions clients/src/test/java/org/apache/kafka/test/TestUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -361,9 +361,9 @@ public static void waitForCondition(final TestCondition testCondition, final lon
* avoid transient failures due to slow or overloaded machines.
*/
public static void waitForCondition(final TestCondition testCondition, final long maxWaitMs, Supplier<String> conditionDetailsSupplier) throws InterruptedException {
String conditionDetailsSupplied = conditionDetailsSupplier != null ? conditionDetailsSupplier.get() : null;
String conditionDetails = conditionDetailsSupplied != null ? conditionDetailsSupplied : "";
retryOnExceptionWithTimeout(maxWaitMs, () -> {
String conditionDetailsSupplied = conditionDetailsSupplier != null ? conditionDetailsSupplier.get() : null;
String conditionDetails = conditionDetailsSupplied != null ? conditionDetailsSupplied : "";

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is pointless unless we evaluate it inside the lambda.

assertThat("Condition not met within timeout " + maxWaitMs + ". " + conditionDetails,
testCondition.conditionMet());
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,11 @@ public class StreamsConfig extends AbstractConfig {
}

public static class InternalConfig {
// This is settable in the main Streams config, but it's a private API for now
public static final String INTERNAL_TASK_ASSIGNOR_CLASS = "internal.task.assignor.class";

// These are not settable in the main Streams config; they are set by the StreamThread to pass internal
// state into the assignor.
public static final String TASK_MANAGER_FOR_PARTITION_ASSIGNOR = "__task.manager.instance__";
public static final String STREAMS_METADATA_STATE_FOR_PARTITION_ASSIGNOR = "__streams.metadata.state.instance__";
public static final String STREAMS_ADMIN_CLIENT = "__streams.admin.client.instance__";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
import org.apache.kafka.streams.processor.internals.assignment.AssignorError;
import org.apache.kafka.streams.processor.internals.assignment.ClientState;
import org.apache.kafka.streams.processor.internals.assignment.CopartitionedTopicsEnforcer;
import org.apache.kafka.streams.processor.internals.assignment.HighAvailabilityTaskAssignor;
import org.apache.kafka.streams.processor.internals.assignment.FallbackPriorTaskAssignor;
import org.apache.kafka.streams.processor.internals.assignment.StickyTaskAssignor;
import org.apache.kafka.streams.processor.internals.assignment.SubscriptionInfo;
import org.apache.kafka.streams.processor.internals.assignment.TaskAssignor;
Expand All @@ -64,6 +64,7 @@
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import static java.util.UUID.randomUUID;
Expand Down Expand Up @@ -171,7 +172,7 @@ public String toString() {
private CopartitionedTopicsEnforcer copartitionedTopicsEnforcer;
private RebalanceProtocol rebalanceProtocol;

private boolean highAvailabilityEnabled;
private Supplier<TaskAssignor> taskAssignorSupplier;

/**
* We need to have the PartitionAssignor and its StreamThread to be mutually accessible since the former needs
Expand Down Expand Up @@ -201,7 +202,7 @@ public void configure(final Map<String, ?> configs) {
internalTopicManager = assignorConfiguration.getInternalTopicManager();
copartitionedTopicsEnforcer = assignorConfiguration.getCopartitionedTopicsEnforcer();
rebalanceProtocol = assignorConfiguration.rebalanceProtocol();
highAvailabilityEnabled = assignorConfiguration.isHighAvailabilityEnabled();
taskAssignorSupplier = assignorConfiguration::getTaskAssignor;
}

@Override
Expand Down Expand Up @@ -361,7 +362,7 @@ public GroupAssignment assign(final Cluster metadata, final GroupSubscription gr
final Map<TaskId, Set<TopicPartition>> partitionsForTask =
partitionGrouper.partitionGroups(sourceTopicsByGroup, fullMetadata);

final boolean followupRebalanceNeeded =
final boolean probingRebalanceNeeded =
assignTasksToClients(allSourceTopics, partitionsForTask, topicGroups, clientMetadataMap, fullMetadata);

// ---------------- Step Three ---------------- //
Expand Down Expand Up @@ -399,7 +400,7 @@ public GroupAssignment assign(final Cluster metadata, final GroupSubscription gr
allOwnedPartitions,
minReceivedMetadataVersion,
minSupportedMetadataVersion,
followupRebalanceNeeded
probingRebalanceNeeded
);
}

Expand Down Expand Up @@ -688,7 +689,7 @@ private Map<TaskId, Set<TopicPartition>> prepareChangelogTopics(final Map<Intege

/**
* Assigns a set of tasks to each client (Streams instance) using the configured task assignor
* @return true if a followup rebalance should be triggered
* @return true if a probing rebalance should be triggered
*/
private boolean assignTasksToClients(final Set<String> allSourceTopics,
final Map<TaskId, Set<TopicPartition>> partitionsForTask,
Expand All @@ -712,29 +713,32 @@ private boolean assignTasksToClients(final Set<String> allSourceTopics,
log.debug("Assigning tasks {} to clients {} with number of replicas {}",
allTasks, clientStates, numStandbyReplicas());

final TaskAssignor taskAssignor;
if (highAvailabilityEnabled) {
if (lagComputationSuccessful) {
taskAssignor = new HighAvailabilityTaskAssignor(
clientStates,
allTasks,
statefulTasks,
assignmentConfigs);
} else {
log.info("Failed to fetch end offsets for changelogs, will return previous assignment to clients and "
+ "trigger another rebalance to retry.");
setAssignmentErrorCode(AssignorError.REBALANCE_NEEDED.code());
taskAssignor = new StickyTaskAssignor(clientStates, allTasks, statefulTasks, assignmentConfigs, true);
}
} else {
taskAssignor = new StickyTaskAssignor(clientStates, allTasks, statefulTasks, assignmentConfigs, false);
}
final boolean followupRebalanceNeeded = taskAssignor.assign();
final TaskAssignor taskAssignor = createTaskAssignor(lagComputationSuccessful);

final boolean probingRebalanceNeeded = taskAssignor.assign(clientStates,
allTasks,
statefulTasks,
assignmentConfigs);

log.info("Assigned tasks to clients as {}{}.",
Utils.NL, clientStates.entrySet().stream().map(Map.Entry::toString).collect(Collectors.joining(Utils.NL)));

return followupRebalanceNeeded;
return probingRebalanceNeeded;
}

private TaskAssignor createTaskAssignor(final boolean lagComputationSuccessful) {
final TaskAssignor taskAssignor = taskAssignorSupplier.get();
if (taskAssignor instanceof StickyTaskAssignor) {
// special case: to preserve pre-existing behavior, we invoke the StickyTaskAssignor
// whether or not lag computation failed.
return taskAssignor;
} else if (lagComputationSuccessful) {
return taskAssignor;
} else {
log.info("Failed to fetch end offsets for changelogs, will return previous assignment to clients and "
+ "trigger another rebalance to retry.");
return new FallbackPriorTaskAssignor();
}
}

/**
Expand Down Expand Up @@ -968,9 +972,9 @@ private void addClientAssignments(final Map<String, Assignment> assignment,
final int minUserMetadataVersion,
final int minSupportedMetadataVersion,
final boolean versionProbing,
final boolean followupRebalanceNeeded) {
boolean encodeNextRebalanceTime = followupRebalanceNeeded;
boolean stableAssignment = !followupRebalanceNeeded && !versionProbing;
final boolean probingRebalanceNeeded) {
boolean encodeNextRebalanceTime = probingRebalanceNeeded;
boolean stableAssignment = !probingRebalanceNeeded && !versionProbing;

// Loop through the consumers and build their assignment
for (final String consumer : clientMetadata.consumers) {
Expand Down Expand Up @@ -1025,7 +1029,7 @@ private void addClientAssignments(final Map<String, Assignment> assignment,
if (stableAssignment) {
log.info("Finished stable assignment of tasks, no followup rebalances required.");
} else {
log.info("Finished unstable assignment of tasks, a followup rebalance will be triggered.");
log.info("Finished unstable assignment of tasks, a followup probing rebalance will be triggered.");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
*/
package org.apache.kafka.streams.processor.internals.assignment;

import java.util.concurrent.atomic.AtomicLong;
import org.apache.kafka.clients.CommonClientConfigs;
import org.apache.kafka.clients.admin.Admin;
import org.apache.kafka.clients.admin.AdminClientConfig;
Expand All @@ -25,6 +24,7 @@
import org.apache.kafka.common.config.ConfigException;
import org.apache.kafka.common.utils.LogContext;
import org.apache.kafka.common.utils.Time;
import org.apache.kafka.common.utils.Utils;
import org.apache.kafka.streams.StreamsConfig;
import org.apache.kafka.streams.StreamsConfig.InternalConfig;
import org.apache.kafka.streams.internals.QuietStreamsConfig;
Expand All @@ -35,14 +35,15 @@

import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

import static org.apache.kafka.common.utils.Utils.getHost;
import static org.apache.kafka.common.utils.Utils.getPort;
import static org.apache.kafka.streams.StreamsConfig.InternalConfig.INTERNAL_TASK_ASSIGNOR_CLASS;
import static org.apache.kafka.streams.processor.internals.assignment.StreamsAssignmentProtocolVersions.LATEST_SUPPORTED_VERSION;

public final class AssignorConfiguration {
public static final String HIGH_AVAILABILITY_ENABLED_CONFIG = "internal.high.availability.enabled";
private final boolean highAvailabilityEnabled;
private final String taskAssignorClass;

private final String logPrefix;
private final Logger log;
Expand Down Expand Up @@ -162,11 +163,11 @@ public AssignorConfiguration(final Map<String, ?> configs) {
copartitionedTopicsEnforcer = new CopartitionedTopicsEnforcer(logPrefix);

{
final Object o = configs.get(HIGH_AVAILABILITY_ENABLED_CONFIG);
final String o = (String) configs.get(INTERNAL_TASK_ASSIGNOR_CLASS);
if (o == null) {
highAvailabilityEnabled = false;
taskAssignorClass = HighAvailabilityTaskAssignor.class.getName();
} else {
highAvailabilityEnabled = (Boolean) o;
taskAssignorClass = o;
}
}
}
Expand Down Expand Up @@ -328,8 +329,15 @@ public AssignmentConfigs getAssignmentConfigs() {
return assignmentConfigs;
}

public boolean isHighAvailabilityEnabled() {
return highAvailabilityEnabled;
public TaskAssignor getTaskAssignor() {
try {
return Utils.newInstance(taskAssignorClass, TaskAssignor.class);
} catch (final ClassNotFoundException e) {
throw new IllegalArgumentException(
"Expected an instantiable class name for " + INTERNAL_TASK_ASSIGNOR_CLASS,
e
);
}
}

public static class AssignmentConfigs {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
import java.util.Set;
import java.util.UUID;

import static java.util.Collections.emptyMap;
import static java.util.Collections.unmodifiableMap;
import static java.util.Collections.unmodifiableSet;
import static org.apache.kafka.common.utils.Utils.union;
import static org.apache.kafka.streams.processor.internals.assignment.SubscriptionInfo.UNKNOWN_OFFSET_SUM;

public class ClientState {
Expand Down Expand Up @@ -86,6 +90,22 @@ private ClientState(final Set<TaskId> activeTasks,
this.capacity = capacity;
}

public ClientState(final Set<TaskId> previousActiveTasks,
Comment thread
vvcephei marked this conversation as resolved.
final Set<TaskId> previousStandbyTasks,
final Map<TaskId, Long> taskLagTotals,
final int capacity) {
Comment on lines 93 to 96

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This constructor is currently only used in tests, but I'm planning a follow-on refactor that would actually use it from the StickyTaskAssignor as well.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm having a failure of imagination to see why any task assignor would ever be creating ClientState objects, but I eagerly wait to be enlightened.

activeTasks = new HashSet<>();
standbyTasks = new HashSet<>();
assignedTasks = new HashSet<>();
prevActiveTasks = unmodifiableSet(new HashSet<>(previousActiveTasks));
prevStandbyTasks = unmodifiableSet(new HashSet<>(previousStandbyTasks));
prevAssignedTasks = unmodifiableSet(union(HashSet::new, previousActiveTasks, previousStandbyTasks));
ownedPartitions = emptyMap();
taskOffsetSums = emptyMap();
this.taskLagTotals = unmodifiableMap(taskLagTotals);
this.capacity = capacity;
}

public ClientState copy() {
return new ClientState(
new HashSet<>(activeTasks),
Expand Down Expand Up @@ -258,7 +278,7 @@ boolean hasUnfulfilledQuota(final int tasksPerThread) {
}

boolean hasMoreAvailableCapacityThan(final ClientState other) {
if (this.capacity <= 0) {
if (capacity <= 0) {
throw new IllegalStateException("Capacity of this ClientState must be greater than 0.");
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.kafka.streams.processor.internals.assignment;

import org.apache.kafka.streams.processor.TaskId;
import org.apache.kafka.streams.processor.internals.assignment.AssignorConfiguration.AssignmentConfigs;

import java.util.Map;
import java.util.Set;
import java.util.UUID;

/**
* A special task assignor implementation to be used as a fallback in case the
* configured assignor couldn't be invoked.
*
* Specifically, this assignor must:
* 1. ignore the task lags in the ClientState map
* 2. always return true, indicating that a follow-up rebalance is needed

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Returning true here will schedule a followup rebalance at the probing interval, but we also schedule a followup rebalance immediately before instantiating this assignor (line 735). Is this intentional? IIUC your proposal was to trigger a followup rebalance right away, which we do by means of the assignment error code.

Of course, this is in memory so if the instance crashes and restarts we lose this information. I think we should actually avoid using the REBALANCE_NEEDED error code inside the assign method, and only allow. it during onAssignment. If we know that a followup rebalance is needed during assign we should just encode the nextScheduledRebalance with the current time

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ah, this is a good point. Actually, I overlooked line 735. I'll remove that one.

My proposal actually was to just wait for the probing rebalance interval in case the lag computation failed. It seems like this should be ok, since Streams will still make progress in the mean time, and it avoids the pathological case where we could just constantly rebalance if the end-offsets API is down for some reason.

*/
public class FallbackPriorTaskAssignor implements TaskAssignor {
private final StickyTaskAssignor delegate;
Comment on lines +26 to +35

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I renamed the PriorTaskAssignor and added a Javadoc to make its role clear.

Note that "PriorTaskAssignor" would be an appropriate behavioral name, except that it also always returns "true", and that it must ignore the lags, which is what makes it a "fallback" assignor here.


public FallbackPriorTaskAssignor() {
delegate = new StickyTaskAssignor(true);
}

@Override
public boolean assign(final Map<UUID, ClientState> clients,
final Set<TaskId> allTaskIds,
final Set<TaskId> standbyTaskIds,
final AssignmentConfigs configs) {
delegate.assign(clients, allTaskIds, standbyTaskIds, configs);
return true;
}
}
Loading