Skip to content

Commit e1b88c6

Browse files
authored
check if reads are enabled before reading in CDC client (linkedin#2341)
When reads are disabled for a store, thin/fast clients cannot read it's data. However, CDC client does not respect this. In this change, CDC client will check the `readEnabled` flag of the store before returning the data.
1 parent 2a73b59 commit e1b88c6

3 files changed

Lines changed: 147 additions & 2 deletions

File tree

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import com.linkedin.venice.compression.VeniceCompressor;
3535
import com.linkedin.venice.controllerapi.D2ControllerClient;
3636
import com.linkedin.venice.exceptions.InvalidVeniceSchemaException;
37+
import com.linkedin.venice.exceptions.StoreDisabledException;
3738
import com.linkedin.venice.exceptions.StoreVersionNotFoundException;
3839
import com.linkedin.venice.exceptions.VeniceException;
3940
import com.linkedin.venice.kafka.protocol.ControlMessage;
@@ -343,6 +344,13 @@ public VeniceChangelogConsumerImpl(
343344
LOGGER.info("Start a change log consumer client for store: {}", storeName);
344345
}
345346

347+
public void throwIfReadsDisabled() {
348+
Store store = getStore();
349+
if (!store.isEnableReads()) {
350+
throw new StoreDisabledException(store.getName(), "read");
351+
}
352+
}
353+
346354
// Unit test only and read only
347355
VersionSwapMessageState getVersionSwapMessageState() {
348356
return this.versionSwapMessageState;
@@ -364,6 +372,7 @@ public CompletableFuture<Void> subscribe(Set<Integer> partitions) {
364372
}
365373

366374
protected CompletableFuture<Void> internalSubscribe(Set<Integer> partitions, PubSubTopic topic) {
375+
throwIfReadsDisabled();
367376
return CompletableFuture.supplyAsync(() -> {
368377
try {
369378
for (int i = 0; i <= MAX_SUBSCRIBE_RETRIES; i++) {
@@ -822,6 +831,7 @@ protected Collection<PubSubMessage<K, ChangeEvent<V>, VeniceChangeCoordinate>> i
822831
long timeoutInMs,
823832
String topicSuffix,
824833
boolean includeControlMessage) {
834+
throwIfReadsDisabled();
825835
Collection<PubSubMessage<K, ChangeEvent<V>, VeniceChangeCoordinate>> pubSubMessages = new ArrayList<>();
826836
Map<PubSubTopicPartition, List<DefaultPubSubMessage>> messagesMap;
827837
boolean lockAcquired = false;

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import static org.mockito.Mockito.atLeastOnce;
1717
import static org.mockito.Mockito.doAnswer;
1818
import static org.mockito.Mockito.doCallRealMethod;
19+
import static org.mockito.Mockito.doNothing;
1920
import static org.mockito.Mockito.doReturn;
2021
import static org.mockito.Mockito.doThrow;
2122
import static org.mockito.Mockito.mock;
@@ -185,6 +186,7 @@ public void setUp() {
185186
when(mockRepository.getValueSchema(storeName, 1)).thenReturn(new SchemaEntry(1, valueSchema));
186187
when(store.getVersionOrThrow(Mockito.anyInt())).thenReturn(mockVersion);
187188
when(store.getVersion(Mockito.anyInt())).thenReturn(mockVersion);
189+
when(store.isEnableReads()).thenReturn(true);
188190

189191
mockPubSubConsumer = mock(PubSubConsumerAdapter.class);
190192
oldVersionTopic = pubSubTopicRepository.getTopic(Version.composeKafkaTopic(storeName, 1));
@@ -315,6 +317,7 @@ public void testAfterImageConsumerSeek() throws ExecutionException, InterruptedE
315317
when(store.getPartitionCount()).thenReturn(2);
316318
when(mockRepository.getStore(anyString())).thenReturn(store);
317319
when(store.getVersion(Mockito.anyInt())).thenReturn(mockVersion);
320+
when(store.isEnableReads()).thenReturn(true);
318321
veniceChangelogConsumer.setStoreRepository(mockRepository);
319322

320323
Assert.assertEquals(veniceChangelogConsumer.getPartitionCount(), 2);
@@ -923,6 +926,7 @@ public void testChunkingSuccess() throws NoSuchFieldException, IllegalAccessExce
923926
@Test
924927
public void testChunkingFailure() throws NoSuchFieldException, IllegalAccessException {
925928
VeniceChangelogConsumerImpl veniceChangelogConsumer = mock(VeniceChangelogConsumerImpl.class);
929+
doNothing().when(veniceChangelogConsumer).throwIfReadsDisabled();
926930

927931
Field changelogClientConfigField = VeniceChangelogConsumerImpl.class.getDeclaredField("changelogClientConfig");
928932
changelogClientConfigField.setAccessible(true);
@@ -1038,6 +1042,9 @@ public void testConcurrentPolls() throws ExecutionException, InterruptedExceptio
10381042
PubSubMessageDeserializer.createDefaultDeserializer(),
10391043
veniceChangelogConsumerClientFactory);
10401044

1045+
VeniceChangelogConsumerImpl<String, Utf8> spyConsumer = Mockito.spy(veniceChangelogConsumer);
1046+
doNothing().when(spyConsumer).throwIfReadsDisabled();
1047+
10411048
/*
10421049
* We make this test deterministic by making the first poll hold the lock longer than the second poll, to ensure
10431050
* the second poll times out before it can acquire the lock. Thus, ensuring poll on the PubSubConsumer only gets
@@ -1058,15 +1065,15 @@ public void testConcurrentPolls() throws ExecutionException, InterruptedExceptio
10581065

10591066
// First poll task - will acquire the lock and hold it
10601067
Callable<Void> firstPollTask = () -> {
1061-
veniceChangelogConsumer.poll(pollTimeoutMs * 4); // Long timeout to ensure it gets the lock
1068+
spyConsumer.poll(pollTimeoutMs * 4); // Long timeout to ensure it gets the lock
10621069
return null;
10631070
};
10641071

10651072
// Second poll task - will timeout waiting for the lock
10661073
Callable<Void> secondPollTask = () -> {
10671074
// Wait for first poll to start, then try to poll with short timeout
10681075
firstPollStarted.await();
1069-
veniceChangelogConsumer.poll(pollTimeoutMs / 2); // Short timeout to ensure it times out
1076+
spyConsumer.poll(pollTimeoutMs / 2); // Short timeout to ensure it times out
10701077
return null;
10711078
};
10721079

@@ -1141,6 +1148,7 @@ public void testPollBeforeSubscribeCompletes() throws ExecutionException, Interr
11411148
when(delayedMockRepository.getValueSchema(storeName, 1)).thenReturn(new SchemaEntry(1, valueSchema));
11421149
when(store.getVersionOrThrow(Mockito.anyInt())).thenReturn(mockVersion);
11431150
when(store.getVersion(Mockito.anyInt())).thenReturn(mockVersion);
1151+
when(store.isEnableReads()).thenReturn(true);
11441152

11451153
CountDownLatch subscribeStarted = new CountDownLatch(1);
11461154

@@ -1240,6 +1248,7 @@ public void testVersionSwapByControlMessage() throws ExecutionException, Interru
12401248
when(store.getVersion(1)).thenReturn(mockOldVersion);
12411249
when(store.getVersionOrThrow(2)).thenReturn(mockNewVersion);
12421250
when(store.getVersion(2)).thenReturn(mockNewVersion);
1251+
when(store.isEnableReads()).thenReturn(true);
12431252
veniceChangeLogConsumer.setStoreRepository(mockRepository);
12441253

12451254
// partition 0 has an irrelevant version swap and partition 1 has a relevant version swap

internal/venice-test-common/src/integrationTest/java/com/linkedin/venice/consumer/TestChangelogConsumer.java

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import static com.linkedin.venice.ConfigKeys.KAFKA_LINGER_MS;
99
import static com.linkedin.venice.ConfigKeys.SERVER_AA_WC_WORKLOAD_PARALLEL_PROCESSING_ENABLED;
1010
import static com.linkedin.venice.ConfigKeys.ZOOKEEPER_ADDRESS;
11+
import static com.linkedin.venice.integration.utils.IntegrationTestUtils.pollChangeEventsFromSpecificChangeCaptureConsumer;
1112
import static com.linkedin.venice.integration.utils.VeniceClusterWrapperConstants.DEFAULT_PARENT_DATA_CENTER_REGION_NAME;
1213
import static com.linkedin.venice.integration.utils.VeniceControllerWrapper.D2_SERVICE_NAME;
1314
import static com.linkedin.venice.stats.ClientType.CHANGE_DATA_CAPTURE_CLIENT;
@@ -59,6 +60,7 @@
5960
import com.linkedin.venice.controllerapi.MultiStoreTopicsResponse;
6061
import com.linkedin.venice.controllerapi.UpdateStoreQueryParams;
6162
import com.linkedin.venice.endToEnd.TestChangelogValue;
63+
import com.linkedin.venice.exceptions.StoreDisabledException;
6264
import com.linkedin.venice.integration.utils.IntegrationTestUtils;
6365
import com.linkedin.venice.integration.utils.PubSubBrokerWrapper;
6466
import com.linkedin.venice.integration.utils.ServiceFactory;
@@ -188,6 +190,130 @@ public void cleanUp() {
188190
TestView.resetCounters();
189191
}
190192

193+
@Test(timeOut = TEST_TIMEOUT, priority = 3)
194+
public void testDisabledStoreVeniceChangelogConsumer() throws Exception {
195+
File inputDir = getTempDataDirectory();
196+
Schema recordSchema = TestWriteUtils.writeSimpleAvroFileWithStringToNameRecordV1Schema(inputDir);
197+
String inputDirPath = "file://" + inputDir.getAbsolutePath();
198+
String storeName = Utils.getUniqueString("store");
199+
Properties props = TestWriteUtils.defaultVPJProps(
200+
parentControllers.get(0).getControllerUrl(),
201+
inputDirPath,
202+
storeName,
203+
clusterWrapper.getPubSubClientProperties());
204+
String keySchemaStr = recordSchema.getField(DEFAULT_KEY_FIELD_PROP).schema().toString();
205+
String valueSchemaStr = NAME_RECORD_V2_SCHEMA.toString();
206+
UpdateStoreQueryParams storeParms = new UpdateStoreQueryParams().setActiveActiveReplicationEnabled(true)
207+
.setHybridRewindSeconds(500)
208+
.setHybridOffsetLagThreshold(8)
209+
.setChunkingEnabled(true)
210+
.setNativeReplicationEnabled(true)
211+
.setPartitionCount(3);
212+
MetricsRepository metricsRepository =
213+
getVeniceMetricsRepository(CHANGE_DATA_CAPTURE_CLIENT, CONSUMER_METRIC_ENTITIES, true);
214+
ControllerClient setupControllerClient =
215+
createStoreForJob(clusterName, keySchemaStr, valueSchemaStr, props, storeParms);
216+
TestUtils.assertCommand(
217+
setupControllerClient
218+
.retryableRequest(5, controllerClient1 -> setupControllerClient.updateStore(storeName, storeParms)));
219+
// Registering real data schema as schema v2.
220+
for (Schema schema: SCHEMA_HISTORY) {
221+
TestUtils.assertCommand(
222+
setupControllerClient.retryableRequest(
223+
5,
224+
controllerClient1 -> setupControllerClient.addValueSchema(storeName, schema.toString())),
225+
"Failed to add schema: " + schema.toString() + " to store " + storeName);
226+
}
227+
228+
IntegrationTestPushUtils.runVPJ(props);
229+
ZkServerWrapper localZkServer = multiRegionMultiClusterWrapper.getChildRegions().get(0).getZkServerWrapper();
230+
PubSubBrokerWrapper localKafka = multiRegionMultiClusterWrapper.getChildRegions().get(0).getPubSubBrokerWrapper();
231+
Properties consumerProperties = new Properties();
232+
String localKafkaUrl = localKafka.getAddress();
233+
consumerProperties.put(KAFKA_BOOTSTRAP_SERVERS, localKafkaUrl);
234+
consumerProperties.put(CLUSTER_NAME, clusterName);
235+
consumerProperties.put(ZOOKEEPER_ADDRESS, localZkServer.getAddress());
236+
consumerProperties.putAll(multiRegionMultiClusterWrapper.getPubSubClientProperties());
237+
ChangelogClientConfig globalChangelogClientConfig =
238+
new ChangelogClientConfig().setConsumerProperties(consumerProperties)
239+
.setControllerD2ServiceName(D2_SERVICE_NAME)
240+
.setD2ServiceName(VeniceRouterWrapper.CLUSTER_DISCOVERY_D2_SERVICE_NAME)
241+
.setD2Client(IntegrationTestPushUtils.getD2Client(localZkServer.getAddress()))
242+
.setLocalD2ZkHosts(localZkServer.getAddress())
243+
.setControllerRequestRetryCount(3)
244+
.setVersionSwapDetectionIntervalTimeInSeconds(1L)
245+
.setSpecificValue(TestChangelogValue.class)
246+
.setBootstrapFileSystemPath(Utils.getUniqueString(inputDirPath));
247+
VeniceChangelogConsumerClientFactory veniceChangelogConsumerClientFactory =
248+
new VeniceChangelogConsumerClientFactory(globalChangelogClientConfig, metricsRepository);
249+
VeniceChangelogConsumer<Utf8, TestChangelogValue> specificChangelogConsumer =
250+
veniceChangelogConsumerClientFactory.getChangelogConsumer(storeName, "0", TestChangelogValue.class);
251+
252+
TestUtils.assertCommand(
253+
setupControllerClient.retryableRequest(
254+
5,
255+
controllerClient1 -> setupControllerClient
256+
.updateStore(storeName, new UpdateStoreQueryParams().setEnableReads(false))));
257+
258+
// Wait for store update to propagate
259+
TestUtils.waitForNonDeterministicAssertion(
260+
globalChangelogClientConfig.getVersionSwapDetectionIntervalTimeInSeconds(),
261+
TimeUnit.SECONDS,
262+
() -> Assert.assertThrows(StoreDisabledException.class, () -> specificChangelogConsumer.subscribeAll().get()));
263+
264+
TestUtils.assertCommand(
265+
setupControllerClient.retryableRequest(
266+
5,
267+
controllerClient1 -> setupControllerClient
268+
.updateStore(storeName, new UpdateStoreQueryParams().setEnableReads(true))));
269+
270+
specificChangelogConsumer.subscribeAll().get();
271+
272+
Map<String, PubSubMessage<Utf8, ChangeEvent<TestChangelogValue>, VeniceChangeCoordinate>> polledChangeEventsMap =
273+
new HashMap<>();
274+
List<PubSubMessage<Utf8, ChangeEvent<TestChangelogValue>, VeniceChangeCoordinate>> polledChangeEventsList =
275+
new ArrayList<>();
276+
277+
TestUtils.assertCommand(
278+
setupControllerClient.retryableRequest(
279+
5,
280+
controllerClient1 -> setupControllerClient
281+
.updateStore(storeName, new UpdateStoreQueryParams().setEnableReads(false))));
282+
283+
// Wait for store update to propagate
284+
TestUtils.waitForNonDeterministicAssertion(
285+
globalChangelogClientConfig.getVersionSwapDetectionIntervalTimeInSeconds(),
286+
TimeUnit.SECONDS,
287+
() -> Assert.assertThrows(
288+
StoreDisabledException.class,
289+
() -> pollChangeEventsFromSpecificChangeCaptureConsumer(
290+
polledChangeEventsMap,
291+
polledChangeEventsList,
292+
specificChangelogConsumer)));
293+
294+
TestUtils.assertCommand(
295+
setupControllerClient.retryableRequest(
296+
5,
297+
controllerClient1 -> setupControllerClient
298+
.updateStore(storeName, new UpdateStoreQueryParams().setEnableReads(true))));
299+
300+
TestUtils.waitForNonDeterministicAssertion(120, TimeUnit.SECONDS, true, () -> {
301+
pollChangeEventsFromSpecificChangeCaptureConsumer(
302+
polledChangeEventsMap,
303+
polledChangeEventsList,
304+
specificChangelogConsumer);
305+
Assert.assertEquals(polledChangeEventsList.size(), 100);
306+
Assert.assertTrue(specificChangelogConsumer.isCaughtUp());
307+
});
308+
309+
Assert.assertTrue(
310+
polledChangeEventsMap.get(Integer.toString(1)).getValue().getCurrentValue() instanceof SpecificRecord);
311+
TestChangelogValue value = new TestChangelogValue();
312+
value.firstName = "first_name_1";
313+
value.lastName = "last_name_1";
314+
Assert.assertEquals(polledChangeEventsMap.get(Integer.toString(1)).getValue().getCurrentValue(), value);
315+
}
316+
191317
// This is a beefier test, so giving it a bit more time
192318
@Test(timeOut = TEST_TIMEOUT * 3, priority = 3)
193319
public void testVersionSwapInALoop() throws Exception {

0 commit comments

Comments
 (0)