/* * ToroDB * Copyright © 2014 8Kdata Technology (www.8kdata.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.torodb.backend; import com.google.common.base.Preconditions; import com.torodb.backend.ErrorHandler.Context; import com.torodb.core.annotations.TorodbIdleService; import com.torodb.core.services.IdleTorodbService; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.sql.Connection; import java.sql.SQLException; import java.util.concurrent.ThreadFactory; import javax.annotation.Nonnull; import javax.sql.DataSource; /** * */ public abstract class AbstractDbBackendService<ConfigurationT extends BackendConfiguration> extends IdleTorodbService implements DbBackendService { private static final Logger LOGGER = LogManager.getLogger(AbstractDbBackendService.class); public static final int SYSTEM_DATABASE_CONNECTIONS = 1; public static final int MIN_READ_CONNECTIONS_DATABASE = 1; public static final int MIN_SESSION_CONNECTIONS_DATABASE = 2; public static final int MIN_CONNECTIONS_DATABASE = SYSTEM_DATABASE_CONNECTIONS + MIN_READ_CONNECTIONS_DATABASE + MIN_SESSION_CONNECTIONS_DATABASE; private final ConfigurationT configuration; private final ErrorHandler errorHandler; private HikariDataSource writeDataSource; private HikariDataSource systemDataSource; private HikariDataSource readOnlyDataSource; /** * Global state variable for data import mode. If true data import mode is enabled, data import * mode is otherwise disabled. Indexes will not be created while data import mode is enabled. When * this mode is enabled importing data will be faster. */ private volatile boolean dataImportMode; /** * Configure the backend. The contract specifies that any subclass must call initialize() method * after properly constructing the object. * * @param threadFactory the thread factory that will be used to create the startup and shutdown * threads * @param configuration * @param errorHandler */ public AbstractDbBackendService(@TorodbIdleService ThreadFactory threadFactory, ConfigurationT configuration, ErrorHandler errorHandler) { super(threadFactory); this.configuration = configuration; this.errorHandler = errorHandler; this.dataImportMode = false; int connectionPoolSize = configuration.getConnectionPoolSize(); int reservedReadPoolSize = configuration.getReservedReadPoolSize(); Preconditions.checkState( connectionPoolSize >= MIN_CONNECTIONS_DATABASE, "At least " + MIN_CONNECTIONS_DATABASE + " total connections with the backend SQL database are required" ); Preconditions.checkState( reservedReadPoolSize >= MIN_READ_CONNECTIONS_DATABASE, "At least " + MIN_READ_CONNECTIONS_DATABASE + " read connection(s) is(are) required" ); Preconditions.checkState( connectionPoolSize - reservedReadPoolSize >= MIN_SESSION_CONNECTIONS_DATABASE, "Reserved read connections must be lower than total connections minus " + MIN_SESSION_CONNECTIONS_DATABASE ); } @Override protected void startUp() throws Exception { int reservedReadPoolSize = configuration.getReservedReadPoolSize(); writeDataSource = createPooledDataSource( configuration, "session", configuration.getConnectionPoolSize() - reservedReadPoolSize - SYSTEM_DATABASE_CONNECTIONS, getCommonTransactionIsolation(), false ); systemDataSource = createPooledDataSource( configuration, "system", SYSTEM_DATABASE_CONNECTIONS, getSystemTransactionIsolation(), false); readOnlyDataSource = createPooledDataSource( configuration, "cursors", reservedReadPoolSize, getGlobalCursorTransactionIsolation(), true); } @Override @SuppressFBWarnings(value = "UWF_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR", justification = "Object lifecyle is managed as a Service. Datasources are initialized in setup method") protected void shutDown() throws Exception { writeDataSource.close(); systemDataSource.close(); readOnlyDataSource.close(); } @Nonnull protected abstract TransactionIsolationLevel getCommonTransactionIsolation(); @Nonnull protected abstract TransactionIsolationLevel getSystemTransactionIsolation(); @Nonnull protected abstract TransactionIsolationLevel getGlobalCursorTransactionIsolation(); private HikariDataSource createPooledDataSource( ConfigurationT configuration, String poolName, int poolSize, TransactionIsolationLevel transactionIsolationLevel, boolean readOnly ) { HikariConfig hikariConfig = new HikariConfig(); // Delegate database-specific setting of connection parameters and any specific configuration hikariConfig.setDataSource(getConfiguredDataSource(configuration, poolName)); // Apply ToroDB-specific datasource configuration hikariConfig.setConnectionTimeout(configuration.getConnectionPoolTimeout()); hikariConfig.setPoolName(poolName); hikariConfig.setMaximumPoolSize(poolSize); hikariConfig.setTransactionIsolation(transactionIsolationLevel.name()); hikariConfig.setReadOnly(readOnly); /* * TODO: implement to add metric support. See * https://github.com/brettwooldridge/HikariCP/wiki/Codahale-Metrics * hikariConfig.setMetricRegistry(...); */ LOGGER.info("Created pool {} with size {} and level {}", poolName, poolSize, transactionIsolationLevel.name()); return new HikariDataSource(hikariConfig); } protected abstract DataSource getConfiguredDataSource(ConfigurationT configuration, String poolName); @Override public void disableDataInsertMode() { this.dataImportMode = false; } @Override public void enableDataInsertMode() { this.dataImportMode = true; } @Override public DataSource getSessionDataSource() { checkState(); return writeDataSource; } @Override public DataSource getSystemDataSource() { checkState(); return systemDataSource; } @Override public DataSource getGlobalCursorDatasource() { checkState(); return readOnlyDataSource; } protected void checkState() { if (!isRunning()) { throw new IllegalStateException("The " + serviceName() + " is not running"); } } @Override public long getDefaultCursorTimeout() { return configuration.getCursorTimeout(); } @Override public boolean isOnDataInsertMode() { return dataImportMode; } @Override public boolean includeForeignKeys() { return configuration.includeForeignKeys(); } protected void postConsume(Connection connection, boolean readOnly) throws SQLException { connection.setReadOnly(readOnly); if (!connection.isValid(500)) { throw new RuntimeException("DB connection is not valid"); } connection.setAutoCommit(false); } private Connection consumeConnection(DataSource ds, boolean readOnly) { checkState(); try { Connection c = ds.getConnection(); postConsume(c, readOnly); return c; } catch (SQLException ex) { throw errorHandler.handleException(Context.GET_CONNECTION, ex); } } @Override public Connection createSystemConnection() { checkState(); return consumeConnection(systemDataSource, false); } @Override public Connection createReadOnlyConnection() { checkState(); return consumeConnection(readOnlyDataSource, true); } @Override public Connection createWriteConnection() { checkState(); return consumeConnection(writeDataSource, false); } }