Skip to content

Commit 5e16b32

Browse files
authored
[da-vinci] [server] Release OFFLINE->STANDBY Latch on Version Demotion to Backup (linkedin#2586)
* release the OFFLINE->STANDBY ingestion latch when a current version is demoted to backup so waitConsumptionCompleted does not block indefinitely and the state transition is left not completed * add regression and idempotency coverage across state-model, dispatcher, and ingestion-task tests
1 parent b68623b commit 5e16b32

3 files changed

Lines changed: 106 additions & 1 deletion

File tree

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1835,6 +1835,10 @@ protected void refreshIngestionContextIfChanged(Store store) throws InterruptedE
18351835
return;
18361836
}
18371837

1838+
if (versionRole == VersionRole.CURRENT && newVersionRole != VersionRole.CURRENT) {
1839+
stopTrackingCurrentVersionIngestion();
1840+
}
1841+
18381842
LOGGER.info(
18391843
"Trigger for version topic: {} due to Previous: version role: {}, workload type: {} "
18401844
+ "changed to New: version role: {}, workload type: {}",
@@ -1855,6 +1859,22 @@ protected void refreshIngestionContextIfChanged(Store store) throws InterruptedE
18551859
}
18561860
}
18571861

1862+
/**
1863+
* {@link AbstractPartitionStateModel#onBecomeStandbyFromOffline} only needs to synchronously wait for ingestion
1864+
* to be completed for current versions. If a current version becomes no longer the current version, it no longer
1865+
* needs to {@link AbstractPartitionStateModel#waitConsumptionCompleted}. In that case, we stop tracking ingestion
1866+
* for those partitions and explicitly release their latches.
1867+
*/
1868+
private void stopTrackingCurrentVersionIngestion() {
1869+
partitionConsumptionStateMap.values()
1870+
.stream()
1871+
.filter(pcs -> pcs.isLatchCreated() && !pcs.isLatchReleased())
1872+
.forEach(pcs -> {
1873+
ingestionNotificationDispatcher.reportStopped(pcs);
1874+
pcs.releaseLatch();
1875+
});
1876+
}
1877+
18581878
private void maybeUnsubscribeCompletedPartitions(Store store) {
18591879
if (hybridStoreConfig.isPresent() || (!serverConfig.isUnsubscribeAfterBatchpushEnabled())) {
18601880
return;

clients/da-vinci-client/src/test/java/com/linkedin/davinci/helix/LeaderFollowerPartitionStateModelTest.java

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
import static org.mockito.Mockito.when;
1818
import static org.testng.Assert.assertEquals;
1919
import static org.testng.Assert.assertFalse;
20+
import static org.testng.Assert.assertNotNull;
21+
import static org.testng.Assert.assertNull;
2022
import static org.testng.Assert.assertTrue;
2123

2224
import com.linkedin.davinci.config.VeniceServerConfig;
@@ -40,12 +42,14 @@
4042
import com.linkedin.venice.meta.VersionStatus;
4143
import com.linkedin.venice.store.rocksdb.RocksDBUtils;
4244
import com.linkedin.venice.utils.PropertyBuilder;
45+
import com.linkedin.venice.utils.TestUtils;
4346
import com.linkedin.venice.utils.Utils;
4447
import com.linkedin.venice.utils.VeniceProperties;
4548
import java.io.File;
4649
import java.nio.ByteBuffer;
4750
import java.util.Random;
4851
import java.util.concurrent.CompletableFuture;
52+
import java.util.concurrent.TimeUnit;
4953
import org.apache.commons.io.FileUtils;
5054
import org.apache.helix.NotificationContext;
5155
import org.apache.helix.model.Message;
@@ -84,7 +88,11 @@ public void setUp() {
8488
partitionPushStatusAccessorFuture = CompletableFuture.completedFuture(mock(HelixPartitionStatusAccessor.class));
8589
stateTransitionStats = mock(ParticipantStateTransitionStats.class);
8690
heartbeatMonitoringService = mock(HeartbeatMonitoringService.class);
87-
leaderFollowerPartitionStateModel = new LeaderFollowerPartitionStateModel(
91+
leaderFollowerPartitionStateModel = buildModel(notifier);
92+
}
93+
94+
private LeaderFollowerPartitionStateModel buildModel(LeaderFollowerIngestionProgressNotifier notifier) {
95+
return new LeaderFollowerPartitionStateModel(
8896
ingestionBackend,
8997
storeAndServerConfigs,
9098
partition,
@@ -359,6 +367,48 @@ public void testNonCurrentVersionSkipsGracefulDrop() {
359367
assertEquals(resetTimestamp, -1L, "Timestamp should be reset to -1 after transition");
360368
}
361369

370+
/**
371+
* OFFLINE->STANDBY needs to complete when the current version is demoted to a backup version.
372+
*/
373+
@Test
374+
public void testOnStandbyCompletionUponDemotionToBackupVersion() throws Exception {
375+
Store store = mock(Store.class);
376+
when(store.getCurrentVersion()).thenReturn(storeVersion); // Version starts as current so the latch is created
377+
when(store.getBootstrapToOnlineTimeoutInHours()).thenReturn(24); // large timeout for latch await
378+
doReturn(store).when(metadataRepo).getStoreOrThrow(anyString());
379+
LeaderFollowerIngestionProgressNotifier notifier = new LeaderFollowerIngestionProgressNotifier();
380+
LeaderFollowerPartitionStateModel model = buildModel(notifier);
381+
382+
// Start the OFFLINE -> STANDBY transition in a background thread which should be blocked in
383+
// waitConsumptionCompleted, because this is the current version,
384+
Message message = mock(Message.class);
385+
when(message.getResourceName()).thenReturn(resourceName);
386+
NotificationContext context = mock(NotificationContext.class);
387+
CompletableFuture<Void> transitionFuture =
388+
CompletableFuture.runAsync(() -> model.onBecomeStandbyFromOffline(message, context));
389+
390+
// Wait until the latch is created, confirming the transition is blocking.
391+
TestUtils.waitForNonDeterministicAssertion(
392+
5,
393+
TimeUnit.SECONDS,
394+
() -> assertNotNull(
395+
notifier.getIngestionCompleteFlag(resourceName, partition),
396+
"Latch must be created for a current-version OFFLINE->STANDBY transition"));
397+
assertFalse(transitionFuture.isDone(), "Transition must be blocked waiting for the latch");
398+
399+
// Simulate CURRENT -> BACKUP version role flip: a newer version has been promoted.
400+
when(store.getCurrentVersion()).thenReturn(storeVersion + 1);
401+
402+
// Simulate StoreIngestionTask.stopTrackingCurrentVersionIngestion() being called
403+
notifier.stopped(resourceName, partition, null);
404+
TestUtils.waitForNonDeterministicCompletion(5, TimeUnit.SECONDS, transitionFuture::isDone);
405+
// Ensure the transition completed successfully (will throw if it completed exceptionally)
406+
transitionFuture.join();
407+
assertNull(
408+
notifier.getIngestionCompleteFlag(resourceName, partition),
409+
"Latch should be released/removed after current version is demoted");
410+
}
411+
362412
/**
363413
* Integration test that verifies RocksDB deletion rate limiting is honored during
364414
* OFFLINE->DROPPED state transition (backup version deletion).

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
import com.linkedin.venice.serialization.avro.AvroProtocolDefinition;
9090
import com.linkedin.venice.serialization.avro.ChunkedValueManifestSerializer;
9191
import com.linkedin.venice.serialization.avro.InternalAvroSpecificSerializer;
92+
import com.linkedin.venice.server.VersionRole;
9293
import com.linkedin.venice.stats.dimensions.VeniceRecordType;
9394
import com.linkedin.venice.storage.protocol.ChunkedValueManifest;
9495
import com.linkedin.venice.utils.ByteUtils;
@@ -1411,6 +1412,40 @@ public void testIngestionTimeoutHandling() throws Exception {
14111412
verify(mockHostLevelStats, times(1)).recordIngestionFailure();
14121413
}
14131414

1415+
@Test
1416+
public void testStopTrackingCurrentVersionIngestionOnDemotion() throws Exception {
1417+
LeaderFollowerStoreIngestionTask storeIngestionTask = mock(LeaderFollowerStoreIngestionTask.class);
1418+
VeniceServerConfig serverConfig = mock(VeniceServerConfig.class);
1419+
doReturn(true).when(serverConfig).isResubscriptionTriggeredByVersionIngestionContextChangeEnabled();
1420+
doReturn(0).when(serverConfig).getResubscriptionCheckIntervalInSeconds();
1421+
setField(storeIngestionTask, "serverConfig", serverConfig);
1422+
setField(storeIngestionTask, "versionRole", VersionRole.CURRENT);
1423+
setVersion(storeIngestionTask, 1);
1424+
doReturn(true).when(storeIngestionTask).isHybridMode();
1425+
doCallRealMethod().when(storeIngestionTask).refreshIngestionContextIfChanged(any(Store.class));
1426+
1427+
Store store = mock(Store.class);
1428+
doReturn(5).when(store).getCurrentVersion();
1429+
1430+
PartitionConsumptionState pcs = mock(PartitionConsumptionState.class);
1431+
doReturn(true).when(pcs).isLatchCreated();
1432+
doReturn(false).when(pcs).isLatchReleased();
1433+
VeniceConcurrentHashMap<Integer, PartitionConsumptionState> pcsMap = new VeniceConcurrentHashMap<>();
1434+
pcsMap.put(1, pcs);
1435+
setField(storeIngestionTask, "partitionConsumptionStateMap", pcsMap);
1436+
1437+
IngestionNotificationDispatcher mockDispatcher = mock(IngestionNotificationDispatcher.class);
1438+
setField(storeIngestionTask, "ingestionNotificationDispatcher", mockDispatcher);
1439+
1440+
// First call: versionRole transitions CURRENT -> BACKUP, reportStopped should be called once
1441+
storeIngestionTask.refreshIngestionContextIfChanged(store);
1442+
verify(mockDispatcher, times(1)).reportStopped(eq(pcs));
1443+
1444+
// Second call: versionRole is already BACKUP, no further latch release
1445+
storeIngestionTask.refreshIngestionContextIfChanged(store);
1446+
verify(mockDispatcher, times(1)).reportStopped(eq(pcs));
1447+
}
1448+
14141449
private static void addStandbyPcs(Map<Integer, PartitionConsumptionState> pcsMap, int partition, long ageMs) {
14151450
PartitionConsumptionState pcs = mock(PartitionConsumptionState.class);
14161451
pcsMap.put(partition, pcs);

0 commit comments

Comments
 (0)