Skip to content
Closed
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 .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ jobs:
- name: Setup Gradle
uses: ./.github/actions/setup-gradle
with:
java-version: 17
java-version: 24
gradle-cache-read-only: ${{ !inputs.is-trunk }}
gradle-cache-write-only: ${{ inputs.is-trunk }}
develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,5 @@ storage/kafka-tiered-storage/
docker/test/report_*.html
kafka.Kafka
__pycache__

.idea/**/*
6 changes: 0 additions & 6 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,6 @@ ext {
"--add-opens=java.security.jgss/sun.security.krb5=ALL-UNNAMED"
)

if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_24)) {
// Spotbugs is not compatible with Java 24+ until Spotbugs 4.9.4. Disable it until we can upgrade to that version.
project.gradle.startParameter.excludedTaskNames.add("spotbugsMain")
project.gradle.startParameter.excludedTaskNames.add("spotbugsTest")
}

maxTestForks = project.hasProperty('maxParallelForks') ? maxParallelForks.toInteger() : Runtime.runtime.availableProcessors()
maxScalacThreads = project.hasProperty('maxScalacThreads') ? maxScalacThreads.toInteger() :
Math.min(Runtime.runtime.availableProcessors(), 8)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -928,6 +928,9 @@ private void close(final Duration timeout, final boolean swallowException) {
}

