All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sap.cloud.mt.subscription.ServiceManagerCache Maven / Gradle / Ivy

There is a newer version: 3.3.1
Show newest version
/*******************************************************************************
 *   © 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.Cloner;
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.ofNullable(cachedInstance);
            }
            insertAndUpdateInstances(Arrays.asList(instance.get()));
            cachedInstance = cachedServiceInstances.get(tenantId);
            if (cachedInstance != null) {
                return Optional.of(cachedInstance);
            } else {
                //The cached instance is empty if no binding is available. Even in this case the instance should be
                //returned.
                return instance;
            }
        } else {
            return Optional.ofNullable(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));
            insertInstanceIntoCache(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.ofNullable(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)) {
                                insertInstanceIntoCache(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) {
            cachedServiceInstances.entrySet().removeIf(entry -> entry.getValue().getId().equals(cachedInstance.getId()));
        }
    }

    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;
        }
    }

    private void insertInstanceIntoCache(String tenantId, ServiceInstance instance) {
        var serviceInstanceCopy = instance.createCopy();
        serviceInstanceCopy.clearTenants();
        serviceInstanceCopy.insertTenant(tenantId);
        cachedServiceInstances.put(tenantId, serviceInstanceCopy);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy