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}