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
2 changes: 1 addition & 1 deletion checkstyle/suppressions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@

<!-- metadata -->
<suppress checks="ClassDataAbstractionCoupling"
files="(ReplicationControlManager|ReplicationControlManagerTest).java"/>
files="(QuorumControllerTest|ReplicationControlManager|ReplicationControlManagerTest).java"/>
<suppress checks="ClassFanOutComplexity"
files="(QuorumController|ReplicationControlManager).java"/>
<suppress checks="ParameterNumber"
Expand Down
15 changes: 9 additions & 6 deletions core/src/main/scala/kafka/server/ControllerApis.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import java.util
import java.util.Collections
import java.util.Map.Entry
import java.util.concurrent.{CompletableFuture, ExecutionException}
import java.util.concurrent.TimeUnit.{MILLISECONDS, NANOSECONDS}
import kafka.network.RequestChannel
import kafka.raft.RaftManager
import kafka.server.QuotaFactory.QuotaManagers
Expand Down Expand Up @@ -204,6 +205,7 @@ class ControllerApis(val requestChannel: RequestChannel,
throw new TopicDeletionDisabledException()
}
}
val deadlineNs = time.nanoseconds() + NANOSECONDS.convert(request.timeoutMs, MILLISECONDS);
// The first step is to load up the names and IDs that have been provided by the
// request. This is a bit messy because we support multiple ways of referring to
// topics (both by name and by id) and because we need to check for duplicates or
Expand Down Expand Up @@ -256,7 +258,7 @@ class ControllerApis(val requestChannel: RequestChannel,
val toAuthenticate = new util.HashSet[String]
toAuthenticate.addAll(providedNames)
val idToName = new util.HashMap[Uuid, String]
controller.findTopicNames(providedIds).thenCompose { topicNames =>
controller.findTopicNames(deadlineNs, providedIds).thenCompose { topicNames =>
topicNames.forEach { (id, nameOrError) =>
if (nameOrError.isError) {
appendResponse(null, id, nameOrError.error())
Expand Down Expand Up @@ -291,7 +293,7 @@ class ControllerApis(val requestChannel: RequestChannel,
}
// For each topic that was provided by name, check if authentication failed.
// If so, create an error response for it. Otherwise, add it to the idToName map.
controller.findTopicIds(providedNames).thenCompose { topicIds =>
controller.findTopicIds(deadlineNs, providedNames).thenCompose { topicIds =>
topicIds.forEach { (name, idOrError) =>
if (!describeable.contains(name)) {
appendResponse(name, ZERO_UUID, new ApiError(TOPIC_AUTHORIZATION_FAILED))
Expand All @@ -315,7 +317,7 @@ class ControllerApis(val requestChannel: RequestChannel,
}
// Finally, the idToName map contains all the topics that we are authorized to delete.
// Perform the deletion and create responses for each one.
controller.deleteTopics(idToName.keySet).thenApply { idToError =>
controller.deleteTopics(deadlineNs, idToName.keySet).thenApply { idToError =>
idToError.forEach { (id, error) =>
appendResponse(idToName.get(id), id, error)
}
Expand Down Expand Up @@ -706,6 +708,7 @@ class ControllerApis(val requestChannel: RequestChannel,
hasClusterAuth: Boolean,
getCreatableTopics: Iterable[String] => Set[String])
: CompletableFuture[util.List[CreatePartitionsTopicResult]] = {
val deadlineNs = time.nanoseconds() + NANOSECONDS.convert(request.timeoutMs, MILLISECONDS);
val responses = new util.ArrayList[CreatePartitionsTopicResult]()
val duplicateTopicNames = new util.HashSet[String]()
val topicNames = new util.HashSet[String]()
Expand Down Expand Up @@ -739,7 +742,7 @@ class ControllerApis(val requestChannel: RequestChannel,
setErrorCode(TOPIC_AUTHORIZATION_FAILED.code))
}
}
controller.createPartitions(topics).thenApply { results =>
controller.createPartitions(deadlineNs, topics).thenApply { results =>
results.forEach(response => responses.add(response))
responses
}
Expand All @@ -750,14 +753,14 @@ class ControllerApis(val requestChannel: RequestChannel,
authHelper.authorizeClusterOperation(request, ALTER)
val response = controller.alterPartitionReassignments(alterRequest.data()).get()
requestHelper.sendResponseMaybeThrottle(request, requestThrottleMs =>
new AlterPartitionReassignmentsResponse(response))
new AlterPartitionReassignmentsResponse(response.setThrottleTimeMs(requestThrottleMs)))
}

def handleListPartitionReassignments(request: RequestChannel.Request): Unit = {
val listRequest = request.body[ListPartitionReassignmentsRequest]
authHelper.authorizeClusterOperation(request, DESCRIBE)
val response = controller.listPartitionReassignments(listRequest.data()).get()
requestHelper.sendResponseMaybeThrottle(request, requestThrottleMs =>
new ListPartitionReassignmentsResponse(response))
new ListPartitionReassignmentsResponse(response.setThrottleTimeMs(requestThrottleMs)))
}
}
8 changes: 4 additions & 4 deletions core/src/test/java/kafka/test/MockController.java
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ static class MockTopic {

@Override
synchronized public CompletableFuture<Map<String, ResultOrError<Uuid>>>
findTopicIds(Collection<String> topicNames) {
findTopicIds(long deadlineNs, Collection<String> topicNames) {
Map<String, ResultOrError<Uuid>> results = new HashMap<>();
for (String topicName : topicNames) {
if (!topicNameToId.containsKey(topicName)) {
Expand All @@ -158,7 +158,7 @@ static class MockTopic {

@Override
synchronized public CompletableFuture<Map<Uuid, ResultOrError<String>>>
findTopicNames(Collection<Uuid> topicIds) {
findTopicNames(long deadlineNs, Collection<Uuid> topicIds) {
Map<Uuid, ResultOrError<String>> results = new HashMap<>();
for (Uuid topicId : topicIds) {
MockTopic topic = topics.get(topicId);
Expand All @@ -173,7 +173,7 @@ static class MockTopic {

@Override
synchronized public CompletableFuture<Map<Uuid, ApiError>>
deleteTopics(Collection<Uuid> topicIds) {
deleteTopics(long deadlineNs, Collection<Uuid> topicIds) {
if (!active) {
CompletableFuture<Map<Uuid, ApiError>> future = new CompletableFuture<>();
future.completeExceptionally(NOT_CONTROLLER_EXCEPTION);
Expand Down Expand Up @@ -303,7 +303,7 @@ public CompletableFuture<Void> waitForReadyBrokers(int minBrokers) {

@Override
synchronized public CompletableFuture<List<CreatePartitionsTopicResult>>
createPartitions(List<CreatePartitionsTopic> topicList) {
createPartitions(long deadlineNs, List<CreatePartitionsTopic> topicList) {
if (!active) {
CompletableFuture<List<CreatePartitionsTopicResult>> future = new CompletableFuture<>();
future.completeExceptionally(NOT_CONTROLLER_EXCEPTION);
Expand Down
23 changes: 17 additions & 6 deletions metadata/src/main/java/org/apache/kafka/controller/Controller.java
Original file line number Diff line number Diff line change
Expand Up @@ -80,27 +80,36 @@ public interface Controller extends AutoCloseable {
/**
* Find the ids for topic names.
*
* @param deadlineNs The time by which this operation needs to be complete, before
* we will complete this operation with a timeout.
* @param topicNames The topic names to resolve.
* @return A future yielding a map from topic name to id.
*/
CompletableFuture<Map<String, ResultOrError<Uuid>>> findTopicIds(Collection<String> topicNames);
CompletableFuture<Map<String, ResultOrError<Uuid>>> findTopicIds(long deadlineNs,
Collection<String> topicNames);

/**
* Find the names for topic ids.
*
* @param deadlineNs The time by which this operation needs to be complete, before
* we will complete this operation with a timeout.
* @param topicIds The topic ids to resolve.
* @return A future yielding a map from topic id to name.
*/
CompletableFuture<Map<Uuid, ResultOrError<String>>> findTopicNames(Collection<Uuid> topicIds);
CompletableFuture<Map<Uuid, ResultOrError<String>>> findTopicNames(long deadlineNs,
Collection<Uuid> topicIds);

/**
* Delete a batch of topics.
*
* @param deadlineNs The time by which this operation needs to be complete, before
* we will complete this operation with a timeout.
* @param topicIds The IDs of the topics to delete.
*
* @return A future yielding the response.
*/
CompletableFuture<Map<Uuid, ApiError>> deleteTopics(Collection<Uuid> topicIds);
CompletableFuture<Map<Uuid, ApiError>> deleteTopics(long deadlineNs,
Collection<Uuid> topicIds);

/**
* Describe the current configuration of various resources.
Expand Down Expand Up @@ -225,11 +234,13 @@ CompletableFuture<Map<ClientQuotaEntity, ApiError>> alterClientQuotas(
/**
* Create partitions on certain topics.
*
* @param topics The list of topics to create partitions for.
* @return A future yielding per-topic results.
* @param deadlineNs The time by which this operation needs to be complete, before
* we will complete this operation with a timeout.
* @param topics The list of topics to create partitions for.
* @return A future yielding per-topic results.
*/
CompletableFuture<List<CreatePartitionsTopicResult>>
createPartitions(List<CreatePartitionsTopic> topics);
createPartitions(long deadlineNs, List<CreatePartitionsTopic> topics);

/**
* Begin shutting down, but don't block. You must still call close to clean up all
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import java.util.OptionalLong;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
Expand Down Expand Up @@ -89,6 +90,7 @@
import org.slf4j.Logger;

import static java.util.concurrent.TimeUnit.MICROSECONDS;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.NANOSECONDS;


Expand Down Expand Up @@ -475,6 +477,12 @@ <T> CompletableFuture<T> appendReadEvent(String name, Supplier<T> handler) {
return event.future();
}

<T> CompletableFuture<T> appendReadEvent(String name, long deadlineNs, Supplier<T> handler) {
ControllerReadEvent<T> event = new ControllerReadEvent<T>(name, handler);
queue.appendWithDeadline(deadlineNs, event);
return event.future();
}

interface ControllerWriteOperation<T> {
/**
* Generate the metadata records needed to implement this controller write
Expand Down Expand Up @@ -602,11 +610,10 @@ public String toString() {
}

private <T> CompletableFuture<T> appendWriteEvent(String name,
long timeoutMs,
long deadlineNs,
ControllerWriteOperation<T> op) {
ControllerWriteEvent<T> event = new ControllerWriteEvent<>(name, op);
queue.appendWithDeadline(time.nanoseconds() +
NANOSECONDS.convert(timeoutMs, TimeUnit.MILLISECONDS), event);
queue.appendWithDeadline(deadlineNs, event);
return event.future();
}

Expand Down Expand Up @@ -961,8 +968,9 @@ public CompletableFuture<AlterIsrResponseData> alterIsr(AlterIsrRequestData requ
if (request.topics().isEmpty()) {
return CompletableFuture.completedFuture(new CreateTopicsResponseData());
}
return appendWriteEvent("createTopics", () ->
replicationControl.createTopics(request));
return appendWriteEvent("createTopics",
time.nanoseconds() + NANOSECONDS.convert(request.timeoutMs(), MILLISECONDS),
() -> replicationControl.createTopics(request));
}

@Override
Expand All @@ -972,23 +980,26 @@ public CompletableFuture<Void> unregisterBroker(int brokerId) {
}

@Override
public CompletableFuture<Map<String, ResultOrError<Uuid>>> findTopicIds(Collection<String> names) {
public CompletableFuture<Map<String, ResultOrError<Uuid>>> findTopicIds(long deadlineNs,
Collection<String> names) {
if (names.isEmpty()) return CompletableFuture.completedFuture(Collections.emptyMap());
return appendReadEvent("findTopicIds",
return appendReadEvent("findTopicIds", deadlineNs,
() -> replicationControl.findTopicIds(lastCommittedOffset, names));
}

@Override
public CompletableFuture<Map<Uuid, ResultOrError<String>>> findTopicNames(Collection<Uuid> ids) {
public CompletableFuture<Map<Uuid, ResultOrError<String>>> findTopicNames(long deadlineNs,
Collection<Uuid> ids) {
if (ids.isEmpty()) return CompletableFuture.completedFuture(Collections.emptyMap());
return appendReadEvent("findTopicNames",
return appendReadEvent("findTopicNames", deadlineNs,
() -> replicationControl.findTopicNames(lastCommittedOffset, ids));
}

@Override
public CompletableFuture<Map<Uuid, ApiError>> deleteTopics(Collection<Uuid> ids) {
public CompletableFuture<Map<Uuid, ApiError>> deleteTopics(long deadlineNs,
Collection<Uuid> ids) {
if (ids.isEmpty()) return CompletableFuture.completedFuture(Collections.emptyMap());
return appendWriteEvent("deleteTopics",
return appendWriteEvent("deleteTopics", deadlineNs,
() -> replicationControl.deleteTopics(ids));
}

Expand All @@ -1002,7 +1013,13 @@ public CompletableFuture<Map<Uuid, ApiError>> deleteTopics(Collection<Uuid> ids)
@Override
public CompletableFuture<ElectLeadersResponseData>
electLeaders(ElectLeadersRequestData request) {
return appendWriteEvent("electLeaders", request.timeoutMs(),
// If topicPartitions is null, we will try to trigger a new leader election on
// all partitions (!). But if it's empty, there is nothing to do.
if (request.topicPartitions() != null && request.topicPartitions().isEmpty()) {
return CompletableFuture.completedFuture(new ElectLeadersResponseData());
}
return appendWriteEvent("electLeaders",
time.nanoseconds() + NANOSECONDS.convert(request.timeoutMs(), MILLISECONDS),
Comment on lines 1016 to 1022

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.

Is this related to timeouts, or is it just a different fix?

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 a slightly different fix, although sort of related.

() -> replicationControl.electLeaders(request));
}

Expand All @@ -1016,6 +1033,9 @@ public CompletableFuture<FeatureMapAndEpoch> finalizedFeatures() {
public CompletableFuture<Map<ConfigResource, ApiError>> incrementalAlterConfigs(
Map<ConfigResource, Map<String, Entry<OpType, String>>> configChanges,
boolean validateOnly) {
if (configChanges.isEmpty()) {
return CompletableFuture.completedFuture(Collections.emptyMap());
}
return appendWriteEvent("incrementalAlterConfigs", () -> {
ControllerResult<Map<ConfigResource, ApiError>> result =
configurationControl.incrementalAlterConfigs(configChanges);
Expand All @@ -1030,17 +1050,24 @@ public CompletableFuture<Map<ConfigResource, ApiError>> incrementalAlterConfigs(
@Override
public CompletableFuture<AlterPartitionReassignmentsResponseData>
alterPartitionReassignments(AlterPartitionReassignmentsRequestData request) {
CompletableFuture<AlterPartitionReassignmentsResponseData> future = new CompletableFuture<>();
future.completeExceptionally(new UnsupportedOperationException());
return future;
if (request.topics().isEmpty()) {
return CompletableFuture.completedFuture(new AlterPartitionReassignmentsResponseData());
}
return appendWriteEvent("alterPartitionReassignments",
time.nanoseconds() + NANOSECONDS.convert(request.timeoutMs(), MILLISECONDS),
() -> {
throw new UnsupportedOperationException();
});
}

@Override
public CompletableFuture<ListPartitionReassignmentsResponseData>
listPartitionReassignments(ListPartitionReassignmentsRequestData request) {
CompletableFuture<ListPartitionReassignmentsResponseData> future = new CompletableFuture<>();
future.completeExceptionally(new UnsupportedOperationException());
return future;
return appendReadEvent("listPartitionReassignments",
time.nanoseconds() + NANOSECONDS.convert(request.timeoutMs(), MILLISECONDS),
() -> {
throw new UnsupportedOperationException();
});
Comment on lines 1068 to 1070

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.

nit: we can do without the curly braces here and above. However, these will soon be replaced by the actual impl

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.

agreed

}

@Override
Expand Down Expand Up @@ -1118,12 +1145,12 @@ public CompletableFuture<Map<ClientQuotaEntity, ApiError>> alterClientQuotas(

@Override
public CompletableFuture<List<CreatePartitionsTopicResult>>
createPartitions(List<CreatePartitionsTopic> topics) {
createPartitions(long deadlineNs, List<CreatePartitionsTopic> topics) {
if (topics.isEmpty()) {
return CompletableFuture.completedFuture(Collections.emptyList());
}
return appendWriteEvent("createPartitions", () ->
replicationControl.createPartitions(topics));
return appendWriteEvent("createPartitions", deadlineNs,
() -> replicationControl.createPartitions(topics));
}

@Override
Expand Down Expand Up @@ -1165,4 +1192,22 @@ public long curClaimEpoch() {
public void close() throws InterruptedException {
queue.close();
}

// VisibleForTesting
CountDownLatch pause() {
final CountDownLatch latch = new CountDownLatch(1);
appendControlEvent("pause", () -> {
try {
latch.await();
} catch (InterruptedException e) {
log.info("Interrupted while waiting for unpause.", e);
}
});
return latch;
}

// VisibleForTesting
Time time() {
return time;
}
Comment on lines 1210 to 1212

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.

Do we need this? Can't we just use the Time we pass into the constructor in tests? Not a big deal really, just wondering

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.

We have several helper classes for creating quorum controllers, so getting access to the time parameter that way would be difficult. Anyway, this is package-private, so it really only applies to unit tests specifically.

}
Loading