Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package com.linkedin.venice.controller;

import static com.linkedin.venice.ConfigKeys.CONTROLLER_AUTO_MATERIALIZE_DAVINCI_PUSH_STATUS_SYSTEM_STORE;
import static com.linkedin.venice.ConfigKeys.CONTROLLER_AUTO_MATERIALIZE_META_SYSTEM_STORE;

import com.linkedin.venice.controllerapi.ControllerClient;
import com.linkedin.venice.controllerapi.ControllerResponse;
import com.linkedin.venice.controllerapi.NewStoreResponse;
import com.linkedin.venice.controllerapi.UpdateStoreQueryParams;
import com.linkedin.venice.exceptions.VeniceRetriableException;
import com.linkedin.venice.integration.utils.ServiceFactory;
import com.linkedin.venice.integration.utils.VeniceControllerWrapper;
import com.linkedin.venice.integration.utils.VeniceMultiRegionClusterCreateOptions;
import com.linkedin.venice.integration.utils.VeniceTwoLayerMultiRegionMultiClusterWrapper;
import com.linkedin.venice.meta.Store;
import com.linkedin.venice.meta.StoreInfo;
import com.linkedin.venice.utils.TestUtils;
import com.linkedin.venice.utils.Time;
import com.linkedin.venice.utils.Utils;
import java.util.Properties;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import org.testng.Assert;
import org.testng.annotations.Test;


public class StoreUpdateHandlerIntegrationTest {
private static final long TEST_TIMEOUT_MS = 2 * Time.MS_PER_MINUTE;
private static final long UPDATED_READ_QUOTA = 1234;

@Test(timeOut = TEST_TIMEOUT_MS)
public void testStoreUpdateHandlerRetriesWithFinalReadOnlySnapshot() throws InterruptedException {
String storeName = Utils.getUniqueString("store-update-handler");
String originalOwner = "test-owner";
RetryingStoreUpdateHandler storeUpdateHandler = new RetryingStoreUpdateHandler(storeName, UPDATED_READ_QUOTA);

Properties controllerProperties = new Properties();
controllerProperties.setProperty(CONTROLLER_AUTO_MATERIALIZE_META_SYSTEM_STORE, Boolean.FALSE.toString());
controllerProperties
.setProperty(CONTROLLER_AUTO_MATERIALIZE_DAVINCI_PUSH_STATUS_SYSTEM_STORE, Boolean.FALSE.toString());
controllerProperties.put(VeniceControllerWrapper.STORE_UPDATE_HANDLER, storeUpdateHandler);

VeniceMultiRegionClusterCreateOptions options =
new VeniceMultiRegionClusterCreateOptions.Builder().numberOfRegions(1)
.numberOfClusters(1)
.numberOfParentControllers(1)
.numberOfChildControllers(1)
.numberOfServers(0)
.numberOfRouters(0)
.replicationFactor(1)
.parentControllerProperties(controllerProperties)
.childControllerProperties(controllerProperties)
.build();

try (VeniceTwoLayerMultiRegionMultiClusterWrapper venice =
ServiceFactory.getVeniceTwoLayerMultiRegionMultiClusterWrapper(options)) {
String clusterName = venice.getClusterNames()[0];
String childControllerUrl = venice.getChildRegions().get(0).getControllerConnectString();
try (
ControllerClient parentControllerClient =
new ControllerClient(clusterName, venice.getControllerConnectString());
ControllerClient childControllerClient = new ControllerClient(clusterName, childControllerUrl)) {
NewStoreResponse newStoreResponse =
parentControllerClient.createNewStore(storeName, originalOwner, "\"string\"", "\"string\"");
Assert.assertFalse(newStoreResponse.isError(), newStoreResponse.getError());
TestUtils.waitForNonDeterministicAssertion(
30,
TimeUnit.SECONDS,
() -> Assert.assertFalse(childControllerClient.getStore(storeName).isError()));

ControllerResponse updateStoreResponse = parentControllerClient
.updateStore(storeName, new UpdateStoreQueryParams().setReadQuotaInCU(UPDATED_READ_QUOTA));
Assert.assertFalse(updateStoreResponse.isError(), updateStoreResponse.getError());
Assert.assertTrue(
storeUpdateHandler.awaitSuccessfulInvocation(30, TimeUnit.SECONDS),
"The store update handler did not succeed after its first-attempt failure");

Store callbackStore = storeUpdateHandler.getLatestStore();
Assert.assertEquals(storeUpdateHandler.getInvocationCount(), 2);
Assert.assertEquals(storeUpdateHandler.getLatestClusterName(), clusterName);
Assert.assertEquals(callbackStore.getName(), storeName);
Assert.assertEquals(callbackStore.getOwner(), originalOwner);
Assert.assertEquals(callbackStore.getReadQuotaInCU(), UPDATED_READ_QUOTA);
Assert.assertTrue(storeUpdateHandler.receivedOnlyReadOnlyStores());

String barrierOwner = "owner-after-update";
ControllerResponse setOwnerResponse = parentControllerClient.setStoreOwner(storeName, barrierOwner);
Assert.assertFalse(setOwnerResponse.isError(), setOwnerResponse.getError());
TestUtils.waitForNonDeterministicAssertion(30, TimeUnit.SECONDS, () -> {
StoreInfo childStore = childControllerClient.getStore(storeName).getStore();
Assert.assertEquals(childStore.getOwner(), barrierOwner);
Assert.assertEquals(childStore.getReadQuotaInCU(), UPDATED_READ_QUOTA);
});

StoreInfo parentStore = parentControllerClient.getStore(storeName).getStore();
Assert.assertEquals(parentStore.getReadQuotaInCU(), UPDATED_READ_QUOTA);
Assert.assertEquals(storeUpdateHandler.getInvocationCount(), 2);
}
}
}

private static final class RetryingStoreUpdateHandler implements StoreUpdateHandler {
private final String targetStoreName;
private final long targetReadQuota;
private final AtomicInteger invocationCount = new AtomicInteger();
private final AtomicReference<String> latestClusterName = new AtomicReference<>();
private final AtomicReference<Store> latestStore = new AtomicReference<>();
private final AtomicBoolean receivedOnlyReadOnlyStores = new AtomicBoolean(true);
private final CountDownLatch successfulInvocation = new CountDownLatch(1);

private RetryingStoreUpdateHandler(String targetStoreName, long targetReadQuota) {
this.targetStoreName = targetStoreName;
this.targetReadQuota = targetReadQuota;
}

@Override
public void handleStoreUpdate(String clusterName, Store store) {
if (!targetStoreName.equals(store.getName()) || store.getReadQuotaInCU() != targetReadQuota) {
return;
}

latestClusterName.set(clusterName);
latestStore.set(store);
try {
store.setOwner("unexpected-mutation");
receivedOnlyReadOnlyStores.set(false);
} catch (UnsupportedOperationException expected) {
// Expected for callback snapshots.
}

if (invocationCount.incrementAndGet() == 1) {
throw new VeniceRetriableException("Expected first-attempt store update handler failure");
}
successfulInvocation.countDown();
}

private boolean awaitSuccessfulInvocation(long timeout, TimeUnit unit) throws InterruptedException {
return successfulInvocation.await(timeout, unit);
}

private int getInvocationCount() {
return invocationCount.get();
}

private String getLatestClusterName() {
return latestClusterName.get();
}

private Store getLatestStore() {
return latestStore.get();
}

private boolean receivedOnlyReadOnlyStores() {
return receivedOnlyReadOnlyStores.get();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
import com.linkedin.venice.acl.VeniceComponent;
import com.linkedin.venice.client.store.ClientConfig;
import com.linkedin.venice.controller.Admin;
import com.linkedin.venice.controller.StoreUpdateHandler;
import com.linkedin.venice.controller.VeniceController;
import com.linkedin.venice.controller.VeniceControllerContext;
import com.linkedin.venice.controller.VeniceHelixAdmin;
Expand Down Expand Up @@ -117,6 +118,7 @@ public class VeniceControllerWrapper extends ProcessWrapper {
public static final String PARENT_D2_SERVICE_NAME = "ParentController";

public static final String SUPERSET_SCHEMA_GENERATOR = "SupersetSchemaGenerator";
public static final String STORE_UPDATE_HANDLER = "StoreUpdateHandler";

public static final double DEFAULT_STORAGE_ENGINE_OVERHEAD_RATIO = 0.85d;

Expand Down Expand Up @@ -421,6 +423,11 @@ static StatefulServiceProvider<VeniceControllerWrapper> generateService(VeniceCo
if (passedSupersetSchemaGenerator instanceof SupersetSchemaGenerator) {
supersetSchemaGenerator = Optional.of((SupersetSchemaGenerator) passedSupersetSchemaGenerator);
}
Optional<StoreUpdateHandler> storeUpdateHandler = Optional.empty();
Object passedStoreUpdateHandler = options.getExtraProperties().get(STORE_UPDATE_HANDLER);
if (passedStoreUpdateHandler instanceof StoreUpdateHandler) {
storeUpdateHandler = Optional.of((StoreUpdateHandler) passedStoreUpdateHandler);
}
Map<String, D2Client> d2Clients = options.getD2Clients();
VeniceControllerContext ctx = new VeniceControllerContext.Builder().setPropertiesList(propertiesList)
.setMetricsRepository(metricsRepository)
Expand All @@ -431,6 +438,7 @@ static StatefulServiceProvider<VeniceControllerWrapper> generateService(VeniceCo
.setRouterClientConfig(consumerClientConfig.orElse(null))
.setExternalSupersetSchemaGenerator(supersetSchemaGenerator.orElse(null))
.setAccessController(options.getDynamicAccessController())
.setStoreUpdateHandler(storeUpdateHandler.orElse(null))
.build();
VeniceController veniceController = new VeniceController(ctx);
return new VeniceControllerWrapper(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.linkedin.venice.controller;

import com.linkedin.venice.meta.Store;


/**
* Handles a successful store update consumed by a parent controller.
*/
@FunctionalInterface
public interface StoreUpdateHandler {
StoreUpdateHandler NO_OP = (clusterName, store) -> {};

/**
* @param clusterName the cluster containing the updated store
* @param store a read-only snapshot of the final store state
*/
void handleStoreUpdate(String clusterName, Store store);
}
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ public static List<Class<? extends ModuleMetricEntityInterface>> getMetricEntity
private final Optional<List<VeniceVersionLifecycleEventListener>> versionLifecycleEventListeners;
private final Optional<List<ValueSchemaCreatedListener>> valueSchemaCreatedListeners;
private final Optional<ExternalETLService> externalETLService;
private final StoreUpdateHandler storeUpdateHandler;

/**
* Allocates a new {@code VeniceController} object.
Expand Down Expand Up @@ -226,6 +227,7 @@ public VeniceController(VeniceControllerContext ctx) {
this.versionLifecycleEventListeners = Optional.ofNullable(ctx.getVersionLifecycleEventListeners());
this.valueSchemaCreatedListeners = Optional.ofNullable(ctx.getValueSchemaCreatedListeners());
this.externalETLService = Optional.ofNullable(ctx.getExternalETLService());
this.storeUpdateHandler = ctx.getStoreUpdateHandler();
this.controllerService = createControllerService();
this.adminServer = createAdminServer(false);
this.secureAdminServer = sslEnabled ? createAdminServer(true) : null;
Expand Down Expand Up @@ -262,7 +264,8 @@ private VeniceControllerService createControllerService() {
pubSubPositionTypeRegistry,
versionLifecycleEventListeners,
valueSchemaCreatedListeners,
externalETLService);
externalETLService,
storeUpdateHandler);
Admin admin = veniceControllerService.getVeniceHelixAdmin();
if (multiClusterConfigs.isParent() && !(admin instanceof VeniceParentHelixAdmin)) {
throw new VeniceException(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public class VeniceControllerContext {
private List<VeniceVersionLifecycleEventListener> versionLifecycleEventListeners;
private List<ValueSchemaCreatedListener> valueSchemaCreatedListeners;
private ExternalETLService externalETLService;
private StoreUpdateHandler storeUpdateHandler;

public List<VeniceProperties> getPropertiesList() {
return propertiesList;
Expand Down Expand Up @@ -97,6 +98,10 @@ public ExternalETLService getExternalETLService() {
return externalETLService;
}

public StoreUpdateHandler getStoreUpdateHandler() {
return storeUpdateHandler;
}

public VeniceControllerContext(Builder builder) {
this.propertiesList = builder.propertiesList;
this.metricsRepository = builder.metricsRepository;
Expand All @@ -112,6 +117,8 @@ public VeniceControllerContext(Builder builder) {
this.versionLifecycleEventListeners = builder.versionLifecycleEventListeners;
this.valueSchemaCreatedListeners = builder.valueSchemaCreatedListeners;
this.externalETLService = builder.externalETLService;
this.storeUpdateHandler =
builder.storeUpdateHandler == null ? StoreUpdateHandler.NO_OP : builder.storeUpdateHandler;
}

public static class Builder {
Expand All @@ -132,6 +139,7 @@ public static class Builder {
private List<VeniceVersionLifecycleEventListener> versionLifecycleEventListeners;
private List<ValueSchemaCreatedListener> valueSchemaCreatedListeners;
private ExternalETLService externalETLService;
private StoreUpdateHandler storeUpdateHandler;

public Builder setPropertiesList(List<VeniceProperties> propertiesList) {
this.propertiesList = propertiesList;
Expand Down Expand Up @@ -207,6 +215,11 @@ public Builder setExternalETLService(ExternalETLService externalETLService) {
return this;
}

public Builder setStoreUpdateHandler(StoreUpdateHandler storeUpdateHandler) {
this.storeUpdateHandler = storeUpdateHandler;
return this;
}

private void addDefaultValues() {
if (metricsRepository == null && !isMetricsRepositorySet) {

Expand All @@ -223,6 +236,9 @@ private void addDefaultValues() {
if (serviceDiscoveryAnnouncers == null && !isServiceDiscoveryAnnouncerSet) {
serviceDiscoveryAnnouncers = Collections.emptyList();
}
if (storeUpdateHandler == null) {
storeUpdateHandler = StoreUpdateHandler.NO_OP;
}
}

public VeniceControllerContext build() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,46 @@ public VeniceControllerService(
Optional<List<VeniceVersionLifecycleEventListener>> versionLifecycleEventListeners,
Optional<List<ValueSchemaCreatedListener>> valueSchemaCreatedListeners,
Optional<ExternalETLService> externalETLService) {
this(
multiClusterConfigs,
metricsRepository,
sslEnabled,
sslConfig,
accessController,
authorizerService,
d2Client,
d2Clients,
routerClientConfig,
icProvider,
externalSupersetSchemaGenerator,
pubSubTopicRepository,
pubSubClientsFactory,
pubSubPositionTypeRegistry,
versionLifecycleEventListeners,
valueSchemaCreatedListeners,
externalETLService,
StoreUpdateHandler.NO_OP);
}

public VeniceControllerService(
VeniceControllerMultiClusterConfig multiClusterConfigs,
MetricsRepository metricsRepository,
boolean sslEnabled,
Optional<SSLConfig> sslConfig,
Optional<DynamicAccessController> accessController,
Optional<AuthorizerService> authorizerService,
D2Client d2Client,
Map<String, D2Client> d2Clients,
Optional<ClientConfig> routerClientConfig,
Optional<ICProvider> icProvider,
Optional<SupersetSchemaGenerator> externalSupersetSchemaGenerator,
PubSubTopicRepository pubSubTopicRepository,
PubSubClientsFactory pubSubClientsFactory,
PubSubPositionTypeRegistry pubSubPositionTypeRegistry,
Optional<List<VeniceVersionLifecycleEventListener>> versionLifecycleEventListeners,
Optional<List<ValueSchemaCreatedListener>> valueSchemaCreatedListeners,
Optional<ExternalETLService> externalETLService,
StoreUpdateHandler storeUpdateHandler) {
this.multiClusterConfigs = multiClusterConfigs;

DelegatingClusterLeaderInitializationRoutine initRoutineForPushJobDetailsSystemStore =
Expand Down Expand Up @@ -207,7 +247,8 @@ public VeniceControllerService(
metricsRepository,
pubSubClientsFactory.getConsumerAdapterFactory(),
pubSubTopicRepository,
pubSubMessageDeserializer);
pubSubMessageDeserializer,
storeUpdateHandler);
this.consumerServicesByClusters.put(cluster, adminConsumerService);

this.admin.setAdminConsumerService(cluster, adminConsumerService);
Expand Down
Loading
Loading