Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
/*
* 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.clients.consumer.internals;

import org.apache.kafka.clients.ClientResponse;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.OffsetAndMetadata;
import org.apache.kafka.clients.consumer.RetriableCommitFailedException;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.message.OffsetCommitRequestData;
import org.apache.kafka.common.record.RecordBatch;
import org.apache.kafka.common.requests.OffsetCommitRequest;
import org.apache.kafka.common.utils.LogContext;
import org.apache.kafka.common.utils.Time;
import org.apache.kafka.common.utils.Timer;
import org.slf4j.Logger;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Queue;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;

public class CommitRequestManager implements RequestManager {
private final Queue<StagedCommit> stagedCommits;
// TODO: We will need to refactor the subscriptionState
private final SubscriptionState subscriptionState;

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 should finalize the plan for subscriptionState, whether we will reuse the same class or implement a new one (and deprecate the old one)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think the main problem with using the current SubscriptionState is that it couples together the commit position and the fetch position. With the move to the background thread, I don't think we can depend on these advancing in lock-step anymore. The fetch position can advance after we have received a fetch position and the commit position will be advanced when the application calls poll(). With that said, it is probably simpler to build this into the current SubscriptionState instead of creating something new.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

+1. I checked the current SusbscriptionState class and feel it's still resuable.

private final Logger log;
private final Optional<AutoCommitState> autoCommitState;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The optional is kind of annoying. I wonder if we could treat auto-commit disabled as having an auto-commit interval of Long.MaxVAlue.

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.

It makes sense to use a MaxValue as well, my counter argument is, i think explicitly disabling autoCommitState makes the logic more straightforward.

private final CoordinatorRequestManager coordinatorRequestManager;
private final GroupStateManager groupState;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'd suggest we either change the class name to just GroupState or name this field groupStateManager? Personally I prefer the former.

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. makes sense. thanks.


public CommitRequestManager(
Comment thread
philipnee marked this conversation as resolved.
Outdated
final Time time,
final LogContext logContext,
final SubscriptionState subscriptionState,
final ConsumerConfig config,
final CoordinatorRequestManager coordinatorRequestManager,
final GroupStateManager groupState) {
Objects.requireNonNull(coordinatorRequestManager, "Coordinator is needed upon committing offsets");
this.log = logContext.logger(getClass());
this.stagedCommits = new LinkedList<>();
if (config.getBoolean(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG)) {
final long autoCommitInterval =
Integer.toUnsignedLong(config.getInt(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG));
this.autoCommitState = Optional.of(new AutoCommitState(time, autoCommitInterval));
} else {
this.autoCommitState = Optional.empty();
}
this.coordinatorRequestManager = coordinatorRequestManager;
this.groupState = groupState;
this.subscriptionState = subscriptionState;
}

/**
* Poll for the commit request if there's any. The function will also try to autocommit, if enabled.
*
* @param currentTimeMs
* @return
*/
@Override
public NetworkClientDelegate.PollResult poll(final long currentTimeMs) {
maybeAutoCommit(currentTimeMs);

if (stagedCommits.isEmpty()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The stagedCommits seems never cleared?

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.

thanks, addressed.

return new NetworkClientDelegate.PollResult(Long.MAX_VALUE, new ArrayList<>());
}

List<NetworkClientDelegate.UnsentRequest> unsentCommitRequests =
stagedCommits.stream().map(StagedCommit::toUnsentRequest).collect(Collectors.toList());
return new NetworkClientDelegate.PollResult(Long.MAX_VALUE, Collections.unmodifiableList(unsentCommitRequests));
}

public CompletableFuture<ClientResponse> add(final Map<TopicPartition, OffsetAndMetadata> offsets) {
StagedCommit commit = new StagedCommit(
offsets,
groupState.groupId,
groupState.groupInstanceId.orElse(null),
groupState.generation);
this.stagedCommits.add(commit);
return commit.future();
}

private void maybeAutoCommit(final long currentTimeMs) {
Comment thread
philipnee marked this conversation as resolved.
Outdated
if (!autoCommitState.isPresent()) {
return;
}

AutoCommitState autocommit = autoCommitState.get();
if (!autocommit.canSendAutocommit(currentTimeMs)) {
return;
}

Map<TopicPartition, OffsetAndMetadata> allConsumedOffsets = subscriptionState.allConsumed();
log.debug("Auto-committing offsets {}", allConsumedOffsets);
sendAutoCommit(allConsumedOffsets);

@guozhangwang guozhangwang Feb 3, 2023

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm wondering if we should reset immediately or should we have a flag of autoCommitInFlight and only reset to re-enable canSendAutocommit, because I vaguely remember in the past, we have seen issues where auto commit keeps being triggered while there are still auto commits inflight due to network partition, causing OOM and other issues. And that's why we ended up adding this flag in the old code.

If we want to go very fancy, we can potentially just update the unsent auto commit request inside the stagedCommits when we want to send another..

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.

Hmm, I see your point. Let's track the inflight status in the autocommit state and reset them upon completing the future (in the callback).

autocommit.reset();
}

