Skip to content

Commit 16a81ce

Browse files
authored
[da-vinci][changelog] Fix NPE when starting clients with no view configured (linkedin#2473)
- Add empty string validation for view names in client factories - Normalize empty view names to null in changelog consumers - Add integration tests for batch stores without views
1 parent 6c22d7c commit 16a81ce

4 files changed

Lines changed: 120 additions & 2 deletions

File tree

clients/da-vinci-client/src/main/java/com/linkedin/davinci/client/factory/CachingDaVinciClientFactory.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,8 @@ protected synchronized DaVinciClient getClient(
470470
DaVinciClientConstructor clientConstructor,
471471
Class clientClass,
472472
boolean startClient) {
473-
String internalStoreName = viewName == null ? storeName : VeniceView.getViewStoreName(storeName, viewName);
473+
String internalStoreName =
474+
(viewName == null || viewName.isEmpty()) ? storeName : VeniceView.getViewStoreName(storeName, viewName);
474475
if (closed) {
475476
throw new VeniceException("Unable to get a client from a closed factory, storeName=" + internalStoreName);
476477
}

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,11 @@ public SchemaReader getSchemaReader() {
148148
}
149149

150150
public ChangelogClientConfig<T> setViewName(String viewName) {
151-
this.viewName = viewName;
151+
if (viewName != null && !viewName.isEmpty()) {
152+
this.viewName = viewName;
153+
} else {
154+
this.viewName = null;
155+
}
152156
return this;
153157
}
154158

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,22 @@ public void testGetChangelogConsumerWithConsumerId()
168168
Assert.assertNotSame(consumer1, consumer2);
169169
}
170170

171+
@Test
172+
public void testChangelogConsumerWithViewName() {
173+
ChangelogClientConfig globalChangelogClientConfig = new ChangelogClientConfig();
174+
175+
// Default of the field is null
176+
Assert.assertNull(globalChangelogClientConfig.getViewName());
177+
178+
// Setting view name should work as expected
179+
globalChangelogClientConfig.setViewName(VIEW_NAME);
180+
Assert.assertEquals(globalChangelogClientConfig.getViewName(), VIEW_NAME);
181+
182+
// reset view name to null through empty string
183+
globalChangelogClientConfig.setViewName("");
184+
Assert.assertNull(globalChangelogClientConfig.getViewName());
185+
}
186+
171187
private void setUpMockStoreResponse(D2ControllerClient mockControllerClient, String storeConsumer) {
172188
StoreResponse mockStoreResponse = Mockito.mock(StoreResponse.class);
173189
Mockito.when(mockStoreResponse.isError()).thenReturn(false);

internal/venice-test-common/src/integrationTest/java/com/linkedin/venice/endToEnd/TestMaterializedViewEndToEnd.java

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,11 @@
8787
import java.util.ArrayList;
8888
import java.util.Collection;
8989
import java.util.Collections;
90+
import java.util.HashSet;
9091
import java.util.List;
9192
import java.util.Map;
9293
import java.util.Properties;
94+
import java.util.Set;
9395
import java.util.concurrent.ExecutionException;
9496
import java.util.concurrent.TimeUnit;
9597
import java.util.stream.IntStream;
@@ -638,6 +640,59 @@ public void testBatchOnlyMaterializedViewDVCConsumerWithOnePush() throws Excepti
638640
testDVCConsumer(storeName, 100);
639641
}
640642

643+
@Test(timeOut = TEST_TIMEOUT)
644+
public void testBatchOnlyNoViewWithStatelessCDCConsumer() throws Exception {
645+
testBatchOnlyNoViewWithCDCConsumer(false);
646+
}
647+
648+
@Test(timeOut = TEST_TIMEOUT)
649+
public void testBatchOnlyNoViewWithStatefulCDCConsumer() throws Exception {
650+
testBatchOnlyNoViewWithCDCConsumer(true);
651+
}
652+
653+
@Test(timeOut = TEST_TIMEOUT)
654+
public void testBatchOnlyNoViewWithDVCConsumer() throws Exception {
655+
// Create a batch only store with no view and run batch push job with 100 records
656+
File inputDir = getTempDataDirectory();
657+
Schema recordSchema = TestWriteUtils.writeSimpleAvroFileWithStringToStringSchema(inputDir);
658+
String inputDirPath = "file:" + inputDir.getAbsolutePath();
659+
String storeName = Utils.getUniqueString("batchStore");
660+
661+
setupStoreWithNoView(inputDirPath, storeName, recordSchema);
662+
663+
// Verify DVC consumer when there is no view.
664+
// Start a DVC client that's subscribed to partition 0, 1, 3 of the store. The DVC client should
665+
// contain all data records.
666+
D2Client d2Client = D2TestUtils
667+
.getAndStartD2Client(multiRegionMultiClusterWrapper.getChildRegions().get(1).getZkServerWrapper().getAddress());
668+
669+
VeniceProperties backendConfig =
670+
new PropertyBuilder().put(DATA_BASE_PATH, Utils.getTempDataDirectory().getAbsolutePath())
671+
.put(PERSISTENCE_TYPE, PersistenceType.ROCKS_DB)
672+
.put(CLIENT_USE_SYSTEM_STORE_REPOSITORY, true)
673+
.put(ROCKSDB_BLOCK_CACHE_SIZE_IN_BYTES, 2 * 1024 * 1024L)
674+
.put(CLIENT_SYSTEM_STORE_REPOSITORY_REFRESH_INTERVAL_SECONDS, 1)
675+
.build();
676+
try (CachingDaVinciClientFactory factory = getCachingDaVinciClientFactory(
677+
d2Client,
678+
VeniceRouterWrapper.CLUSTER_DISCOVERY_D2_SERVICE_NAME,
679+
new MetricsRepository(),
680+
backendConfig,
681+
multiRegionMultiClusterWrapper)) {
682+
DaVinciClient<String, Object> client = factory.getAndStartGenericAvroClient(storeName, new DaVinciConfig());
683+
Set<Integer> partitions = new HashSet<>();
684+
partitions.add(0);
685+
partitions.add(1);
686+
partitions.add(2);
687+
client.subscribe(partitions).get();
688+
for (int i = 1; i <= 100; i++) {
689+
assertEquals(client.get(Integer.toString(i)).get().toString(), DEFAULT_USER_DATA_VALUE_PREFIX + i);
690+
}
691+
} finally {
692+
D2ClientUtils.shutdownClient(d2Client);
693+
}
694+
}
695+
641696
private void testBatchOnlyMaterializedViewCDCConsumer(boolean useStatefulConsumer) throws Exception {
642697
// Create a batch only store with materialized view and run batch push job with 100 records
643698
File inputDir = getTempDataDirectory();
@@ -663,6 +718,29 @@ private void testBatchOnlyMaterializedViewCDCConsumer(boolean useStatefulConsume
663718
}
664719
}
665720

721+
public void testBatchOnlyNoViewWithCDCConsumer(boolean isStatefulClient) throws Exception {
722+
// Create a batch only store with no view and run batch push job with 100 records
723+
File inputDir = getTempDataDirectory();
724+
Schema recordSchema = TestWriteUtils.writeSimpleAvroFileWithStringToStringSchema(inputDir);
725+
String inputDirPath = "file:" + inputDir.getAbsolutePath();
726+
String storeName = Utils.getUniqueString("batchStore");
727+
728+
setupStoreWithNoView(inputDirPath, storeName, recordSchema);
729+
730+
// Setup D2Client and ChangelogClientConfig
731+
D2Client d2Client = D2TestUtils
732+
.getAndStartD2Client(multiRegionMultiClusterWrapper.getChildRegions().get(0).getZkServerWrapper().getAddress());
733+
734+
try {
735+
ChangelogClientConfig changelogClientConfig = createChangelogClientConfig(d2Client, inputDirPath, "");
736+
737+
// Test CDC consumer
738+
testCDCConsumer(storeName, changelogClientConfig, isStatefulClient);
739+
} finally {
740+
D2ClientUtils.shutdownClient(d2Client);
741+
}
742+
}
743+
666744
private void testDVCConsumer(String storeName, int recordCount) throws Exception {
667745
// Start a DVC client that's subscribed to partition 0 of the store's materialized view. The DVC client should
668746
// contain all data records.
@@ -693,6 +771,25 @@ private void testDVCConsumer(String storeName, int recordCount) throws Exception
693771
}
694772
}
695773

