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

com.sap.cloud.mt.runtime.DbHealthIndicatorImpl Maven / Gradle / Ivy

The newest version!
/*******************************************************************************
 *   © 2019-2024 SAP SE or an SAP affiliate company. All rights reserved.
 ******************************************************************************/
package com.sap.cloud.mt.runtime;

import com.sap.cloud.mt.subscription.DataSourceInfo;
import com.sap.cloud.mt.subscription.DbIdentifiers;
import com.sap.cloud.mt.subscription.SqlOperations;
import com.sap.cloud.mt.subscription.exceptions.InternalError;
import com.sap.cloud.mt.subscription.exceptions.ParameterException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;

public class DbHealthIndicatorImpl {
	private static final Logger logger = LoggerFactory.getLogger(DbHealthIndicatorImpl.class);
	private static final int STACK_TRACE_WRITE_PERIOD = 100;
	private static final String DETAIL_INFORMATION = "Detail information:";
	private final TenantAwareDataSource dataSource;
	private volatile long lastChecked = 0;
	private AtomicReference lastHealth = new AtomicReference<>(null);
	private final Long healthCheckIntervalMillis;
	private final HealthUp healthUp;
	private final HealthDownDetails healthDownDetails;
	private final HealthUpDetails healthUpDetails;
	private AtomicInteger callCounter = new AtomicInteger(0);
	private final ReentrantLock lock = new ReentrantLock();
	private final SqlOperations sqlOperations;
	private final String healthDummySelect;

	public DbHealthIndicatorImpl(String healthDummySelect, TenantAwareDataSource dataSource,
								 Long healthCheckIntervalMillis,
								 HealthUp healthUp, HealthDownDetails healthDownDetails,
								 HealthUpDetails healthUpDetails) {
		this.healthDummySelect = healthDummySelect;
		this.dataSource = dataSource;
		this.healthCheckIntervalMillis = healthCheckIntervalMillis;
		this.healthUp = healthUp;
		this.healthDownDetails = healthDownDetails;
		this.healthUpDetails = healthUpDetails;
		try {
			if (dataSource.getDbType() != null) {
				this.sqlOperations = SqlOperations.build(dataSource.getDbType());
			} else {
				this.sqlOperations = SqlOperations.build(DbIdentifiers.DB.HANA);
			}
			this.sqlOperations.setDummySelectStatement(healthDummySelect);
		} catch (InternalError internalError) {
			throw new ParameterException(internalError);
		}
	}

	public T health() {
		boolean hasLock = false;
		try {
			hasLock = lock.tryLock();
			if (!hasLock) {
				if (lastHealth.get() == null) {
					logger.debug("Lock for health check couldn't be acquired. Return positive result as no last health check exists");
					return healthUp.execute();
				}
				logger.atDebug()
						.setMessage("Return result of last health check as no lock could be acquired. Result was {}")
						.addArgument(() -> lastHealth.get()).log();
				return lastHealth.get();
			}
			callCounter.incrementAndGet();
			if (callCounter.get() > STACK_TRACE_WRITE_PERIOD) {
				callCounter.set(0);
			}
			if (lastChecked != 0 && ((System.currentTimeMillis() - lastChecked) < healthCheckIntervalMillis)) {
				logger.atDebug()
						.setMessage("Result of last health check is returned as not much time has passed, result is {}")
						.addArgument(() -> lastHealth.get()).log();
				return lastHealth.get();
			}
			try {
				List detailInfo = new ArrayList<>();
				boolean down = false;
				// If database identifiers are set, mt-lib's own containers can be used for the health check
				if (dataSource.getDataSourceLookup().knowsDbCredentials()) {
					if (dataSource.getDataSourceLookup().hasDbIdentifiers()) {
						down = isDownNewCheck(detailInfo);
					} else {
						down = isDown(detailInfo, getOneDataSourceInfoPerDb());
					}
				} else {
					detailInfo.add("Could not determine DB credentials, no tenant subscribed");
					down = false;
				}
				if (down) {
					lastHealth.set(healthDownDetails.execute(DETAIL_INFORMATION, detailInfo));
				} else {
					lastHealth.set(healthUpDetails != null ?
							healthUpDetails.execute(DETAIL_INFORMATION, detailInfo) : healthUp.execute());
				}
			} catch (Exception e) {
				logger.error("Unexpected exception was thrown in health check", e);
				lastHealth.set(healthDownDetails.execute(DETAIL_INFORMATION, Arrays.asList(e.getMessage())));
			}
			lastChecked = System.currentTimeMillis();
			return lastHealth.get();
		} finally {
			if (hasLock) {
				lock.unlock();
			}
		}
	}