// Visible for testing
CompletableFuture<ClientResponse> sendAutoCommit(final Map<TopicPartition, OffsetAndMetadata> allConsumedOffsets) {
CompletableFuture<ClientResponse> future = this.add(allConsumedOffsets);
future.whenComplete((response, throwable) -> {
if (throwable == null) {
log.debug("Completed asynchronous auto-commit of offsets {}", allConsumedOffsets);
}

if (throwable instanceof RetriableCommitFailedException) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

merge this as else if along with line 124?

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 moved it to the exceptionally block, it think it would be clearer this way.

log.debug("Asynchronous auto-commit of offsets {} failed due to retriable error: {}", allConsumedOffsets,
throwable);
} else {
log.warn("Asynchronous auto-commit of offsets {} failed: {}", allConsumedOffsets,
throwable.getMessage());
}
});
return future;
}

public void clientPoll(final long currentTimeMs) {
this.autoCommitState.ifPresent(t -> t.ack(currentTimeMs));
}

private class StagedCommit {
private final Map<TopicPartition, OffsetAndMetadata> offsets;
private final String groupId;
private final GroupStateManager.Generation generation;
private final String groupInstanceId;
private final NetworkClientDelegate.FutureCompletionHandler future;

public StagedCommit(final Map<TopicPartition, OffsetAndMetadata> offsets,
final String groupId,
final String groupInstanceId,
final GroupStateManager.Generation generation) {
this.offsets = offsets;
// if no callback is provided, DefaultOffsetCommitCallback will be used.
this.future = new NetworkClientDelegate.FutureCompletionHandler();
this.groupId = groupId;
this.generation = generation;
this.groupInstanceId = groupInstanceId;
}

public CompletableFuture<ClientResponse> future() {
return future.future();
}

public NetworkClientDelegate.UnsentRequest toUnsentRequest() {
Map<String, OffsetCommitRequestData.OffsetCommitRequestTopic> requestTopicDataMap = new HashMap<>();
for (Map.Entry<TopicPartition, OffsetAndMetadata> entry : offsets.entrySet()) {
TopicPartition topicPartition = entry.getKey();
OffsetAndMetadata offsetAndMetadata = entry.getValue();

OffsetCommitRequestData.OffsetCommitRequestTopic topic = requestTopicDataMap
.getOrDefault(topicPartition.topic(),
new OffsetCommitRequestData.OffsetCommitRequestTopic()
.setName(topicPartition.topic())
);

topic.partitions().add(new OffsetCommitRequestData.OffsetCommitRequestPartition()
.setPartitionIndex(topicPartition.partition())
.setCommittedOffset(offsetAndMetadata.offset())
.setCommittedLeaderEpoch(offsetAndMetadata.leaderEpoch().orElse(RecordBatch.NO_PARTITION_LEADER_EPOCH))
.setCommittedMetadata(offsetAndMetadata.metadata())
);
requestTopicDataMap.put(topicPartition.topic(), topic);
}

OffsetCommitRequest.Builder builder = new OffsetCommitRequest.Builder(
new OffsetCommitRequestData()
.setGroupId(this.groupId)
.setGenerationId(generation.generationId)
.setMemberId(generation.memberId)
.setGroupInstanceId(groupInstanceId)
.setTopics(new ArrayList<>(requestTopicDataMap.values())));
return new NetworkClientDelegate.UnsentRequest(
builder,
coordinatorRequestManager.coordinator(),
future);
}
}

