com.sap.cloud.mt.runtime.DbHealthIndicatorImpl Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of multi-tenant-runtime Show documentation
Show all versions of multi-tenant-runtime Show documentation
Spring Boot Enablement Parent
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