private void stopFindCoordinatorOnClose() {
if (applicationEventHandler == null) {
return;
}
log.debug("Stop finding coordinator during consumer close");
applicationEventHandler.add(new StopFindCoordinatorOnCloseEvent());
}
Expand All @@ -944,6 +947,10 @@ private Timer createTimerForCloseRequests(Duration timeout) {
* 2. leave the group
*/
private void sendAcknowledgementsAndLeaveGroup(final Timer timer, final AtomicReference<Throwable> firstException) {
if (applicationEventHandler == null || backgroundEventProcessor == null ||
backgroundEventReaper == null || backgroundEventQueue == null) {
return;
}
completeQuietly(
() -> applicationEventHandler.addAndGet(new ShareAcknowledgeOnCloseEvent(acknowledgementsToSend(), calculateDeadlineMs(timer))),
"Failed to send pending acknowledgements with a timeout(ms)=" + timer.timeoutMs(), firstException);
Expand Down Expand Up @@ -1035,6 +1042,9 @@ private void maybeThrowInvalidGroupIdException() {
* If the acknowledgement commit callback throws an exception, this method will throw an exception.
*/
private void handleCompletedAcknowledgements(boolean onClose) {
if (backgroundEventQueue == null || backgroundEventReaper == null || backgroundEventProcessor == null) {
return;
}
// If the user gets any fatal errors, they will get these exceptions in the background queue.
// While closing, we ignore these exceptions so that the consumers close successfully.
processBackgroundEvents(onClose ? e -> (e instanceof GroupAuthorizationException
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import org.apache.kafka.common.metrics.Metrics;
import org.apache.kafka.common.protocol.Errors;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.kafka.common.utils.LogCaptureAppender;
import org.apache.kafka.common.utils.LogContext;
import org.apache.kafka.common.utils.MockTime;
import org.apache.kafka.common.utils.Time;
Expand Down Expand Up @@ -77,6 +78,7 @@
import static java.util.Collections.singletonList;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
Expand Down Expand Up @@ -207,11 +209,19 @@ public void testFailConstructor() {
props.put(ConsumerConfig.GROUP_ID_CONFIG, "group-id");
props.put(ConsumerConfig.METRIC_REPORTER_CLASSES_CONFIG, "an.invalid.class");
final ConsumerConfig config = new ConsumerConfig(props);

LogCaptureAppender appender = LogCaptureAppender.createAndRegister();
KafkaException ce = assertThrows(
KafkaException.class,
() -> newConsumer(config));
assertTrue(ce.getMessage().contains("Failed to construct Kafka share consumer"), "Unexpected exception message: " + ce.getMessage());
assertTrue(ce.getCause().getMessage().contains("Class an.invalid.class cannot be found"), "Unexpected cause: " + ce.getCause());

boolean npeLogged = appender.getEvents().stream()
.flatMap(event -> event.getThrowableInfo().stream())
.anyMatch(str -> str.contains("NullPointerException"));

assertFalse(npeLogged, "Unexpected NullPointerException during consumer construction");
}

@Test
Expand Down
24 changes: 20 additions & 4 deletions core/src/main/java/kafka/server/share/DelayedShareFetch.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.apache.kafka.common.protocol.Errors;
import org.apache.kafka.common.requests.FetchRequest;
import org.apache.kafka.common.utils.Time;
import org.apache.kafka.raft.errors.NotLeaderException;
import org.apache.kafka.server.LogReadResult;
import org.apache.kafka.server.metrics.KafkaMetricsGroup;
import org.apache.kafka.server.purgatory.DelayedOperation;
Expand Down Expand Up @@ -368,6 +369,14 @@ public boolean tryComplete() {
"topic partitions {}", shareFetch.groupId(), shareFetch.memberId(),
sharePartitions.keySet());
}
// At this point, there could be delayed requests sitting in the purgatory which are waiting on
// DelayedShareFetchPartitionKeys corresponding to partitions, whose leader has been changed to a different broker.
// In that case, such partitions would not be able to get acquired, and the tryComplete will keep on returning false.
// Eventually the operation will get timed out and completed, but it might not get removed from the purgatory.
// This has been eventually left it like this because the purging mechanism will trigger whenever the number of completed
// but still being watched operations is larger than the purge interval. This purge interval is defined by the config
// share.fetch.purgatory.purge.interval.requests and is 1000 by default, thereby ensuring that such stale operations do not
// grow indefinitely.
return false;
} catch (Exception e) {
log.error("Error processing delayed share fetch request", e);
Expand Down Expand Up @@ -757,30 +766,37 @@ private void processRemoteFetchOrException(
* Case a: The partition is in an offline log directory on this broker
* Case b: This broker does not know the partition it tries to fetch
* Case c: This broker is no longer the leader of the partition it tries to fetch
* Case d: All remote storage read requests completed
* Case d: This broker is no longer the leader or follower of the partition it tries to fetch
* Case e: All remote storage read requests completed
* @return boolean representing whether the remote fetch is completed or not.
*/
private boolean maybeCompletePendingRemoteFetch() {
boolean canComplete = false;

for (TopicIdPartition topicIdPartition : pendingRemoteFetchesOpt.get().fetchOffsetMetadataMap().keySet()) {
try {
replicaManager.getPartitionOrException(topicIdPartition.topicPartition());
Partition partition = replicaManager.getPartitionOrException(topicIdPartition.topicPartition());
if (!partition.isLeader()) {
throw new NotLeaderException("Broker is no longer the leader of topicPartition: " + topicIdPartition);
}
} catch (KafkaStorageException e) { // Case a
log.debug("TopicPartition {} is in an offline log directory, satisfy {} immediately", topicIdPartition, shareFetch.fetchParams());
canComplete = true;
} catch (UnknownTopicOrPartitionException e) { // Case b
log.debug("Broker no longer knows of topicPartition {}, satisfy {} immediately", topicIdPartition, shareFetch.fetchParams());
canComplete = true;
} catch (NotLeaderOrFollowerException e) { // Case c
} catch (NotLeaderException e) { // Case c
log.debug("Broker is no longer the leader of topicPartition {}, satisfy {} immediately", topicIdPartition, shareFetch.fetchParams());
canComplete = true;
} catch (NotLeaderOrFollowerException e) { // Case d
log.debug("Broker is no longer the leader or follower of topicPartition {}, satisfy {} immediately", topicIdPartition, shareFetch.fetchParams());
canComplete = true;
}
if (canComplete)
break;
}

if (canComplete || pendingRemoteFetchesOpt.get().isDone()) { // Case d
if (canComplete || pendingRemoteFetchesOpt.get().isDone()) { // Case e
return forceComplete();
} else
return false;
Expand Down
57 changes: 18 additions & 39 deletions core/src/main/java/kafka/server/share/SharePartition.java
Original file line number Diff line number Diff line change
Expand Up @@ -803,7 +803,7 @@ public ShareAcquiredRecords acquire(
}

InFlightState updateResult = inFlightBatch.tryUpdateBatchState(RecordState.ACQUIRED, DeliveryCountOps.INCREASE, maxDeliveryCount, memberId);
if (updateResult == null) {
if (updateResult == null || updateResult.state() != RecordState.ACQUIRED) {
log.info("Unable to acquire records for the batch: {} in share partition: {}-{}",
inFlightBatch, groupId, topicIdPartition);
continue;
Expand Down Expand Up @@ -1009,12 +1009,7 @@ private Optional<Throwable> releaseAcquiredRecordsForPerOffsetBatch(String membe
updatedStates.add(updateResult);
stateBatches.add(new PersisterStateBatch(offsetState.getKey(), offsetState.getKey(),
updateResult.state().id(), (short) updateResult.deliveryCount()));

// If the maxDeliveryCount limit has been exceeded, the record will be transitioned to ARCHIVED state.
// This should not change the next fetch offset because the record is not available for acquisition
if (updateResult.state() != RecordState.ARCHIVED) {
updateFindNextFetchOffset(true);
}
// Do not update the next fetch offset as the offset has not completed the transition yet.
}
}
return Optional.empty();
Expand Down Expand Up @@ -1054,12 +1049,7 @@ private Optional<Throwable> releaseAcquiredRecordsForCompleteBatch(String member
updatedStates.add(updateResult);
stateBatches.add(new PersisterStateBatch(inFlightBatch.firstOffset(), inFlightBatch.lastOffset(),
updateResult.state().id(), (short) updateResult.deliveryCount()));

// If the maxDeliveryCount limit has been exceeded, the record will be transitioned to ARCHIVED state.
// This should not change the next fetch offset because the record is not available for acquisition
if (updateResult.state() != RecordState.ARCHIVED) {
updateFindNextFetchOffset(true);
}
// Do not update the next fetch offset as the batch has not completed the transition yet.
}
return Optional.empty();
}
Expand Down Expand Up @@ -1641,7 +1631,7 @@ private int acquireSubsetBatchRecords(

InFlightState updateResult = offsetState.getValue().tryUpdateState(RecordState.ACQUIRED, DeliveryCountOps.INCREASE,
maxDeliveryCount, memberId);
if (updateResult == null) {
if (updateResult == null || updateResult.state() != RecordState.ACQUIRED) {
log.trace("Unable to acquire records for the offset: {} in batch: {}"
+ " for the share partition: {}-{}", offsetState.getKey(), inFlightBatch,
groupId, topicIdPartition);
Expand Down Expand Up @@ -1941,12 +1931,7 @@ private Optional<Throwable> acknowledgePerOffsetBatchRecords(
updatedStates.add(updateResult);
stateBatches.add(new PersisterStateBatch(offsetState.getKey(), offsetState.getKey(),
updateResult.state().id(), (short) updateResult.deliveryCount()));
// If the maxDeliveryCount limit has been exceeded, the record will be transitioned to ARCHIVED state.
// This should not change the next fetch offset because the record is not available for acquisition
if (recordState == RecordState.AVAILABLE
&& updateResult.state() != RecordState.ARCHIVED) {
updateFindNextFetchOffset(true);
}
// Do not update the nextFetchOffset as the offset has not completed the transition yet.
}
} finally {
lock.writeLock().unlock();
Expand Down Expand Up @@ -1996,13 +1981,7 @@ private Optional<Throwable> acknowledgeCompleteBatch(
stateBatches.add(
new PersisterStateBatch(inFlightBatch.firstOffset(), inFlightBatch.lastOffset(),
updateResult.state().id(), (short) updateResult.deliveryCount()));

// If the maxDeliveryCount limit has been exceeded, the record will be transitioned to ARCHIVED state.
// This should not change the nextFetchOffset because the record is not available for acquisition
if (recordState == RecordState.AVAILABLE
&& updateResult.state() != RecordState.ARCHIVED) {
updateFindNextFetchOffset(true);
}
// Do not update the next fetch offset as the batch has not completed the transition yet.
} finally {
lock.writeLock().unlock();
}
Expand Down Expand Up @@ -2445,22 +2424,22 @@ && checkForStartOffsetWithinBatch(inFlightBatch.firstOffset(), inFlightBatch.las
releaseAcquisitionLockOnTimeoutForPerOffsetBatch(inFlightBatch, stateBatches, memberId, firstOffset, lastOffset);
}
}

if (!stateBatches.isEmpty()) {
writeShareGroupState(stateBatches).whenComplete((result, exception) -> {
if (exception != null) {
log.debug("Failed to write the share group state on acquisition lock timeout for share partition: {}-{} memberId: {}",
groupId, topicIdPartition, memberId, exception);
}
// Even if write share group state RPC call fails, we will still go ahead with the state transition.
// Update the cached state and start and end offsets after releasing the acquisition lock on timeout.
maybeUpdateCachedStateAndOffsets();
});
}
} finally {
lock.writeLock().unlock();
}

if (!stateBatches.isEmpty()) {
writeShareGroupState(stateBatches).whenComplete((result, exception) -> {
if (exception != null) {
log.debug("Failed to write the share group state on acquisition lock timeout for share partition: {}-{} memberId: {}",
groupId, topicIdPartition, memberId, exception);
}
// Even if write share group state RPC call fails, we will still go ahead with the state transition.
// Update the cached state and start and end offsets after releasing the acquisition lock on timeout.
maybeUpdateCachedStateAndOffsets();
});
}

// If we have an acquisition lock timeout for a share-partition, then we should check if
// there is a pending share fetch request for the share-partition and complete it.
// Skip null check for stateBatches, it should always be initialized if reached here.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,7 @@ private static void removeSharePartitionFromCache(
if (sharePartition != null) {
sharePartition.markFenced();
replicaManager.removeListener(sharePartitionKey.topicIdPartition().topicPartition(), sharePartition.listener());
replicaManager.completeDelayedShareFetchRequest(new DelayedShareFetchGroupKey(sharePartitionKey.groupId(), sharePartitionKey.topicIdPartition()));
}
}

Expand Down
Loading