Skip to content

Commit 5e7c7c9

Browse files
jeqoclaude
andauthored
fix(inkless:consolidation): prevent ConsolidationFetcherThread crash on topic deletion (#627)
When a diskless topic is deleted, the ConsolidationFetcherThread crashes permanently in buildFetch(): - localLogOrException() throws UnknownTopicOrPartitionException - only KafkaStorageException is caught ? exception escapes doWork() - ShutdownableThread exits with no recovery This kills consolidation for ALL topics on the broker until restart. The race: BrokerMetadataPublisher sets metadataCache.setImage(newImage) (removing the topic from the cache) before applyDelta calls stopPartitions (which removes the partition from the fetcher). In that window, the fetcher's buildFetch reads a partition still in partitionStates but gone from allPartitions/metadataCache. The regular ReplicaFetcher never hits this because stopPartitions removes from the fetcher BEFORE allPartitions.remove Ñ so the fetcher never sees a partition that's been deleted from allPartitions. The consolidation fetcher hits the window because metadata-view staleness creates a gap. Fix: catch UnknownTopicOrPartitionException alongside KafkaStorageException in DisklessLeaderEndPoint.buildFetch. The partition is added to partitionsWithError, delayed, and evicted on the next cycle once removePartitions completes. --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f82bb06 commit 5e7c7c9

3 files changed

Lines changed: 30 additions & 2 deletions

File tree

.github/workflows/inkless.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,11 +169,13 @@ jobs:
169169
--tests "org.apache.kafka.image.TopicsImageTest" && \
170170
./gradlew ${GRADLE_ARGS} :core:test \
171171
--tests "*Inkless*" --tests "*Diskless*" \
172+
--tests "io.aiven.inkless.*" \
172173
--tests "kafka.log.LogConfigTest" \
173174
--tests "kafka.server.KafkaConfigTest" \
174175
--tests "kafka.server.ReplicaManagerTest" \
175176
--tests "kafka.server.KafkaApisTest" \
176177
--tests "kafka.cluster.PartitionTest" \
178+
--tests "kafka.server.AbstractFetcherThreadTest" \
177179
--tests "kafka.server.DynamicBrokerConfigTest" \
178180
--tests "kafka.server.DynamicConfigChangeTest" \
179181
--tests "kafka.server.ReplicaFetcherThreadTest" \

core/src/main/scala/io/aiven/inkless/consolidation/DisklessLeaderEndPoint.scala

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import io.aiven.inkless.consume.{FetchHandler, FetchOffsetHandler}
2222
import kafka.server.{KafkaConfig, ReplicaManager, ReplicaQuota}
2323
import kafka.utils.Logging
2424
import org.apache.kafka.common.Uuid
25-
import org.apache.kafka.common.errors.KafkaStorageException
25+
import org.apache.kafka.common.errors.{KafkaStorageException, UnknownTopicOrPartitionException}
2626
import org.apache.kafka.common.message.{FetchResponseData, OffsetForLeaderEpochRequestData}
2727
import org.apache.kafka.common.message.ListOffsetsRequestData.ListOffsetsPartition
2828
import org.apache.kafka.common.message.OffsetForLeaderEpochResponseData.EpochEndOffset
@@ -251,7 +251,10 @@ class DisklessLeaderEndPoint(
251251
)
252252
)
253253
} catch {
254-
case _: KafkaStorageException =>
254+
// UnknownTopicOrPartitionException from localLogOrException when partition is
255+
// deleted from allPartitions before the fetcher's partitionStates is cleaned up.
256+
case e @ (_: KafkaStorageException | _: UnknownTopicOrPartitionException) =>
257+
logger.info("Partition {} unavailable during buildFetch: {}", topicPartition, e.getMessage)
255258
partitionsWithError += topicPartition
256259
}
257260
}

core/src/test/scala/io/aiven/inkless/consolidation/DisklessLeaderEndPointTest.scala

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,29 @@ class DisklessLeaderEndPointTest {
462462
assertEquals(Set(topicPartition), result.partitionsWithError.asScala.toSet)
463463
}
464464

465+
@Test
466+
def testBuildFetchMarksPartitionWithUnknownTopicOrPartitionException(): Unit = {
467+
val fetchHandler = mock(classOf[FetchHandler])
468+
val fetchOffsetHandler = mock(classOf[FetchOffsetHandler])
469+
val replicaManager = mock(classOf[ReplicaManager])
470+
when(replicaManager.localLogOrException(topicPartition)).thenThrow(new UnknownTopicOrPartitionException("deleted"))
471+
472+
val endPoint = newEndPoint(fetchHandler, fetchOffsetHandler, replicaManager)
473+
val fetchState = new PartitionFetchState(
474+
Optional.of(topicId),
475+
0L,
476+
Optional.empty(),
477+
1,
478+
Optional.empty(),
479+
ReplicaState.FETCHING,
480+
Optional.empty()
481+
)
482+
val result = endPoint.buildFetch(util.Map.of(topicPartition, fetchState))
483+
484+
assertTrue(result.result.isEmpty)
485+
assertEquals(Set(topicPartition), result.partitionsWithError.asScala.toSet)
486+
}
487+
465488
@Test
466489
def testBuildFetchSkipsPartitionWhenFollowerShouldThrottle(): Unit = {
467490
val fetchHandler = mock(classOf[FetchHandler])

0 commit comments

Comments
 (0)