package com.griddynamics.jagger.jaas.service.impl;
import static com.google.common.collect.Lists.newArrayList;
import static java.lang.String.format;
import static org.apache.commons.lang3.StringUtils.contains;
import com.griddynamics.jagger.config.DataServiceConfig;
import com.griddynamics.jagger.engine.e1.services.DataService;
import com.griddynamics.jagger.jaas.service.DynamicDataService;
import com.griddynamics.jagger.jaas.service.JaggerPropertyName;
import com.griddynamics.jagger.jaas.storage.DbConfigDao;
import com.griddynamics.jagger.jaas.storage.model.DbConfigEntity;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.stereotype.Service;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Field;
import java.net.ConnectException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.List;
import java.util.Objects;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
import javax.annotation.PreDestroy;
/**
* Provides {@link com.griddynamics.jagger.engine.e1.services.DataService} service
* based on configuration described by {@link DbConfigEntity}
* and handles storage for {@link com.griddynamics.jagger.jaas.storage.CrudDao} entities.
*/
@Service
public class DynamicDataServiceImpl implements DynamicDataService {
private static final Logger LOGGER = LoggerFactory.getLogger(DynamicDataServiceImpl.class);
private final ExecutorService destroyerService = Executors.newSingleThreadExecutor(r -> new Thread(r, "DynamicDataServiceDestoyer"));
private final ConcurrentMap<Long, AbstractApplicationContext> dataServiceContexts = new ConcurrentHashMap<>();
private final DbConfigDao jaasDao;
private final DbConfigEntity defaultDbConfigEntity;
private int dataServiceCacheSize = 10;
public DynamicDataServiceImpl(@Autowired DbConfigDao jaasDao,
@Value("${jaas.data.service.cache.size:10}") int dataServiceCacheSize,
@Autowired DbConfigEntity defaultDbConfigEntity
) {
this.jaasDao = jaasDao;
if (dataServiceCacheSize > 0) {
this.dataServiceCacheSize = dataServiceCacheSize;
}
if (jaasDao.readAll().isEmpty()) {
LOGGER.info("Registering default jagger test db config: {}", defaultDbConfigEntity);
jaasDao.create(defaultDbConfigEntity);
}
this.defaultDbConfigEntity = defaultDbConfigEntity;
}
@PreDestroy
public void destroy() {
destroyerService.shutdown();
}
protected void destroy(final AbstractApplicationContext context) {
destroyerService.execute(() -> doDestroy(context));
}
protected void evictIfAboveThreshold() {
destroyerService.execute(() -> {
if (dataServiceContexts.size() >= dataServiceCacheSize) {
doDestroy(dataServiceContexts.remove(dataServiceContexts.keySet().iterator().next()));
}
});
}
/**
* To be called only inside {@link #destroyerService} workers
* which guarantees serial eviction as soon as {@link #destroyerService} is single-threaded.
*
* @param context to be destroyed
*/
private void doDestroy(final AbstractApplicationContext context) {
if (context != null) {
LOGGER.info("Destroying jagger db context...");
context.destroy();
}
}
@Override
public DataService getDataServiceFor(final Long configId) {
ApplicationContext context = getDynamicContextFor(configId);
if (Objects.isNull(context)) {
return null;
}
return context.getBean(DataService.class);
}
@Override
public ApplicationContext getDynamicContextFor(final Long configId) {
Objects.requireNonNull(configId);
ApplicationContext applicationContext = dataServiceContexts.get(configId);
if (Objects.isNull(applicationContext)) {
DbConfigEntity config = read(configId);
if (Objects.isNull(config)) {
return null;
}
evictIfAboveThreshold();
applicationContext = dataServiceContexts.computeIfAbsent(configId, s -> initDataServiceContextFor(config));
}
return applicationContext;
}
protected AbstractApplicationContext initDataServiceContextFor(DbConfigEntity config) {
LOGGER.debug("Initializing spring context for jagger test db config: {}", config);
checkConnectionToDb(config);
PropertiesPropertySource propertySource =
new PropertiesPropertySource(this.getClass().getName(), extractPropsFrom(config));
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.getEnvironment().getPropertySources().addFirst(propertySource);
applicationContext.register(DataServiceConfig.class);
applicationContext.refresh();
LOGGER.debug("Spring context has been initialized for jagger test db config: {}", config);
return applicationContext;
}
private void checkConnectionToDb(DbConfigEntity config) {
try {
Class.forName(config.getJdbcDriver());
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
try (Connection connection = DriverManager.getConnection(config.getUrl(), config.getUser(), config.getPass())) {
connection.isValid(1);
} catch (SQLException e) {
Throwable rootCause = ExceptionUtils.getRootCause(e);
if (rootCause instanceof ConnectException && contains(rootCause.getMessage(), "Connection refused")) {
LOGGER.error(format("Cannot establish connection to data base %s. ", config));
}
throw new RuntimeException(e);
}
}
@Override
public Properties extractPropsFrom(DbConfigEntity config) {
Properties configProps = new Properties();
for (Field field : config.getClass().getDeclaredFields()) {
JaggerPropertyName propertyName = field.getAnnotation(JaggerPropertyName.class);
if (Objects.nonNull(propertyName)) {
field.setAccessible(true);
configProps.setProperty(propertyName.value(), (String) ReflectionUtils.getField(field, config));
}
}
return configProps;
}
@Override
public DbConfigEntity read(Long configId) {
if (DEFAULT_DB_CONFIG_ID.equals(configId)) {
return defaultDbConfigEntity;
}
return jaasDao.read(configId);
}
@Override
public List<DbConfigEntity> readAll() {
return newArrayList(jaasDao.readAll());
}
@Override
public void create(DbConfigEntity config) {
jaasDao.create(config);
}
@Override
public void update(DbConfigEntity config) {
evictableOperation(jaasDao::update, config);
}
@Override
public void createOrUpdate(DbConfigEntity config) {
evictableOperation(jaasDao::createOrUpdate, config);
}
@Override
public void delete(DbConfigEntity config) {
evictableOperation(jaasDao::delete, config);
}
private void evictableOperation(Consumer<DbConfigEntity> op, DbConfigEntity config) {
op.accept(config);
destroy(dataServiceContexts.remove(config.getId()));
}
}