static class AutoCommitState {
private final Timer timer;
private final long autoCommitInterval;

public AutoCommitState(
final Time time,
final long autoCommitInterval) {
this.autoCommitInterval = autoCommitInterval;
this.timer = time.timer(autoCommitInterval);
}

public boolean canSendAutocommit(final long currentTimeMs) {
return this.timer.isExpired();
}

public void reset() {
this.timer.reset(autoCommitInterval);
}

public void ack(final long currentTimeMs) {
this.timer.update(currentTimeMs);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@
import org.apache.kafka.common.utils.Utils;
import org.slf4j.Logger;

import java.util.Iterator;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Queue;
Expand All @@ -46,7 +48,7 @@
* initialized by the polling thread.
*/
public class DefaultBackgroundThread extends KafkaThread {
private static final int MAX_POLL_TIMEOUT_MS = 5000;
private static final long MAX_POLL_TIMEOUT_MS = 5000;
private static final String BACKGROUND_THREAD_NAME =
"consumer_background_thread";
private final Time time;
Expand All @@ -56,13 +58,14 @@ public class DefaultBackgroundThread extends KafkaThread {
private final ConsumerMetadata metadata;
private final ConsumerConfig config;
// empty if groupId is null
private final Optional<CoordinatorRequestManager> coordinatorManager;
private final ApplicationEventProcessor applicationEventProcessor;
private final NetworkClientDelegate networkClientDelegate;
private final ErrorEventHandler errorEventHandler;

private final GroupStateManager groupState;
private boolean running;

private final Map<RequestManager.Type, Optional<RequestManager>> requestManagerRegistry;

// Visible for testing
DefaultBackgroundThread(final Time time,
final ConsumerConfig config,
Expand All @@ -73,7 +76,9 @@ public class DefaultBackgroundThread extends KafkaThread {
final ApplicationEventProcessor processor,
final ConsumerMetadata metadata,
final NetworkClientDelegate networkClient,
final CoordinatorRequestManager coordinatorManager) {
final GroupStateManager groupState,
final CoordinatorRequestManager coordinatorManager,
CommitRequestManager commitRequestManager) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why this is not final?

super(BACKGROUND_THREAD_NAME, true);
this.time = time;
this.running = true;
Expand All @@ -84,8 +89,12 @@ public class DefaultBackgroundThread extends KafkaThread {
this.config = config;
this.metadata = metadata;
this.networkClientDelegate = networkClient;
this.coordinatorManager = Optional.ofNullable(coordinatorManager);
this.errorEventHandler = errorEventHandler;
this.groupState = groupState;

this.requestManagerRegistry = new HashMap<>();
this.requestManagerRegistry.put(RequestManager.Type.COORDINATOR, Optional.ofNullable(coordinatorManager));
this.requestManagerRegistry.put(RequestManager.Type.COMMIT, Optional.ofNullable(commitRequestManager));
}
public DefaultBackgroundThread(final Time time,
final ConsumerConfig config,
Expand All @@ -111,23 +120,37 @@ public DefaultBackgroundThread(final Time time,
networkClient);
this.running = true;
this.errorEventHandler = new ErrorEventHandler(this.backgroundEventQueue);
String groupId = rebalanceConfig.groupId;
this.coordinatorManager = groupId == null ?
Optional.empty() :
Optional.of(new CoordinatorRequestManager(
time,
logContext,
config.getLong(ConsumerConfig.RETRY_BACKOFF_MS_CONFIG),
errorEventHandler,
groupId
));
this.applicationEventProcessor = new ApplicationEventProcessor(backgroundEventQueue);
this.groupState = new GroupStateManager(rebalanceConfig);
this.requestManagerRegistry = Collections.unmodifiableMap(buildRequestManagerRegistry(logContext));
this.applicationEventProcessor = new ApplicationEventProcessor(backgroundEventQueue, requestManagerRegistry);
} catch (final Exception e) {
close();
throw new KafkaException("Failed to construct background processor", e.getCause());
}
}

private Map<RequestManager.Type, Optional<RequestManager>> buildRequestManagerRegistry(final LogContext logContext) {

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.

Not sure if it is worth writing a builder.

Map<RequestManager.Type, Optional<RequestManager>> registry = new HashMap<>();
CoordinatorRequestManager coordinatorManager = groupState.groupId == null ?
null :
new CoordinatorRequestManager(
time,
logContext,
config.getLong(ConsumerConfig.RETRY_BACKOFF_MS_CONFIG),
errorEventHandler,
groupState.groupId);
// Add subscriptionState
CommitRequestManager commitRequestManager = coordinatorManager == null ?
null :
new CommitRequestManager(time,
logContext, null, config,
coordinatorManager,
groupState);
registry.put(RequestManager.Type.COORDINATOR, Optional.ofNullable(coordinatorManager));
registry.put(RequestManager.Type.COMMIT, Optional.ofNullable(commitRequestManager));
return registry;
}

@Override
public void run() {
try {
Expand All @@ -151,31 +174,25 @@ public void run() {

/**
* Poll and process an {@link ApplicationEvent}. It performs the following tasks:
* 1. Drains and try to process all of the requests in the queue.
* 2. Poll request managers to queue up the necessary requests.
* 1. Drains and try to process all the requests in the queue.
* 2. Iterate through the registry, poll, and get the next poll time for the network poll
* 3. Poll the networkClient to send and retrieve the response.
*/
void runOnce() {
drainAndProcess();

drain();
final long currentTimeMs = time.milliseconds();
long pollWaitTimeMs = MAX_POLL_TIMEOUT_MS;

if (coordinatorManager.isPresent()) {
pollWaitTimeMs = Math.min(
pollWaitTimeMs,
handlePollResult(coordinatorManager.get().poll(currentTimeMs)));
}

final long pollWaitTimeMs = requestManagerRegistry.values().stream()
.filter(Optional::isPresent)
.map(m -> m.get().poll(currentTimeMs))
.map(this::handlePollResult)
.reduce(MAX_POLL_TIMEOUT_MS, Math::min);
networkClientDelegate.poll(pollWaitTimeMs, currentTimeMs);
}

private void drainAndProcess() {
private void drain() {
Queue<ApplicationEvent> events = pollApplicationEvent();
Iterator<ApplicationEvent> iter = events.iterator();
while (iter.hasNext()) {
ApplicationEvent event = iter.next();
log.debug("processing application event: {}", event);
for (ApplicationEvent event : events) {
log.debug("Consuming application event: {}", event);
consumeApplicationEvent(event);
}
}
Expand All @@ -198,7 +215,6 @@ private Queue<ApplicationEvent> pollApplicationEvent() {
}

private void consumeApplicationEvent(final ApplicationEvent event) {
log.debug("try consuming event: {}", Optional.ofNullable(event));
Objects.requireNonNull(event);
applicationEventProcessor.process(event);
}
Expand Down
Loading