	private Map> getOneDataSourceInfoPerDb() throws InternalError {
		List dataSourceInfos = dataSource.getDataSourceLookup().getCachedDataSource();
		//maybe service was just restarted and no pools available, yet
		if (dataSourceInfos.isEmpty()) {
			logger.debug("No datasource cached. Load one data source for health check");
			dataSource.getDataSourceLookup().loadOneTenantPerDb();
			dataSourceInfos = dataSource.getDataSourceLookup().getCachedDataSource();
			if (dataSourceInfos.isEmpty()) {
				logger.error("Could not determine a data source for health check");
			}
		}
		return dataSourceInfoPerDb(dataSourceInfos);
	}

	private boolean isDown(List detailInfo, Map> uriToDbInfo) {
		logger.debug("Execute the old implementation of the health check.");
		if (uriToDbInfo.isEmpty()) {
			detailInfo.add("No DB schemas for test available");
			return false;
		}
		AtomicBoolean down = new AtomicBoolean(false);
		uriToDbInfo.entrySet().forEach(infoListEntry -> checkOneDB(detailInfo, down, infoListEntry.getValue()));
		return down.get();
	}

	private void checkOneDB(List detailInfo, AtomicBoolean down, List infoListEntry) {
		if (infoListEntry == null) {
			return;
		}
		//find first connection that works or fails
		infoListEntry.stream()
				.filter(info -> {
					try {
						dataSource.getDataSourceLookup().checkDataSource(info.getTenantId(), healthDummySelect);
						detailInfo.add("Connection for DB " + info.getHost() + ":" + info.getPort() + " is ok");
						//This DB is ok
						return true;
					} catch (SQLException e) {
						//not necessarily an error, tenant could be unsubscribed, this doesn't mean that the DB is broken
						try {
							if (dataSource.doesTenantExist(info.getTenantId())) {
								logger.error("Could not open connection for DB {}", info.getHost() + ":" + info.getPort());
								logger.debug("The following error was reported: {}", e.getMessage());
								detailInfo.add("Could not open connection for DB " + info.getHost() + ":" + info.getPort());
								down.set(true);
								//problem with DB or pool
								return true;
							} else {
								//cannot decide if DB is down, tenant could be unsubscribed
								return false;
							}
						} catch (InternalError internalError) {
							detailInfo.add("Error occurred:" + internalError.getMessage());
							down.set(true);
							//problem with DB or pool
							return true;
						}
					}
				}).findFirst();
	}

	private boolean isDownNewCheck(List detailInfo) {
		logger.debug("Execute the new implementation of the health check.");
		List healthCheckResults = dataSource.getDataSourceLookup().checkDataSourcePerDb(healthDummySelect);
		Boolean[] down = {false};
		healthCheckResults.stream().forEach(result -> {
			if (!result.isOk()) {
				logger.error("Could not open connection for DB {}", result.getDbIdentifier());
				if (callCounter.get() == STACK_TRACE_WRITE_PERIOD) {
					logger.error("The following error was reported: ", result.getException());
				} else {
					logger.error("The following error was reported: {}", result.getException().getMessage());
				}
				detailInfo.add("Could not open connection for DB " + result.getDbIdentifier() + ". Error is: " +
						result.getException().getMessage());
				down[0] = true;
			} else {
				logger.debug("Connection for DB {} is ok", result.getDbIdentifier());
				detailInfo.add("Connection for DB " + result.getDbIdentifier() + " is ok");
			}
		});
		return down[0];
	}

	public String getHealthDummySelect() {
		return healthDummySelect;
	}

	private Map> dataSourceInfoPerDb(List infoList) {
		Map> urlToInfo = new HashMap<>();
		infoList.stream().forEach(i -> {
			List list = urlToInfo.get(i.getDbKey());
			if (list == null) {
				list = new ArrayList<>();
				urlToInfo.put(i.getDbKey(), list);
			}
			list.add(i);
		});
		return urlToInfo;
	}

	@FunctionalInterface
	public interface HealthUp {
		public T execute();
	}

	@FunctionalInterface
	public interface HealthDown {
		public T execute();
	}

	@FunctionalInterface
	public interface HealthDownDetails {
		public T execute(String text, List detailInfo);
	}

	@FunctionalInterface
	public interface HealthUpDetails {
		public T execute(String text, List detailInfo);
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy