Skip to content

Commit f20fdbd

Browse files
authored
KAFKA-12778: Fix QuorumController request timeouts and electLeaders (#10688)
The QuorumController should honor the timeout for RPC requests which feature a timeout. For electLeaders, attempt to trigger a leader election for all partitions when the request specifies null for the topics argument. Reviewers: David Arthur <[email protected]>
1 parent f2785f3 commit f20fdbd

File tree

7 files changed

+241
-48
lines changed

7 files changed

+241
-48
lines changed

checkstyle/suppressions.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@
270270

271271
<!-- metadata -->
272272
<suppress checks="ClassDataAbstractionCoupling"
273-
files="(ReplicationControlManager|ReplicationControlManagerTest).java"/>
273+
files="(QuorumControllerTest|ReplicationControlManager|ReplicationControlManagerTest).java"/>
274274
<suppress checks="ClassFanOutComplexity"
275275
files="(QuorumController|ReplicationControlManager).java"/>
276276
<suppress checks="ParameterNumber"

core/src/main/scala/kafka/server/ControllerApis.scala

+9-6
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import java.util
2121
import java.util.Collections
2222
import java.util.Map.Entry
2323
import java.util.concurrent.{CompletableFuture, ExecutionException}
24+
import java.util.concurrent.TimeUnit.{MILLISECONDS, NANOSECONDS}
2425
import kafka.network.RequestChannel
2526
import kafka.raft.RaftManager
2627
import kafka.server.QuotaFactory.QuotaManagers
@@ -204,6 +205,7 @@ class ControllerApis(val requestChannel: RequestChannel,
204205
throw new TopicDeletionDisabledException()
205206
}
206207
}
208+
val deadlineNs = time.nanoseconds() + NANOSECONDS.convert(request.timeoutMs, MILLISECONDS);
207209
// The first step is to load up the names and IDs that have been provided by the
208210
// request. This is a bit messy because we support multiple ways of referring to
209211
// topics (both by name and by id) and because we need to check for duplicates or
@@ -256,7 +258,7 @@ class ControllerApis(val requestChannel: RequestChannel,
256258
val toAuthenticate = new util.HashSet[String]
257259
toAuthenticate.addAll(providedNames)
258260
val idToName = new util.HashMap[Uuid, String]
259-
controller.findTopicNames(providedIds).thenCompose { topicNames =>
261+
controller.findTopicNames(deadlineNs, providedIds).thenCompose { topicNames =>
260262
topicNames.forEach { (id, nameOrError) =>
261263
if (nameOrError.isError) {
262264
appendResponse(null, id, nameOrError.error())
@@ -291,7 +293,7 @@ class ControllerApis(val requestChannel: RequestChannel,
291293
}
292294
// For each topic that was provided by name, check if authentication failed.
293295
// If so, create an error response for it. Otherwise, add it to the idToName map.
294-
controller.findTopicIds(providedNames).thenCompose { topicIds =>
296+
controller.findTopicIds(deadlineNs, providedNames).thenCompose { topicIds =>
295297
topicIds.forEach { (name, idOrError) =>
296298
if (!describeable.contains(name)) {
297299
appendResponse(name, ZERO_UUID, new ApiError(TOPIC_AUTHORIZATION_FAILED))
@@ -315,7 +317,7 @@ class ControllerApis(val requestChannel: RequestChannel,
315317
}
316318
// Finally, the idToName map contains all the topics that we are authorized to delete.
317319
// Perform the deletion and create responses for each one.
318-
controller.deleteTopics(idToName.keySet).thenApply { idToError =>
320+
controller.deleteTopics(deadlineNs, idToName.keySet).thenApply { idToError =>
319321
idToError.forEach { (id, error) =>
320322
appendResponse(idToName.get(id), id, error)
321323
}
@@ -706,6 +708,7 @@ class ControllerApis(val requestChannel: RequestChannel,
706708
hasClusterAuth: Boolean,
707709
getCreatableTopics: Iterable[String] => Set[String])
708710
: CompletableFuture[util.List[CreatePartitionsTopicResult]] = {
711+
val deadlineNs = time.nanoseconds() + NANOSECONDS.convert(request.timeoutMs, MILLISECONDS);
709712
val responses = new util.ArrayList[CreatePartitionsTopicResult]()
710713
val duplicateTopicNames = new util.HashSet[String]()
711714
val topicNames = new util.HashSet[String]()
@@ -739,7 +742,7 @@ class ControllerApis(val requestChannel: RequestChannel,
739742
setErrorCode(TOPIC_AUTHORIZATION_FAILED.code))
740743
}
741744
}
742-
controller.createPartitions(topics).thenApply { results =>
745+
controller.createPartitions(deadlineNs, topics).thenApply { results =>
743746
results.forEach(response => responses.add(response))
744747
responses
745748
}
@@ -750,14 +753,14 @@ class ControllerApis(val requestChannel: RequestChannel,
750753
authHelper.authorizeClusterOperation(request, ALTER)
751754
val response = controller.alterPartitionReassignments(alterRequest.data()).get()
752755
requestHelper.sendResponseMaybeThrottle(request, requestThrottleMs =>
753-
new AlterPartitionReassignmentsResponse(response))
756+
new AlterPartitionReassignmentsResponse(response.setThrottleTimeMs(requestThrottleMs)))
754757
}
755758

756759
def handleListPartitionReassignments(request: RequestChannel.Request): Unit = {
757760
val listRequest = request.body[ListPartitionReassignmentsRequest]
758761
authHelper.authorizeClusterOperation(request, DESCRIBE)
759762
val response = controller.listPartitionReassignments(listRequest.data()).get()
760763
requestHelper.sendResponseMaybeThrottle(request, requestThrottleMs =>
761-
new ListPartitionReassignmentsResponse(response))
764+
new ListPartitionReassignmentsResponse(response.setThrottleTimeMs(requestThrottleMs)))
762765
}
763766
}

core/src/test/java/kafka/test/MockController.java

+4-4
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ static class MockTopic {
144144

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

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

174174
@Override
175175
synchronized public CompletableFuture<Map<Uuid, ApiError>>
176-
deleteTopics(Collection<Uuid> topicIds) {
176+
deleteTopics(long deadlineNs, Collection<Uuid> topicIds) {
177177
if (!active) {
178178
CompletableFuture<Map<Uuid, ApiError>> future = new CompletableFuture<>();
179179
future.completeExceptionally(NOT_CONTROLLER_EXCEPTION);
@@ -303,7 +303,7 @@ public CompletableFuture<Void> waitForReadyBrokers(int minBrokers) {
303303

304304
@Override
305305
synchronized public CompletableFuture<List<CreatePartitionsTopicResult>>
306-
createPartitions(List<CreatePartitionsTopic> topicList) {
306+
createPartitions(long deadlineNs, List<CreatePartitionsTopic> topicList) {
307307
if (!active) {
308308
CompletableFuture<List<CreatePartitionsTopicResult>> future = new CompletableFuture<>();
309309
future.completeExceptionally(NOT_CONTROLLER_EXCEPTION);

metadata/src/main/java/org/apache/kafka/controller/Controller.java

+17-6
Original file line numberDiff line numberDiff line change
@@ -80,27 +80,36 @@ public interface Controller extends AutoCloseable {
8080
/**
8181
* Find the ids for topic names.
8282
*
83+
* @param deadlineNs The time by which this operation needs to be complete, before
84+
* we will complete this operation with a timeout.
8385
* @param topicNames The topic names to resolve.
8486
* @return A future yielding a map from topic name to id.
8587
*/
86-
CompletableFuture<Map<String, ResultOrError<Uuid>>> findTopicIds(Collection<String> topicNames);
88+
CompletableFuture<Map<String, ResultOrError<Uuid>>> findTopicIds(long deadlineNs,
89+
Collection<String> topicNames);
8790

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

96102
/**
97103
* Delete a batch of topics.
98104
*
105+
* @param deadlineNs The time by which this operation needs to be complete, before
106+
* we will complete this operation with a timeout.
99107
* @param topicIds The IDs of the topics to delete.
100108
*
101109
* @return A future yielding the response.
102110
*/
103-
CompletableFuture<Map<Uuid, ApiError>> deleteTopics(Collection<Uuid> topicIds);
111+
CompletableFuture<Map<Uuid, ApiError>> deleteTopics(long deadlineNs,
112+
Collection<Uuid> topicIds);
104113

105114
/**
106115
* Describe the current configuration of various resources.
@@ -225,11 +234,13 @@ CompletableFuture<Map<ClientQuotaEntity, ApiError>> alterClientQuotas(
225234
/**
226235
* Create partitions on certain topics.
227236
*
228-
* @param topics The list of topics to create partitions for.
229-
* @return A future yielding per-topic results.
237+
* @param deadlineNs The time by which this operation needs to be complete, before
238+
* we will complete this operation with a timeout.
239+
* @param topics The list of topics to create partitions for.
240+
* @return A future yielding per-topic results.
230241
*/
231242
CompletableFuture<List<CreatePartitionsTopicResult>>
232-
createPartitions(List<CreatePartitionsTopic> topics);
243+
createPartitions(long deadlineNs, List<CreatePartitionsTopic> topics);
233244

234245
/**
235246
* Begin shutting down, but don't block. You must still call close to clean up all

metadata/src/main/java/org/apache/kafka/controller/QuorumController.java

+66-21
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.util.OptionalLong;
2828
import java.util.Random;
2929
import java.util.concurrent.CompletableFuture;
30+
import java.util.concurrent.CountDownLatch;
3031
import java.util.concurrent.RejectedExecutionException;
3132
import java.util.concurrent.TimeUnit;
3233
import java.util.function.Function;
@@ -89,6 +90,7 @@
8990
import org.slf4j.Logger;
9091

9192
import static java.util.concurrent.TimeUnit.MICROSECONDS;
93+
import static java.util.concurrent.TimeUnit.MILLISECONDS;
9294
import static java.util.concurrent.TimeUnit.NANOSECONDS;
9395

9496

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

480+
<T> CompletableFuture<T> appendReadEvent(String name, long deadlineNs, Supplier<T> handler) {
481+
ControllerReadEvent<T> event = new ControllerReadEvent<T>(name, handler);
482+
queue.appendWithDeadline(deadlineNs, event);
483+
return event.future();
484+
}
485+
478486
interface ControllerWriteOperation<T> {
479487
/**
480488
* Generate the metadata records needed to implement this controller write
@@ -602,11 +610,10 @@ public String toString() {
602610
}
603611

604612
private <T> CompletableFuture<T> appendWriteEvent(String name,
605-
long timeoutMs,
613+
long deadlineNs,
606614
ControllerWriteOperation<T> op) {
607615
ControllerWriteEvent<T> event = new ControllerWriteEvent<>(name, op);
608-
queue.appendWithDeadline(time.nanoseconds() +
609-
NANOSECONDS.convert(timeoutMs, TimeUnit.MILLISECONDS), event);
616+
queue.appendWithDeadline(deadlineNs, event);
610617
return event.future();
611618
}
612619

@@ -961,8 +968,9 @@ public CompletableFuture<AlterIsrResponseData> alterIsr(AlterIsrRequestData requ
961968
if (request.topics().isEmpty()) {
962969
return CompletableFuture.completedFuture(new CreateTopicsResponseData());
963970
}
964-
return appendWriteEvent("createTopics", () ->
965-
replicationControl.createTopics(request));
971+
return appendWriteEvent("createTopics",
972+
time.nanoseconds() + NANOSECONDS.convert(request.timeoutMs(), MILLISECONDS),
973+
() -> replicationControl.createTopics(request));
966974
}
967975

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

974982
@Override
975-
public CompletableFuture<Map<String, ResultOrError<Uuid>>> findTopicIds(Collection<String> names) {
983+
public CompletableFuture<Map<String, ResultOrError<Uuid>>> findTopicIds(long deadlineNs,
984+
Collection<String> names) {
976985
if (names.isEmpty()) return CompletableFuture.completedFuture(Collections.emptyMap());
977-
return appendReadEvent("findTopicIds",
986+
return appendReadEvent("findTopicIds", deadlineNs,
978987
() -> replicationControl.findTopicIds(lastCommittedOffset, names));
979988
}
980989

981990
@Override
982-
public CompletableFuture<Map<Uuid, ResultOrError<String>>> findTopicNames(Collection<Uuid> ids) {
991+
public CompletableFuture<Map<Uuid, ResultOrError<String>>> findTopicNames(long deadlineNs,
992+
Collection<Uuid> ids) {
983993
if (ids.isEmpty()) return CompletableFuture.completedFuture(Collections.emptyMap());
984-
return appendReadEvent("findTopicNames",
994+
return appendReadEvent("findTopicNames", deadlineNs,
985995
() -> replicationControl.findTopicNames(lastCommittedOffset, ids));
986996
}
987997

988998
@Override
989-
public CompletableFuture<Map<Uuid, ApiError>> deleteTopics(Collection<Uuid> ids) {
999+
public CompletableFuture<Map<Uuid, ApiError>> deleteTopics(long deadlineNs,
1000+
Collection<Uuid> ids) {
9901001
if (ids.isEmpty()) return CompletableFuture.completedFuture(Collections.emptyMap());
991-
return appendWriteEvent("deleteTopics",
1002+
return appendWriteEvent("deleteTopics", deadlineNs,
9921003
() -> replicationControl.deleteTopics(ids));
9931004
}
9941005

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

@@ -1016,6 +1033,9 @@ public CompletableFuture<FeatureMapAndEpoch> finalizedFeatures() {
10161033
public CompletableFuture<Map<ConfigResource, ApiError>> incrementalAlterConfigs(
10171034
Map<ConfigResource, Map<String, Entry<OpType, String>>> configChanges,
10181035
boolean validateOnly) {
1036+
if (configChanges.isEmpty()) {
1037+
return CompletableFuture.completedFuture(Collections.emptyMap());
1038+
}
10191039
return appendWriteEvent("incrementalAlterConfigs", () -> {
10201040
ControllerResult<Map<ConfigResource, ApiError>> result =
10211041
configurationControl.incrementalAlterConfigs(configChanges);
@@ -1030,17 +1050,24 @@ public CompletableFuture<Map<ConfigResource, ApiError>> incrementalAlterConfigs(
10301050
@Override
10311051
public CompletableFuture<AlterPartitionReassignmentsResponseData>
10321052
alterPartitionReassignments(AlterPartitionReassignmentsRequestData request) {
1033-
CompletableFuture<AlterPartitionReassignmentsResponseData> future = new CompletableFuture<>();
1034-
future.completeExceptionally(new UnsupportedOperationException());
1035-
return future;
1053+
if (request.topics().isEmpty()) {
1054+
return CompletableFuture.completedFuture(new AlterPartitionReassignmentsResponseData());
1055+
}
1056+
return appendWriteEvent("alterPartitionReassignments",
1057+
time.nanoseconds() + NANOSECONDS.convert(request.timeoutMs(), MILLISECONDS),
1058+
() -> {
1059+
throw new UnsupportedOperationException();
1060+
});
10361061
}
10371062

10381063
@Override
10391064
public CompletableFuture<ListPartitionReassignmentsResponseData>
10401065
listPartitionReassignments(ListPartitionReassignmentsRequestData request) {
1041-
CompletableFuture<ListPartitionReassignmentsResponseData> future = new CompletableFuture<>();
1042-
future.completeExceptionally(new UnsupportedOperationException());
1043-
return future;
1066+
return appendReadEvent("listPartitionReassignments",
1067+
time.nanoseconds() + NANOSECONDS.convert(request.timeoutMs(), MILLISECONDS),
1068+
() -> {
1069+
throw new UnsupportedOperationException();
1070+
});
10441071
}
10451072

10461073
@Override
@@ -1118,12 +1145,12 @@ public CompletableFuture<Map<ClientQuotaEntity, ApiError>> alterClientQuotas(
11181145

11191146
@Override
11201147
public CompletableFuture<List<CreatePartitionsTopicResult>>
1121-
createPartitions(List<CreatePartitionsTopic> topics) {
1148+
createPartitions(long deadlineNs, List<CreatePartitionsTopic> topics) {
11221149
if (topics.isEmpty()) {
11231150
return CompletableFuture.completedFuture(Collections.emptyList());
11241151
}
1125-
return appendWriteEvent("createPartitions", () ->
1126-
replicationControl.createPartitions(topics));
1152+
return appendWriteEvent("createPartitions", deadlineNs,
1153+
() -> replicationControl.createPartitions(topics));
11271154
}
11281155

11291156
@Override
@@ -1165,4 +1192,22 @@ public long curClaimEpoch() {
11651192
public void close() throws InterruptedException {
11661193
queue.close();
11671194
}
1195+
1196+
// VisibleForTesting
1197+
CountDownLatch pause() {
1198+
final CountDownLatch latch = new CountDownLatch(1);
1199+
appendControlEvent("pause", () -> {
1200+
try {
1201+
latch.await();
1202+
} catch (InterruptedException e) {
1203+
log.info("Interrupted while waiting for unpause.", e);
1204+
}
1205+
});
1206+
return latch;
1207+
}
1208+
1209+
// VisibleForTesting
1210+
Time time() {
1211+
return time;
1212+
}
11681213
}

0 commit comments

Comments
 (0)