Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
4894131
refactor logic of buildRemoteAuxLog into TierStateMachine interface. …
mattwong949 Jan 26, 2023
63e2839
build fixes, checkstyle fixes, testing AbstracterFetcher related test…
mattwong949 Jan 31, 2023
7851d09
fixed AbstractFetcherThreadTest
mattwong949 Feb 3, 2023
42f0937
Merge branch 'trunk' of github.com:apache/kafka into tsm-interface-re…
mattwong949 Feb 8, 2023
c29d421
address comments
mattwong949 Feb 8, 2023
95755c8
fix imports and nits in tests
mattwong949 Feb 8, 2023
1742b8f
address more comments
mattwong949 Feb 8, 2023
f937391
Merge branch 'trunk' of github.com:apache/kafka into tsm-interface-re…
mattwong949 Feb 17, 2023
9610005
fixing some nits, trying to resolve comments
mattwong949 Feb 17, 2023
1f8d636
Merge branch 'trunk' of github.com:apache/kafka into tsm-interface-re…
mattwong949 Feb 21, 2023
3adb0b0
fixing PartitionData usage from the FetchResponse
mattwong949 Feb 22, 2023
d2a3e76
Merge branch 'trunk' of github.com:apache/kafka into tsm-interface-re…
mattwong949 Feb 22, 2023
2e38027
fix build on scala 2.12
mattwong949 Feb 22, 2023
9aa8af0
remove unused import
mattwong949 Feb 23, 2023
6525189
nits, will do refactor of test per comments
mattwong949 Feb 23, 2023
4b81be1
refactor test per Jun's comment
mattwong949 Feb 23, 2023
f6f4416
refactor MockTierStateMachine and MockFetcherThread
mattwong949 Feb 24, 2023
32d6e01
Merge branch 'trunk' of github.com:apache/kafka into tsm-interface-re…
mattwong949 Feb 24, 2023
5d118a0
fix imports and offsetAndEpoch changes from merge
mattwong949 Feb 24, 2023
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
6 changes: 2 additions & 4 deletions checkstyle/import-control-core.xml
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,8 @@
</subpackage>

<subpackage name="server">
<subpackage name="builders">
<allow pkg="kafka" />
<allow pkg="org.apache.kafka" />
</subpackage>
<allow pkg="kafka" />
<allow pkg="org.apache.kafka" />
</subpackage>

<subpackage name="test">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* 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 kafka.server;

import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.requests.FetchRequest;

import java.util.Optional;

