Skip to content

Commit 36285fb

Browse files
authored
[da-vinci][server] Guard version-role-change resubscribe against in-flight blob transfer (linkedin#2756)
1 parent 3fc3d9e commit 36285fb

2 files changed

Lines changed: 130 additions & 0 deletions

File tree

clients/da-vinci-client/src/main/java/com/linkedin/davinci/kafka/consumer/StoreIngestionTask.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,19 @@ public synchronized void subscribePartition(
793793
void resubscribeForAllPartitions() throws InterruptedException {
794794
throwIfNotRunning();
795795
for (PartitionConsumptionState partitionConsumptionState: partitionConsumptionStateMap.values()) {
796+
/**
797+
* Skip partitions with an in-flight blob transfer. Resubscribing reopens RocksDB on the final
798+
* partition directory while blob transfer is still receiving files into the temp directory,
799+
* causing the post-transfer rename to fail with "Final partition directory is not empty".
800+
* completeBlobTransferAndSubscribe will subscribe once the transfer finishes, using the
801+
* already-updated versionRole/workloadType.
802+
*/
803+
if (partitionConsumptionState.isBlobTransferInProgress()) {
804+
LOGGER.info(
805+
"Skipping version-role-change resubscribe for replica: {} because blob transfer is in progress.",
806+
partitionConsumptionState.getReplicaId());
807+
continue;
808+
}
796809
/**
797810
* For completed current version replica, if we resubscribe during version role change, it will be resubscribed to
798811
* correct high priority pool. We will mark {@link PartitionConsumptionState#hasResubscribedAfterBootstrapAsCurrentVersion}
@@ -5907,6 +5920,20 @@ && getResubscribeRequestQueue().peek() != null) {
59075920
getReplicaId(versionTopic, partition));
59085921
continue;
59095922
}
5923+
5924+
/**
5925+
* Skip partitions with an in-flight blob transfer. Resubscribing reopens RocksDB on the final
5926+
* partition directory while blob transfer is still receiving files into the temp directory,
5927+
* causing the post-transfer rename to fail. completeBlobTransferAndSubscribe will subscribe
5928+
* once the transfer finishes.
5929+
*/
5930+
if (pcs.isBlobTransferInProgress()) {
5931+
LOGGER.info(
5932+
"Skipping lag-based resubscribe for replica: {} because blob transfer is in progress.",
5933+
pcs.getReplicaId());
5934+
continue;
5935+
}
5936+
59105937
/**
59115938
* As of now, this feature intends to resolve ingestion performance issue introduced by consumer. We will rely on
59125939
* the error reset feature to handle the error replica properly.

clients/da-vinci-client/src/test/java/com/linkedin/davinci/kafka/consumer/StoreIngestionTaskTest.java

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@
270270
import java.util.concurrent.ExecutorService;
271271
import java.util.concurrent.Executors;
272272
import java.util.concurrent.Future;
273+
import java.util.concurrent.PriorityBlockingQueue;
273274
import java.util.concurrent.ThreadPoolExecutor;
274275
import java.util.concurrent.TimeUnit;
275276
import java.util.concurrent.TimeoutException;
@@ -4720,6 +4721,108 @@ public void testResubscribeForCompletedCurrentVersionPartition() throws Interrup
47204721

47214722
}
47224723

4724+
/**
4725+
* Verifies that {@link StoreIngestionTask#resubscribeForAllPartitions()} skips partitions whose
4726+
* blob transfer is still in flight. Without this guard, a version-role change (e.g. FUTURE->CURRENT)
4727+
* would unsubscribe + resubscribe the partition, causing RocksDB to reopen on the final partition
4728+
* directory while the blob transfer was still streaming into the temp directory -- which breaks
4729+
* the post-transfer rename and drives the replica to Helix ERROR.
4730+
*/
4731+
@Test
4732+
public void testResubscribeForAllPartitionsSkipsBlobTransferInProgress() throws Exception {
4733+
StoreIngestionTask storeIngestionTask = mock(StoreIngestionTask.class);
4734+
doCallRealMethod().when(storeIngestionTask).resubscribeForAllPartitions();
4735+
4736+
// pcs0: blob transfer in progress -- MUST be skipped.
4737+
PartitionConsumptionState pcsBlobInProgress = mock(PartitionConsumptionState.class);
4738+
doReturn(true).when(pcsBlobInProgress).isBlobTransferInProgress();
4739+
doReturn("store_v1-0").when(pcsBlobInProgress).getReplicaId();
4740+
4741+
// pcs1: no blob transfer, not complete -- should be resubscribed.
4742+
PartitionConsumptionState pcsNotComplete = mock(PartitionConsumptionState.class);
4743+
doReturn(false).when(pcsNotComplete).isBlobTransferInProgress();
4744+
doReturn(false).when(pcsNotComplete).isComplete();
4745+
4746+
// pcs2: no blob transfer, complete, not yet flipped -- should be resubscribed AND flipped.
4747+
PartitionConsumptionState pcsCompleteNotFlipped = mock(PartitionConsumptionState.class);
4748+
doReturn(false).when(pcsCompleteNotFlipped).isBlobTransferInProgress();
4749+
doReturn(true).when(pcsCompleteNotFlipped).isComplete();
4750+
doReturn(false).when(pcsCompleteNotFlipped).hasResubscribedAfterBootstrapAsCurrentVersion();
4751+
4752+
VeniceConcurrentHashMap<Integer, PartitionConsumptionState> pcsMap = new VeniceConcurrentHashMap<>();
4753+
pcsMap.put(0, pcsBlobInProgress);
4754+
pcsMap.put(1, pcsNotComplete);
4755+
pcsMap.put(2, pcsCompleteNotFlipped);
4756+
4757+
// Inject into the protected final partitionConsumptionStateMap field on the mock.
4758+
Field pcsMapField = StoreIngestionTask.class.getDeclaredField("partitionConsumptionStateMap");
4759+
pcsMapField.setAccessible(true);
4760+
pcsMapField.set(storeIngestionTask, pcsMap);
4761+
4762+
doReturn(true).when(storeIngestionTask).isCurrentVersion();
4763+
4764+
storeIngestionTask.resubscribeForAllPartitions();
4765+
4766+
// Blob-transfer-in-progress partition is skipped entirely.
4767+
verify(storeIngestionTask, never()).resubscribe(pcsBlobInProgress);
4768+
verify(pcsBlobInProgress, never()).setHasResubscribedAfterBootstrapAsCurrentVersion(anyBoolean());
4769+
4770+
// Other partitions are resubscribed as usual.
4771+
verify(storeIngestionTask, times(1)).resubscribe(pcsNotComplete);
4772+
verify(storeIngestionTask, times(1)).resubscribe(pcsCompleteNotFlipped);
4773+
verify(pcsCompleteNotFlipped, times(1)).setHasResubscribedAfterBootstrapAsCurrentVersion(eq(true));
4774+
}
4775+
4776+
/**
4777+
* Verifies that {@link StoreIngestionTask#maybeProcessResubscribeRequest()} (the lag-based
4778+
* auto-resubscribe path fed by {@code HeartbeatMonitoringService}) skips partitions whose blob
4779+
* transfer is still in flight. Same race as the version-role-change path: resubscribing would
4780+
* reopen RocksDB on the final partition directory mid-transfer and break the post-transfer rename.
4781+
*/
4782+
@Test
4783+
public void testMaybeProcessResubscribeRequestSkipsBlobTransferInProgress() throws Exception {
4784+
StoreIngestionTask storeIngestionTask = mock(StoreIngestionTask.class);
4785+
doCallRealMethod().when(storeIngestionTask).maybeProcessResubscribeRequest();
4786+
4787+
VeniceServerConfig serverConfig = mock(VeniceServerConfig.class);
4788+
doReturn(serverConfig).when(storeIngestionTask).getServerConfig();
4789+
doReturn(10).when(serverConfig).getLagBasedReplicaAutoResubscribeMaxReplicaCount();
4790+
doReturn(60).when(serverConfig).getLagBasedReplicaAutoResubscribeIntervalInSeconds();
4791+
4792+
// pcs0: blob transfer in progress -- MUST be skipped.
4793+
PartitionConsumptionState pcsBlobInProgress = mock(PartitionConsumptionState.class);
4794+
doReturn(true).when(pcsBlobInProgress).isBlobTransferInProgress();
4795+
doReturn("store_v1-0").when(pcsBlobInProgress).getReplicaId();
4796+
4797+
// pcs1: no blob transfer -- should be resubscribed.
4798+
PartitionConsumptionState pcsReady = mock(PartitionConsumptionState.class);
4799+
doReturn(false).when(pcsReady).isBlobTransferInProgress();
4800+
doReturn(false).when(pcsReady).isErrorReported();
4801+
doReturn("store_v1-1").when(pcsReady).getReplicaId();
4802+
4803+
VeniceConcurrentHashMap<Integer, PartitionConsumptionState> pcsMap = new VeniceConcurrentHashMap<>();
4804+
pcsMap.put(0, pcsBlobInProgress);
4805+
pcsMap.put(1, pcsReady);
4806+
doReturn(pcsMap).when(storeIngestionTask).getPartitionConsumptionStateMap();
4807+
4808+
PriorityBlockingQueue<Integer> resubscribeQueue = new PriorityBlockingQueue<>();
4809+
resubscribeQueue.add(0);
4810+
resubscribeQueue.add(1);
4811+
doReturn(resubscribeQueue).when(storeIngestionTask).getResubscribeRequestQueue();
4812+
4813+
// Empty previous-resubscribe-time map means no rate-limiting will trigger.
4814+
doReturn(new VeniceConcurrentHashMap<Integer, Long>()).when(storeIngestionTask)
4815+
.getPartitionToPreviousResubscribeTimeMap();
4816+
4817+
storeIngestionTask.maybeProcessResubscribeRequest();
4818+
4819+
// Blob-transfer-in-progress partition is skipped entirely.
4820+
verify(storeIngestionTask, never()).resubscribe(pcsBlobInProgress);
4821+
4822+
// Ready partition is resubscribed as usual.
4823+
verify(storeIngestionTask, times(1)).resubscribe(pcsReady);
4824+
}
4825+
47234826
@Test(dataProvider = "aaConfigProvider")
47244827
public void testWrappedInterruptExceptionDuringGracefulShutdown(AAConfig aaConfig) throws Exception {
47254828
hybridStoreConfig = Optional.of(

0 commit comments

Comments
 (0)