774+
private void setupStoreWithNoView(String inputDirPath, String storeName, Schema recordSchema) {
775+
Properties props = TestWriteUtils.defaultVPJProps(
776+
parentControllers.get(0).getControllerUrl(),
777+
inputDirPath,
778+
storeName,
779+
multiRegionMultiClusterWrapper.getPubSubClientProperties());
780+
String keySchemaStr = recordSchema.getField(DEFAULT_KEY_FIELD_PROP).schema().toString();
781+
String valueSchemaStr = recordSchema.getField(DEFAULT_VALUE_FIELD_PROP).schema().toString();
782+
UpdateStoreQueryParams storeParms = new UpdateStoreQueryParams().setActiveActiveReplicationEnabled(false)
783+
.setChunkingEnabled(true)
784+
.setRmdChunkingEnabled(true)
785+
.setNativeReplicationEnabled(true)
786+
.setNativeReplicationSourceFabric(childDatacenters.get(0).getRegionName())
787+
.setPartitionCount(3);
788+
789+
IntegrationTestPushUtils.createStoreForJob(clusterName, keySchemaStr, valueSchemaStr, props, storeParms).close();
790+
IntegrationTestPushUtils.runVPJ(props);
791+
}
792+
696793
private void setupStoreAndMaterializedView(
697794
String inputDirPath,
698795
String storeName,

0 commit comments

Comments
 (0)