/**
The replica alter log dirs tier state machine is unsupported but is provided to the ReplicaAlterLogDirsThread.
*/
public class ReplicaAlterLogDirsTierStateMachine implements TierStateMachine {
Comment thread
mattwong949 marked this conversation as resolved.

public PartitionFetchState start(TopicPartition topicPartition,
PartitionFetchState currentFetchState,
FetchRequest.PartitionData fetchPartitionData) throws Exception {
// JBOD is not supported with tiered storage.
throw new UnsupportedOperationException("Building remote log aux state is not supported in ReplicaAlterLogDirsThread.");
}

public Optional<PartitionFetchState> maybeAdvanceState(TopicPartition topicPartition,
PartitionFetchState currentFetchState) {
return Optional.empty();
}
}
266 changes: 266 additions & 0 deletions core/src/main/java/kafka/server/ReplicaFetcherTierStateMachine.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
/*
* 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 kafka.server;

import kafka.cluster.Partition;
import kafka.log.LeaderOffsetIncremented$;
import kafka.log.UnifiedLog;
import kafka.log.remote.RemoteLogManager;
import org.apache.kafka.common.KafkaException;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.message.OffsetForLeaderEpochResponseData.EpochEndOffset;
import org.apache.kafka.common.message.OffsetForLeaderEpochRequestData.OffsetForLeaderPartition;
import org.apache.kafka.common.protocol.Errors;
import org.apache.kafka.storage.internals.checkpoint.LeaderEpochCheckpointFile;
import org.apache.kafka.storage.internals.log.EpochEntry;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import org.apache.kafka.common.requests.FetchRequest.PartitionData;
import org.apache.kafka.common.utils.Utils;
import org.apache.kafka.server.common.CheckpointFile;
import org.apache.kafka.server.log.remote.storage.RemoteLogSegmentMetadata;
import org.apache.kafka.server.log.remote.storage.RemoteStorageException;
import org.apache.kafka.server.log.remote.storage.RemoteStorageManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import scala.Option;
import scala.Tuple2;
import scala.jdk.CollectionConverters;

/**
The replica fetcher tier state machine follows a state machine progression.

Currently, the tier state machine follows a synchronous execution, and we only need to start the machine.
There is no need to advance the state.

When started, the tier state machine will fetch the local log start offset of the
leader and then build the follower's remote log aux state until the leader's
local log start offset.
*/
public class ReplicaFetcherTierStateMachine implements TierStateMachine {
private final Logger log = LoggerFactory.getLogger(ReplicaFetcherTierStateMachine.class);

private LeaderEndPoint leader;
private ReplicaManager replicaMgr;

public ReplicaFetcherTierStateMachine(LeaderEndPoint leader,
ReplicaManager replicaMgr) {
this.leader = leader;
this.replicaMgr = replicaMgr;
}


/**
* Start the tier state machine for the provided topic partition. Currently, this start method will build the
* entire remote aux log state synchronously.
*
* @param topicPartition the topic partition
* @param currentFetchState the current PartitionFetchState which will
* be used to derive the return value
* @param fetchPartitionData the data from the fetch response that returned the offset moved to tiered storage error
*
* @return the new PartitionFetchState after the successful start of the
* tier state machine
*/
public PartitionFetchState start(TopicPartition topicPartition,
Comment thread
mattwong949 marked this conversation as resolved.
PartitionFetchState currentFetchState,
PartitionData fetchPartitionData) throws Exception {

Tuple2<Object, Object> epochAndLeaderLocalStartOffset = leader.fetchEarliestLocalOffset(topicPartition, currentFetchState.currentLeaderEpoch());
int epoch = (int) epochAndLeaderLocalStartOffset._1;
long leaderLocalStartOffset = (long) epochAndLeaderLocalStartOffset._2;

long offsetToFetch = buildRemoteLogAuxState(topicPartition, currentFetchState.currentLeaderEpoch(), leaderLocalStartOffset, epoch, fetchPartitionData.logStartOffset);

Tuple2<Object, Object> fetchLatestOffsetResult = leader.fetchLatestOffset(topicPartition, currentFetchState.currentLeaderEpoch());
long leaderEndOffset = (long) fetchLatestOffsetResult._2;

long initialLag = leaderEndOffset - offsetToFetch;

return PartitionFetchState.apply(currentFetchState.topicId(), offsetToFetch, Option.apply(initialLag), currentFetchState.currentLeaderEpoch(),
Fetching$.MODULE$, replicaMgr.localLogOrException(topicPartition).latestEpoch());
}

/**
* This is currently a no-op but will be used for implementing async tiering logic in KAFKA-13560.
*
* @param topicPartition the topic partition
* @param currentFetchState the current PartitionFetchState which will
* be used to derive the return value
*
* @return the original PartitionFetchState
*/
public Optional<PartitionFetchState> maybeAdvanceState(TopicPartition topicPartition,
Comment thread
mattwong949 marked this conversation as resolved.
PartitionFetchState currentFetchState) {
// No-op for now
return Optional.of(currentFetchState);
}

private EpochEndOffset fetchEarlierEpochEndOffset(Integer epoch,
TopicPartition partition,
Integer currentLeaderEpoch) {
int previousEpoch = epoch - 1;

// Find the end-offset for the epoch earlier to the given epoch from the leader
Map<TopicPartition, OffsetForLeaderPartition> partitionsWithEpochs = new HashMap<>();
partitionsWithEpochs.put(partition, new OffsetForLeaderPartition().setPartition(partition.partition()).setCurrentLeaderEpoch(currentLeaderEpoch).setLeaderEpoch(previousEpoch));

Option<EpochEndOffset> maybeEpochEndOffset = leader.fetchEpochEndOffsets(CollectionConverters.MapHasAsScala(partitionsWithEpochs).asScala()).get(partition);
if (maybeEpochEndOffset.isEmpty()) {
throw new KafkaException("No response received for partition: " + partition);
}

EpochEndOffset epochEndOffset = maybeEpochEndOffset.get();
if (epochEndOffset.errorCode() != Errors.NONE.code()) {
throw Errors.forCode(epochEndOffset.errorCode()).exception();
}

return epochEndOffset;
}

private List<EpochEntry> readLeaderEpochCheckpoint(RemoteLogManager rlm,
RemoteLogSegmentMetadata remoteLogSegmentMetadata) throws IOException, RemoteStorageException {
InputStream inputStream = rlm.storageManager().fetchIndex(remoteLogSegmentMetadata, RemoteStorageManager.IndexType.LEADER_EPOCH);
try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
CheckpointFile.CheckpointReadBuffer<EpochEntry> readBuffer = new CheckpointFile.CheckpointReadBuffer<>("", bufferedReader, 0, LeaderEpochCheckpointFile.FORMATTER);
return readBuffer.read();
}
}

