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