com.sap.cloud.mt.subscription.ServiceManagerCache Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of multi-tenant-subscription Show documentation
Show all versions of multi-tenant-subscription Show documentation
Spring Boot Enablement Parent
/*******************************************************************************
* © 2019-2023 SAP SE or an SAP affiliate company. All rights reserved.
******************************************************************************/
package com.sap.cloud.mt.subscription;
import com.sap.cloud.mt.subscription.exceptions.InternalError;
import com.sap.cloud.mt.tools.api.ResilienceConfig;
import com.sap.cloud.mt.tools.impl.Retry;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.ref.Cleaner;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.TimerTask;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BooleanSupplier;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import static java.util.function.Predicate.not;
/**
* For performance reasons the SM should not be accessed with single requests.Therefore, a cache is used that is filled by a scheduled
* executer at a configurable time interval. It uses method getManagedInstances() to get all information with one request.
* Operations that must be accurate, use a forceCacheUpdate flag to enforce SM access.
**/
public class ServiceManagerCache {
//used only by unit tests, to switch the scheduled reader on and off
private static BooleanSupplier blockRefresh = () -> false;
private static Callable afterFillCache = () -> null;
private static final Logger logger = LoggerFactory.getLogger(ServiceManagerCache.class);
private final ServiceManager serviceManager;
// tenantId->service instance
private final ConcurrentHashMap cachedServiceInstances = new ConcurrentHashMap<>();
// Set to true if at least one time getManagedInstances was called
private final AtomicBoolean instancesSelectedOnce = new AtomicBoolean(false);
private final Retry retryInstance;
private final Retry retryBinding;
private final ResilienceConfig resilienceConfig;
private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor((Runnable r) -> {
Thread t = Executors.defaultThreadFactory().newThread(r);
t.setDaemon(true);
return t;
});
private static final Cleaner cleaner = Cleaner.create();
private final boolean acceptInstancesWithoutTenant;
public ServiceManagerCache(ServiceManager serviceManager, Duration smCacheRefreshInterval, ResilienceConfig resilienceConfig) {
this(serviceManager, smCacheRefreshInterval, resilienceConfig, false);
}
public ServiceManagerCache(ServiceManager serviceManager, Duration smCacheRefreshInterval, ResilienceConfig resilienceConfig, boolean acceptInstancesWithoutTenant) {
this.acceptInstancesWithoutTenant = acceptInstancesWithoutTenant;
cleaner.register(this, executor::shutdownNow);
this.serviceManager = serviceManager;
if (smCacheRefreshInterval == null || smCacheRefreshInterval.isZero()) {
logger.info("Service Manager cache refresher isn't started");
} else {
startRefreshScheduler(smCacheRefreshInterval);
}
this.resilienceConfig = resilienceConfig;
var waitTimeFunction = resilienceConfig.getWaitTimeFunction();
retryInstance = Retry.RetryBuilder.create()
.numOfRetries(resilienceConfig.getNumOfRetries())
.baseWaitTime(resilienceConfig.getBaseWaitTime())
.waitTimeFunction(waitTimeFunction)
.retryExceptions(InstanceNotReady.class)
.build();
retryBinding = Retry.RetryBuilder.create()
.numOfRetries(resilienceConfig.getNumOfRetries())
.baseWaitTime(resilienceConfig.getBaseWaitTime())
.waitTimeFunction(waitTimeFunction)
.retryExceptions(BindingNotReady.class)
.build();
}
public Optional getInstance(String tenantId, boolean forceCacheUpdate) throws InternalError {
checkTenantId(tenantId);
var cachedInstance = cachedServiceInstances.get(tenantId);
// read from SM if forced, not cached or binding not ready
if (forceCacheUpdate ||
cachedInstance == null || !cachedInstance.isUsable() ||
cachedInstance.getBinding().isEmpty() || !cachedInstance.getBinding().get().isUsable()) {
var instance = getServiceInstanceFromSm(tenantId);
var bindings = new ArrayList();
if (acceptInstancesWithoutTenant && instance.isEmpty()) {
//Missing instance label workaround:
//We have the problem that instances were created that don't have an instance id label
//Determine them with labelled bindings
bindings.addAll(serviceManager.readBindingsForTenant(tenantId));
if (!bindings.isEmpty()) {
var instanceId = bindings.get(0).getServiceInstanceId();
for (var binding : bindings) {
if (!binding.getServiceInstanceId().equals(instanceId)) {
throw new InternalError("Binding for tenant %s is assigned to instance %s and not to %s"
.formatted(tenantId, binding.getServiceInstanceId(), instanceId));
}
}
logger.error("Instance with id {} is not labelled with tenant id {} \n Please fix this inconsistency manually", instanceId, tenantId);
instance = serviceManager.readInstance(instanceId);
instance.ifPresent(inst -> inst.insertTenant(tenantId));
}
}
if (instance.isEmpty()) {
deleteTenantFromCache(tenantId);
return Optional.empty();
}
if (bindings.isEmpty()) {
getBindingsFromSmAndSetThem(tenantId, instance);
} else {
//Missing instance label workaround: binding already read
instance.ifPresent(inst -> inst.setBindings(bindings));
}
if (isCachedBindingNewer(instance.get(), cachedInstance)) {
return Optional.of(cachedInstance);
}
insertAndUpdateInstances(Arrays.asList(instance.get()));
return instance;
} else {
return Optional.of(cachedInstance);
}
}
public List getInstances(boolean forceCacheUpdate) throws InternalError {
if (forceCacheUpdate || !instancesSelectedOnce.get()) {
var instances = serviceManager.readInstances().stream().toList();
var bindings = serviceManager.readBindings().stream().filter(ServiceBinding::hasTenant).toList();
var instanceIdToBindings = new HashMap>();
bindings.stream().forEach(b -> {
var bindingList = instanceIdToBindings.computeIfAbsent(b.getServiceInstanceId(), key -> new ArrayList());
bindingList.add(b);
});
instances.stream().forEach(i -> i.setBindings(instanceIdToBindings.get(i.getId())));
//Missing instance label workaround:
//We have the problem that instances were created that don't have an instance id label
//Determine them with labelled bindings
if (acceptInstancesWithoutTenant) {
//(already set tenant id,instance id it was set to)
Map setTenantIds = new HashMap<>();
var internalErrors = new ArrayList();
instances.stream().filter(Predicate.not(ServiceInstance::hasTenant)).forEach(i ->
i.getBinding().ifPresent(b -> b.getTenants().stream().forEach(tenantId -> {
//set tenant id from binding as workaround
logger.error("Instance with id {} is not labelled with tenant id {} \n Please fix this inconsistency manually", i.getId(), tenantId);
if (setTenantIds.containsKey(tenantId)) {
internalErrors.add(new InternalError("Tenant id %s was already set to service instance %s"
.formatted(tenantId, setTenantIds.get(tenantId))));
}
i.insertTenant(tenantId);
setTenantIds.put(tenantId, i.getId());
}))
);
if (!internalErrors.isEmpty()) {
throw internalErrors.get(0);
}
}
syncCacheWithSmResults(instances);
instancesSelectedOnce.set(true);
}
return new ArrayList<>(cachedServiceInstances.values());
}
public void deleteInstance(String tenantId) throws InternalError {
checkTenantId(tenantId);
var instanceOptional = getInstance(tenantId, true);
deleteTenantFromCache(tenantId);
if (instanceOptional.isEmpty()) {
return;
}
var instance = instanceOptional.get();
final var errors = new ArrayList();
instance.getBindings().forEach(binding -> {
try {
serviceManager.deleteBinding(binding.getId());
} catch (InternalError e) {
errors.add("Cannot delete binding %s".formatted(binding.getId()));
errors.add("Cause: %s".formatted(e.getMessage()));
}
});
if (errors.isEmpty()) {
try {
serviceManager.deleteInstance(instance.getId());
} catch (InternalError e) {
errors.add("Cannot delete instance %s".formatted(instance.getId()));
errors.add("Cause: %s".formatted(e.getMessage()));
}
}
if (!errors.isEmpty()) {
throw new InternalError(String.join("\n", errors));
}
}
public ServiceInstance createInstance(String tenantId, ProvisioningParameters provisioningParameters,
BindingParameters bindingParameters) throws InternalError {
checkTenantId(tenantId);
var instance = serviceManager.createInstance(tenantId, provisioningParameters)
.orElseThrow(() -> new InternalError("No instance returned"));
var binding = serviceManager.createBinding(tenantId, instance.getId(), bindingParameters);
binding.ifPresent(b -> {
instance.setBindings(Arrays.asList(b));
cachedServiceInstances.put(tenantId, instance);
});
return instance;
}
public static void setBlockRefresh(BooleanSupplier blockRefresh) {
ServiceManagerCache.blockRefresh = blockRefresh;
}
public void clearCache() {
cachedServiceInstances.clear();
}
public static void setAfterFillCache(Callable afterFillCache) {
ServiceManagerCache.afterFillCache = afterFillCache;
}
private static void checkTenantId(String tenantId) throws InternalError {
if (StringUtils.isBlank(tenantId)) {
throw new InternalError("Tenant id is null");
}
}
private void getBindingsFromSmAndSetThem(String tenantId, Optional instance) throws InternalError {
if (instance.isEmpty()) {
return;
}
final AtomicReference instanceRef = new AtomicReference<>(instance.get());
try {
retryBinding.execute(() -> {
instanceRef.get().setBindings(serviceManager.readBindingsForTenant(tenantId));
if (instanceRef.get().getBinding().isEmpty()) {
throw new BindingNotReady();
}
});
} catch (InternalError e) {
throw e;
} catch (BindingNotReady bindingNotReady) {
// retry could not yield a ready binding, the program must cope with it
} catch (Exception e) {
throw new InternalError(e);
}
}
private Optional getServiceInstanceFromSm(String tenantId) throws InternalError {
Optional instance;
try {
instance = retryInstance.execute(() -> {
var inst = serviceManager.readInstanceForTenant(tenantId);
checkInstance(inst);
return inst;
});
} catch (InternalError e) {
throw e;
} catch (InstanceNotReady instanceNotReady) {
// retry could not yield a ready instance, the program must cope with it
instance = Optional.of(instanceNotReady.getInstance());
} catch (Exception e) {
throw new InternalError(e);
}
return instance;
}
private void syncCacheWithSmResults(List readInstances) {
//delete entries
var keysOfReadInstance = readInstances.stream()
.map(ServiceInstance::getTenants)
.flatMap(Collection::stream)
.collect(Collectors.toSet());
var deleteKeys = cachedServiceInstances.values().stream()
.map(ServiceInstance::getTenants)
.flatMap(Collection::stream)
.filter(not(keysOfReadInstance::contains))
.collect(Collectors.toSet());
deleteKeys.forEach(cachedServiceInstances::remove);
// insert + update
insertAndUpdateInstances(readInstances);
}
private void checkInstance(Optional inst) throws InstanceNotReady {
if (inst.isEmpty()) {
logger.debug("Instance is null");
} else if (!inst.get().isUsable()) {
throw new InstanceNotReady(inst.get());
}
}
private void insertAndUpdateInstances(List readInstances) {
readInstances.stream()
.forEach(instance ->
instance.getTenants().forEach(tenantId -> {
var cachedInstance = cachedServiceInstances.get(tenantId);
if (!isCachedBindingNewer(instance, cachedInstance)) {
cachedServiceInstances.put(tenantId, instance);
}
})
);
}
private boolean isCachedBindingNewer(ServiceInstance instance, ServiceInstance cachedInstance) {
Optional cachedBinding = cachedInstance != null ? cachedInstance.getBinding() : Optional.empty();
if (cachedBinding.isPresent() && instance.getBinding().isEmpty()) {
return true;
}
return cachedBinding.isPresent()
&& cachedBinding.get().getCreatedAt().isAfter(instance.getBinding().get().getCreatedAt());
}
/**
* This method fills the cache periodically with fresh data. The boolean supplier "blockRefresh" is used only by unit tests. To be able
* to test this parallel initialization a controlled way to switch it on and off in tests is needed. Tests can implement a blockRefresh supplier
* that gives them control about this aspect.
*/
private void startRefreshScheduler(Duration smCacheRefreshInterval) {
logger.debug("Service Manager cache refresher is started with interval {}", smCacheRefreshInterval.toMinutes());
TimerTask refreshSmClientCache = new TimerTask() {
@Override
public void run() {
fillCache();
}
};
executor.scheduleAtFixedRate(refreshSmClientCache, 0, smCacheRefreshInterval.toMillis(), TimeUnit.MILLISECONDS);
}
private void fillCache() {
try {
if (!blockRefresh.getAsBoolean()) {
logger.debug("Read all managed instances into instance manager client lib cache");
var instances = getInstances(true);
if (instances == null || instances.isEmpty()) {
logger.debug("Service Manager didn't return service instances");
}
afterFillCache.call();
}
} catch (InternalError e) {//NOSONAR
logger.error("Could not access Service Manager", e);
} catch (Exception e) {
logger.error("Problem with afterFillCache", e);
}
}
private void deleteTenantFromCache(String tenantId) {
var cachedInstance = cachedServiceInstances.remove(tenantId);
if (cachedInstance != null) {
cachedInstance.getTenants().forEach(cachedServiceInstances::remove);
}
}
private class BindingNotReady extends Exception {
}
private class InstanceNotReady extends Exception {
private final ServiceInstance instance;
public InstanceNotReady(ServiceInstance instance) {
this.instance = instance;
}
public ServiceInstance getInstance() {
return instance;
}
}
}