private void buildProducerSnapshotFile(File snapshotFile,
RemoteLogSegmentMetadata remoteLogSegmentMetadata,
RemoteLogManager rlm) throws IOException, RemoteStorageException {
File tmpSnapshotFile = new File(snapshotFile.getAbsolutePath() + ".tmp");
// Copy it to snapshot file in atomic manner.
Files.copy(rlm.storageManager().fetchIndex(remoteLogSegmentMetadata, RemoteStorageManager.IndexType.PRODUCER_SNAPSHOT),
tmpSnapshotFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
Utils.atomicMoveWithFallback(tmpSnapshotFile.toPath(), snapshotFile.toPath(), false);
}

/**
* It tries to build the required state for this partition from leader and remote storage so that it can start
* fetching records from the leader. The return value is the next offset to fetch from the leader, which is the
* next offset following the end offset of the remote log portion.
*/
Comment thread
mattwong949 marked this conversation as resolved.
private Long buildRemoteLogAuxState(TopicPartition topicPartition,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Have you considered refactoring this method to ease testability? This could be part of another PR too to avoid changing too many parts in one code refactor.

Integer currentLeaderEpoch,
Long leaderLocalLogStartOffset,
Integer epochForLeaderLocalLogStartOffset,
Long leaderLogStartOffset) throws IOException, RemoteStorageException {

UnifiedLog unifiedLog = replicaMgr.localLogOrException(topicPartition);

long nextOffset;

if (unifiedLog.remoteStorageSystemEnable() && unifiedLog.config().remoteLogConfig.remoteStorageEnable) {
if (replicaMgr.remoteLogManager().isEmpty()) throw new IllegalStateException("RemoteLogManager is not yet instantiated");

RemoteLogManager rlm = replicaMgr.remoteLogManager().get();

// Find the respective leader epoch for (leaderLocalLogStartOffset - 1). We need to build the leader epoch cache
// until that offset
long previousOffsetToLeaderLocalLogStartOffset = leaderLocalLogStartOffset - 1;
int targetEpoch;
// If the existing epoch is 0, no need to fetch from earlier epoch as the desired offset(leaderLogStartOffset - 1)
// will have the same epoch.
if (epochForLeaderLocalLogStartOffset == 0) {
targetEpoch = epochForLeaderLocalLogStartOffset;
} else {
// Fetch the earlier epoch/end-offset(exclusive) from the leader.
EpochEndOffset earlierEpochEndOffset = fetchEarlierEpochEndOffset(epochForLeaderLocalLogStartOffset, topicPartition, currentLeaderEpoch);
// Check if the target offset lies within the range of earlier epoch. Here, epoch's end-offset is exclusive.
if (earlierEpochEndOffset.endOffset() > previousOffsetToLeaderLocalLogStartOffset) {
// Always use the leader epoch from returned earlierEpochEndOffset.
// This gives the respective leader epoch, that will handle any gaps in epochs.
// For ex, leader epoch cache contains:
// leader-epoch start-offset
// 0 20
// 1 85
// <2> - gap no messages were appended in this leader epoch.
// 3 90
// 4 98
// There is a gap in leader epoch. For leaderLocalLogStartOffset as 90, leader-epoch is 3.
// fetchEarlierEpochEndOffset(2) will return leader-epoch as 1, end-offset as 90.
// So, for offset 89, we should return leader epoch as 1 like below.
targetEpoch = earlierEpochEndOffset.leaderEpoch();
} else {
targetEpoch = epochForLeaderLocalLogStartOffset;
}
}

Optional<RemoteLogSegmentMetadata> maybeRlsm = rlm.fetchRemoteLogSegmentMetadata(topicPartition, targetEpoch, previousOffsetToLeaderLocalLogStartOffset);

if (maybeRlsm.isPresent()) {

@Hangleton Hangleton Feb 8, 2023

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Note: if the rlmMetadata is unavailable for an extended period of time, the replica fetcher will keep retrying indefinitely to construct the starting fetch state for the partition. This will lead to an OffsetForLeaderEpoch and ListOffsets requests every time. If a large number of partitions are impacted, that will generate unnecessary inter-broker traffic on the cluster - although marginal most of the time. As an optimization, we could store the leader epoch associated to the leader's local log start offset - 1 which was retrieved here (we would still, however, need to query for the local log start offset on the leader on every iteration, and fetch the associated leader epoch if it has changed).

The asynchronous resolution of the correct fetch state from the remote storages (KAFKA-13560) will prevent the extra load on the replica fetcher thread itself. The consideration above applies to the RPCs which are made on the synchronous fetch path.

RemoteLogSegmentMetadata remoteLogSegmentMetadata = maybeRlsm.get();
// Build leader epoch cache, producer snapshots until remoteLogSegmentMetadata.endOffset() and start
// segments from (remoteLogSegmentMetadata.endOffset() + 1)
// Assign nextOffset with the offset from which next fetch should happen.
nextOffset = remoteLogSegmentMetadata.endOffset() + 1;

// Truncate the existing local log before restoring the leader epoch cache and producer snapshots.
Partition partition = replicaMgr.getPartitionOrException(topicPartition);
partition.truncateFullyAndStartAt(nextOffset, false);

// Build leader epoch cache.
unifiedLog.maybeIncrementLogStartOffset(leaderLogStartOffset, LeaderOffsetIncremented$.MODULE$);
List<EpochEntry> epochs = readLeaderEpochCheckpoint(rlm, remoteLogSegmentMetadata);
if (unifiedLog.leaderEpochCache().isDefined()) {
unifiedLog.leaderEpochCache().get().assign(epochs);
}

log.debug("Updated the epoch cache from remote tier till offset: {} with size: {} for {}", leaderLocalLogStartOffset, epochs.size(), partition);

// Restore producer snapshot
File snapshotFile = UnifiedLog.producerSnapshotFile(unifiedLog.dir(), nextOffset);
buildProducerSnapshotFile(snapshotFile, remoteLogSegmentMetadata, rlm);

// Reload producer snapshots.
unifiedLog.producerStateManager().truncateFullyAndReloadSnapshots();
unifiedLog.loadProducerState(nextOffset);
log.debug("Built the leader epoch cache and producer snapshots from remote tier for {}, " +
"with active producers size: {}, leaderLogStartOffset: {}, and logEndOffset: {}",
partition, unifiedLog.producerStateManager().activeProducers().size(), leaderLogStartOffset, nextOffset);
} else {
throw new RemoteStorageException("Couldn't build the state from remote store for partition: " + topicPartition +
", currentLeaderEpoch: " + currentLeaderEpoch +
", leaderLocalLogStartOffset: " + leaderLocalLogStartOffset +
", leaderLogStartOffset: " + leaderLogStartOffset +
", epoch: " + targetEpoch +
"as the previous remote log segment metadata was not found");
}
} else {
// If the tiered storage is not enabled throw an exception back so that it will retry until the tiered storage
// is set as expected.
throw new RemoteStorageException("Couldn't build the state from remote store for partition " + topicPartition + ", as remote log storage is not yet enabled");
}

return nextOffset;
}
}
59 changes: 59 additions & 0 deletions core/src/main/java/kafka/server/TierStateMachine.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* 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 kafka.server;

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.

