package org.gbif.checklistbank.config;
import org.gbif.checklistbank.utils.PropertiesUtils;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import com.beust.jcommander.Parameter;
import com.google.common.base.MoreObjects;
import com.google.common.collect.Sets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A configuration for the checklist bank database connection pool
* as used by the mybatis layer. Knows how to insert a service guice module.
*/
@SuppressWarnings("PublicField")
public class ClbConfiguration {
private static final Logger LOG = LoggerFactory.getLogger(ClbConfiguration.class);
private static final String PROPERTY_PREFIX = "checklistbank.db.";
private static final Set<String> DATASOURCE_SET = Sets.newHashSet("serverName", "databaseName", "user", "password");
private static final Set<String> IGNORE = Sets.newHashSet("parserTimeout", "syncThreads", "workMem");
private static final String CONNECTION_INIT_SQL_PROP = "connectionInitSql";
public static final String PARSER_TIMEOUT_PROP = "checklistbank.parser.timeout";
public static final String IMPORT_THREADS_PROP = "checklistbank.import.threads";
private static final String WORK_MEM_PROP = "checklistbank.pg.workMem";
@NotNull
@Parameter(names = "--clb-host")
public String serverName = "localhost";
@NotNull
@Parameter(names = "--clb-db")
public String databaseName;
@NotNull
@Parameter(names = "--clb-user")
public String user;
@NotNull
@Parameter(names = "--clb-password", password = true)
public String password;
@Parameter(names = "--clb-maximum-pool-size")
@Min(1)
public int maximumPoolSize = 8;
/**
* TThe minimum number of idle connections that the pool tries to maintain.
* If the idle connections dip below this value, the pool will make a best effort to add additional connections quickly and efficiently.
* However, for maximum performance and responsiveness to spike demands, it is recommended to set this value not too low.
* Beware that postgres statically allocates the work_mem for each session which can eat up memory a lot.
*/
@Parameter(names = "--clb-minimum-idle")
@Min(0)
public int minimumIdle = 1;
/**
* This property controls the maximum amount of time that a connection is allowed to sit idle in the pool.
* A connection will never be retired as idle before this timeout.
* A value of 0 means that idle connections are never removed from the pool.
*/
@Parameter(names = "--clb-idle-timeout")
@Min(0)
public int idleTimeout = min(1);
/**
* This property controls the maximum lifetime of a connection in the pool.
* When a connection reaches this timeout it will be retired from the pool.
* An in-use connection will never be retired, only when it is closed will it then be removed.
* A value of 0 indicates no maximum lifetime (infinite lifetime), subject of course to the idleTimeout setting.
*/
@Parameter(names = "--clb-max-lifetime")
@Min(0)
public int maxLifetime = min(15);
/**
* The postgres work_mem session setting in MB that should be used for each connection.
* A value of zero or below does not set anything and thus uses the global postgres settings
*/
@Parameter(names = "--clb-work-mem")
public int workMem = 0;
@Parameter(names = "--clb-connection-timeout")
@Min(1000)
public int connectionTimeout = sec(5);
@Parameter(names = "--parser-timeout")
@Min(100)
public int parserTimeout = sec(1);
@Parameter(names = "--sync-threads")
@Min(0)
public int syncThreads = 1;
/**
* @return converted minutes in milliseconds
*/
private static int min(int minutes) {
return minutes*60000;
}
/**
* @return converted seconds in milliseconds
*/
private static int sec(int seconds) {
return seconds*1000;
}
public Properties toProps(boolean withPrefix) {
final String prefix = withPrefix ? PROPERTY_PREFIX : "";
Properties props = new Properties();
props.put(prefix + "dataSourceClassName", "org.postgresql.ds.PGSimpleDataSource");
if (withPrefix) {
props.put(PARSER_TIMEOUT_PROP, String.valueOf(parserTimeout));
props.put(IMPORT_THREADS_PROP, String.valueOf(syncThreads));
props.put(WORK_MEM_PROP, String.valueOf(workMem));
}
if (workMem > 0) {
props.put(prefix + CONNECTION_INIT_SQL_PROP, "SET work_mem='"+workMem+"MB'");
}
for (Field field : ClbConfiguration.class.getDeclaredFields()) {
if (!field.isSynthetic() && Modifier.isPublic(field.getModifiers()) && !Modifier.isStatic(field.getModifiers())) {
try {
if (IGNORE.contains(field.getName())) {
// ignore
} else if (DATASOURCE_SET.contains(field.getName())) {
props.put(prefix + "dataSource." + field.getName(), String.valueOf(field.get(this)));
} else {
props.put(prefix + field.getName(), String.valueOf(field.get(this)));
}
} catch (IllegalAccessException e) {
// cant happen, we check for public access
throw new RuntimeException(e);
}
}
}
return props;
}
public static ClbConfiguration fromProperties(Properties props) {
ClbConfiguration cfg = new ClbConfiguration();
cfg.parserTimeout = PropertiesUtils.getIntProp(props, PARSER_TIMEOUT_PROP, cfg.parserTimeout);
cfg.syncThreads = PropertiesUtils.getIntProp(props, IMPORT_THREADS_PROP, cfg.syncThreads);
cfg.workMem = PropertiesUtils.getIntProp(props, WORK_MEM_PROP, cfg.workMem);
for (Field field : ClbConfiguration.class.getDeclaredFields()) {
if (!field.isSynthetic() && Modifier.isPublic(field.getModifiers()) && !Modifier.isStatic(field.getModifiers())) {
try {
if (!IGNORE.contains(field.getName())) {
String prefix;
if (DATASOURCE_SET.contains(field.getName())) {
prefix = PROPERTY_PREFIX+"dataSource.";
} else {
prefix = PROPERTY_PREFIX;
}
Class<?> clazz = field.getType();
if (int.class == clazz) {
field.setInt(cfg, Integer.parseInt(props.getProperty(prefix + field.getName(), String.valueOf(field.get(cfg)))));
} else {
field.set(cfg, props.getProperty(prefix + field.getName(), String.valueOf(field.get(cfg))));
}
}
} catch (IllegalAccessException e) {
// cant happen, we check for public access
throw new RuntimeException(e);
}
}
}
return cfg;
}
/**
* @return a new simple postgres jdbc connection
*/
public Connection connect() throws SQLException {
String url = "jdbc:postgresql://" + serverName + "/" + databaseName;
return DriverManager.getConnection(url, user, password);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("serverName", serverName)
.add("databaseName", databaseName)
.add("user", user)
.add("password", password)
.add("connectionTimeout", connectionTimeout)
.add("maximumPoolSize", maximumPoolSize)
.add("minimumIdle", minimumIdle)
.add("idleTimeout", idleTimeout)
.add("maxLifetime", maxLifetime)
.add("workMem", workMem)
.add("parserTimeout", parserTimeout)
.add("syncThreads", syncThreads)
.toString();
}
@Override
public int hashCode() {
return Objects.hash(serverName, databaseName, user, password, maximumPoolSize, minimumIdle, idleTimeout, maxLifetime, workMem, connectionTimeout, parserTimeout, syncThreads);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
final ClbConfiguration other = (ClbConfiguration) obj;
return Objects.equals(this.serverName, other.serverName)
&& Objects.equals(this.databaseName, other.databaseName)
&& Objects.equals(this.user, other.user)
&& Objects.equals(this.password, other.password)
&& Objects.equals(this.maximumPoolSize, other.maximumPoolSize)
&& Objects.equals(this.minimumIdle, other.minimumIdle)
&& Objects.equals(this.idleTimeout, other.idleTimeout)
&& Objects.equals(this.maxLifetime, other.maxLifetime)
&& Objects.equals(this.workMem, other.workMem)
&& Objects.equals(this.connectionTimeout, other.connectionTimeout)
&& Objects.equals(this.parserTimeout, other.parserTimeout)
&& Objects.equals(this.syncThreads, other.syncThreads);
}
}