001/* 002 * #%L 003 * Netarchivesuite - archive 004 * %% 005 * Copyright (C) 2005 - 2014 The Royal Danish Library, the Danish State and University Library, 006 * the National Library of France and the Austrian National Library. 007 * %% 008 * This program is free software: you can redistribute it and/or modify 009 * it under the terms of the GNU Lesser General Public License as 010 * published by the Free Software Foundation, either version 2.1 of the 011 * License, or (at your option) any later version. 012 * 013 * This program is distributed in the hope that it will be useful, 014 * but WITHOUT ANY WARRANTY; without even the implied warranty of 015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 016 * GNU General Lesser Public License for more details. 017 * 018 * You should have received a copy of the GNU General Lesser Public 019 * License along with this program. If not, see 020 * <http://www.gnu.org/licenses/lgpl-2.1.html>. 021 * #L% 022 */ 023 024package dk.netarkivet.archive.arcrepositoryadmin; 025 026import java.beans.PropertyVetoException; 027import java.sql.Connection; 028import java.sql.SQLException; 029 030import org.slf4j.Logger; 031import org.slf4j.LoggerFactory; 032 033import com.mchange.v2.c3p0.ComboPooledDataSource; 034 035import dk.netarkivet.archive.ArchiveSettings; 036import dk.netarkivet.common.exceptions.ArgumentNotValid; 037import dk.netarkivet.common.exceptions.IOFailure; 038import dk.netarkivet.common.utils.ExceptionUtils; 039import dk.netarkivet.common.utils.Settings; 040import dk.netarkivet.common.utils.TimeUtils; 041 042/** 043 * This class handles connections to the Archive database 044 * <p> 045 * The statements to create the tables are in scripts/sql/createBitpreservationDB.sql 046 * <p> 047 * The implementation relies on a connection pool. Once acquired through the get() method, a connection must be 048 * explicitly returned to the pool by calling the release(Connection) method. 049 * <p> 050 * THis class is intended to be used statically, and hence cannot be instantiated and is final. 051 */ 052public final class ArchiveDBConnection { 053 054 /** The class logger. */ 055 private static final Logger log = LoggerFactory.getLogger(ArchiveDBConnection.class); 056 057 /** max number of database retries. */ 058 private static final int maxdatabaseRetries = Settings.getInt(ArchiveSettings.RECONNECT_MAX_TRIES_ADMIN_DATABASE); 059 /** max time to wait between retries. */ 060 private static final int delaybetweenretries = Settings.getInt(ArchiveSettings.RECONNECT_DELAY_ADMIN_DATABASE); 061 /** The c3p0 pooled datasource backing this implementation. */ 062 private static ComboPooledDataSource dataSource = null; 063 064 /** 065 * Makes sure that the class can't be instantiated, as it is designed to be used statically. 066 */ 067 private ArchiveDBConnection() { 068 } 069 070 /** 071 * Get a connection to the harvest definition database from the pool. The pool is configured via the following 072 * configuration properties: 073 * <ul> 074 * <li>@see {@link ArchiveSettings#DB_POOL_MIN_SIZE}</li> 075 * <li>@see {@link ArchiveSettings#DB_POOL_MAX_SIZE}</li> 076 * <li>@see {@link ArchiveSettings#DB_POOL_ACQ_INC}</li> 077 * </ul> 078 * Note that the connection obtained must be returned to the pool by calling {@link #release(Connection)}. 079 * 080 * @return a connection to the harvest definition database 081 * @throws IOFailure if we cannot connect to the database (or find the driver). 082 */ 083 public static synchronized Connection get() { 084 DBSpecifics dbSpec = DBSpecifics.getInstance(); 085 String jdbcUrl = getArchiveUrl(); 086 int tries = 0; 087 Connection con = null; 088 while (tries < maxdatabaseRetries && con == null) { 089 ++tries; 090 try { 091 if (dataSource == null) { 092 initDataSource(dbSpec, jdbcUrl); 093 } 094 con = dataSource.getConnection(); 095 con.setAutoCommit(false); // different from in 096 // HarvestDBConnection 097 } catch (SQLException e) { 098 final String message = "Can't connect to database with DBurl: '" + jdbcUrl + "' using driver '" 099 + dbSpec.getDriverClassName() + "'" + "\n" + ExceptionUtils.getSQLExceptionCause(e); 100 101 if (log.isWarnEnabled()) { 102 log.warn(message, e); 103 } 104 if (tries < maxdatabaseRetries) { 105 log.info("Will wait {} before retrying", delaybetweenretries / TimeUtils.SECOND_IN_MILLIS); 106 try { 107 Thread.sleep(delaybetweenretries); 108 } catch (InterruptedException e1) { 109 // ignore this exception 110 log.trace("Interruption ignored.", e1); 111 } 112 } else { 113 throw new IOFailure(message, e); 114 } 115 } 116 } 117 return con; 118 } 119 120 /** 121 * Closes the underlying data source. 122 */ 123 public static synchronized void cleanup() { 124 if (dataSource == null) { 125 return; 126 } 127 128 try { 129 // Unclosed connections are not supposed to be found. 130 // Anyway log if there are some. 131 int numUnclosedConn = dataSource.getNumBusyConnections(); 132 if (numUnclosedConn > 0) { 133 log.error("There are {} unclosed connections!", numUnclosedConn); 134 } 135 } catch (SQLException e) { 136 if (log.isWarnEnabled()) { 137 log.warn("Could not query pool status", e); 138 } 139 } 140 if (dataSource != null) { 141 dataSource.close(); 142 dataSource = null; 143 } 144 } 145 146 /** 147 * Helper method to return a connection to the pool. 148 * 149 * @param connection a connection 150 */ 151 public static synchronized void release(Connection connection) { 152 ArgumentNotValid.checkNotNull(connection, "connection"); 153 try { 154 connection.close(); 155 } catch (SQLException e) { 156 log.error("Failed to close connection", e); 157 } 158 } 159 160 /** 161 * Method for retrieving the url for the archive database. This url will be constructed from the base-url, the 162 * machine, the port and the directory. 163 * 164 * @return The url for the archive database. 165 */ 166 public static String getArchiveUrl() { 167 StringBuilder res = new StringBuilder(); 168 res.append(Settings.get(ArchiveSettings.BASEURL_ARCREPOSITORY_ADMIN_DATABASE)); 169 170 // append the machine part of the url, if it exists. 171 String tmp = Settings.get(ArchiveSettings.MACHINE_ARCREPOSITORY_ADMIN_DATABASE); 172 if (!tmp.isEmpty()) { 173 res.append("://"); 174 res.append(tmp); 175 } 176 177 // append the port part of the url, if it exists. 178 tmp = Settings.get(ArchiveSettings.PORT_ARCREPOSITORY_ADMIN_DATABASE); 179 if (!tmp.isEmpty()) { 180 res.append(":"); 181 res.append(tmp); 182 } 183 184 // append the machine part of the url, if it exists. 185 tmp = Settings.get(ArchiveSettings.DIR_ARCREPOSITORY_ADMIN_DATABASE); 186 if (!tmp.isEmpty()) { 187 res.append("/"); 188 res.append(tmp); 189 } 190 return res.toString(); 191 } 192 193 /** 194 * Initializes the connection pool. 195 * 196 * @param dbSpec the object representing the chosen DB target system. 197 * @param jdbcUrl the JDBC URL to connect to. 198 * @throws SQLException 199 */ 200 private static void initDataSource(DBSpecifics dbSpec, String jdbcUrl) throws SQLException { 201 dataSource = new ComboPooledDataSource(); 202 String username = Settings.get(ArchiveSettings.DB_USERNAME); 203 if (!username.isEmpty()) { 204 dataSource.setUser(username); 205 } 206 String password = Settings.get(ArchiveSettings.DB_PASSWORD); 207 if (!password.isEmpty()) { 208 dataSource.setPassword(password); 209 } 210 try { 211 dataSource.setDriverClass(dbSpec.getDriverClassName()); 212 } catch (PropertyVetoException e) { 213 final String message = "Failed to set datasource JDBC driver class '" + dbSpec.getDriverClassName() + "'" 214 + "\n"; 215 throw new IOFailure(message, e); 216 } 217 218 log.info("Using jdbc url: " + jdbcUrl); 219 dataSource.setJdbcUrl(jdbcUrl); 220 221 // Configure pool size 222 dataSource.setMinPoolSize(Settings.getInt(ArchiveSettings.DB_POOL_MIN_SIZE)); 223 dataSource.setMaxPoolSize(Settings.getInt(ArchiveSettings.DB_POOL_MAX_SIZE)); 224 dataSource.setAcquireIncrement(Settings.getInt(ArchiveSettings.DB_POOL_ACQ_INC)); 225 226 // Configure idle connection testing 227 int testPeriod = Settings.getInt(ArchiveSettings.DB_POOL_IDLE_CONN_TEST_PERIOD); 228 if (testPeriod > 0) { 229 dataSource.setIdleConnectionTestPeriod(testPeriod); 230 dataSource.setTestConnectionOnCheckin(Settings 231 .getBoolean(ArchiveSettings.DB_POOL_IDLE_CONN_TEST_ON_CHECKIN)); 232 String testQuery = Settings.get(ArchiveSettings.DB_POOL_IDLE_CONN_TEST_QUERY); 233 if (!testQuery.isEmpty()) { 234 dataSource.setPreferredTestQuery(testQuery); 235 } 236 } 237 238 // Configure statement pooling 239 dataSource.setMaxStatements(Settings.getInt(ArchiveSettings.DB_POOL_MAX_STM)); 240 dataSource.setMaxStatementsPerConnection(Settings.getInt(ArchiveSettings.DB_POOL_MAX_STM_PER_CONN)); 241 242 // FIXME: unreturnedConnectionTimeout for testing. 243 dataSource.setUnreturnedConnectionTimeout(10000); 244 dataSource.setDebugUnreturnedConnectionStackTraces(true); 245 246 log.info("Connection pool initialized with the following values:\n" + "- minPoolSize={}\n" 247 + "- maxPoolSize={}\n" + "- acquireIncrement={}\n" + "- maxStatements={}\n" 248 + "- maxStatementsPerConnection={}\n" + "- idleConnTestPeriod={}\n" + "- idleConnTestQuery='{}'\n" 249 + "- idleConnTestOnCheckin={}", dataSource.getMinPoolSize(), dataSource.getMaxPoolSize(), 250 dataSource.getAcquireIncrement(), dataSource.getMaxStatements(), 251 dataSource.getMaxStatementsPerConnection(), dataSource.getIdleConnectionTestPeriod(), 252 dataSource.getPreferredTestQuery(), dataSource.isTestConnectionOnCheckin()); 253 } 254 255}