Should this be in the storage module under org.apache.kafka.server.log.remote.storage package? Eventually, we could move ReplicaFetcherTierStateMachine to the storage module too.

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.

yup makes sense I'll move the interface over

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.

actually I remember why I didn't initially put it in the storage package. it seems like it wouldn't be a simple change to move this to there because the TSM relies on the PartitionFetchState case class in AbstractFetcherThread.scala. it would've created a circular dependency across modules that I wanted to avoid. @junrao wdyt about keeping it in the core module in this PR


import java.util.Optional;

import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.requests.FetchRequest.PartitionData;

/**
* This interface defines the APIs needed to handle any state transitions
* related to tiering in AbstractFetcherThread.
Comment thread
mattwong949 marked this conversation as resolved.
Outdated
*/
public interface TierStateMachine {

/**
* Start the tier state machine for the provided topic partition.
*
* @param topicPartition the topic partition
* @param currentFetchState the current PartitionFetchState which will
* be used to derive the return value
* @param fetchPartitionData the data from the fetch response that returned the offset moved to tiered storage error
*
* @return the new PartitionFetchState after the successful start of the
* tier state machine
*/
PartitionFetchState start(TopicPartition topicPartition,
PartitionFetchState currentFetchState,
PartitionData fetchPartitionData) throws Exception;

/**
* Optionally advance the state of the tier state machine, based on the
* current PartitionFetchState. The decision to advance the tier
* state machine is implementation specific.
*
* @param topicPartition the topic partition
* @param currentFetchState the current PartitionFetchState which will
* be used to derive the return value
*
* @return the new PartitionFetchState if the tier state machine was advanced
Comment thread
mattwong949 marked this conversation as resolved.
Outdated
*/
Optional<PartitionFetchState> maybeAdvanceState(TopicPartition topicPartition,
PartitionFetchState currentFetchState);
}
Loading