org.geowebcache.sqlite.SqliteConnectionManager.java Source code

Java tutorial

Introduction

Here is the source code for org.geowebcache.sqlite.SqliteConnectionManager.java

Source

/**
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * <p>
 * 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 General Public License for more details.
 * <p>
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @author Nuno Oliveira, GeoSolutions S.A.S., Copyright 2016
 */
package org.geowebcache.sqlite;

import org.apache.commons.io.FileUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.io.File;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * Manages the connections to sqlite databases files taking care of the concurrent access.
 * The concurrent access are managed by JVM if two JVMs access the same database file the
 * result is unpredictable.
 */
public final class SqliteConnectionManager {

    private static Log LOGGER = LogFactory.getLog(SqliteConnectionManager.class);

    private final ConcurrentHashMap<File, PooledConnection> pool = new ConcurrentHashMap<>();

    private volatile boolean stopPoolReaper = false;

    public SqliteConnectionManager(SqliteConfiguration configuration) {
        this(configuration.getPoolSize(), configuration.getPoolReaperIntervalMs());
    }

    SqliteConnectionManager(long poolSize, long poolReaperIntervalMs) {
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info(String.format("Initiating connection poll: [poolSize='%d', poolReaperIntervalMs='%d'].",
                    poolSize, poolReaperIntervalMs));
        }
        // let's load the sqlite driver
        try {
            Class.forName("org.sqlite.JDBC");
        } catch (Exception exception) {
            throw Utils.exception(exception, "Error initiating sqlite driver.");
        }
        // computing some values used by the pool reaper
        double poolSizeThreshold = poolSize * 0.9;
        double connectionsToRemove = poolSize * 0.1;
        // starting the connection pool reaper
        new Thread(() -> {
            while (!stopPoolReaper) {
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug(String.format("Current pool size is '%d' and threshold is '%f'.", pool.size(),
                            poolSizeThreshold));
                }
                if (pool.size() > poolSizeThreshold) {
                    // we exceed the pool size threshold, time to reap the less used connections
                    if (LOGGER.isInfoEnabled()) {
                        LOGGER.info(String.format("Reaping connections, current pool size %d.", pool.size()));
                    }
                    List<PooledConnection> pooledConnections = new ArrayList<>(pool.values());
                    Collections.sort(pooledConnections);
                    for (int i = 0; i < connectionsToRemove && i < pooledConnections.size(); i++) {
                        pooledConnections.get(i).reapConnection();
                    }
                }
                try {
                    Thread.sleep(poolReaperIntervalMs);
                } catch (Exception exception) {
                    Thread.currentThread().interrupt();
                    if (LOGGER.isWarnEnabled()) {
                        LOGGER.warn("Pool reaper interrupted.");
                    }
                }
            }
            if (LOGGER.isWarnEnabled()) {
                LOGGER.warn("Pool reaper stop.");
            }
        }).start();
    }

    /**
     * Helper interface to submit work.
     */
    interface Work {
        void doWork(Connection connection);
    }

    /**
     * Helper interface to submit work that needs to return something.
     */
    interface WorkWithResult<T> {
        T doWork(Connection connection);
    }

    /**
     * Helper interface to submit work that needs to return something.
     */
    interface ResultExtractor<T> {
        T extract(ResultSet resultSet) throws SQLException;
    }

    /**
     * Submit an SQL statement to be executed.
     */
    void executeSql(File file, String sql, Object... parameters) {
        doWork(file, false, connection -> {
            executeSql(connection, sql, parameters);
        });
    }

    /**
     * Submit an SQL statement to be executed with the provided connection.
     */
    void executeSql(Connection connection, String sql, Object... parameters) {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug(String.format("Executing SQL '%s'.", sql));
        }
        try (PreparedStatement statement = connection.prepareStatement(sql)) {
            for (int i = 0; i < parameters.length; i++) {
                statement.setObject(i + 1, parameters[i]);
            }
            statement.execute();
        } catch (Exception exception) {
            throw Utils.exception(exception, "Error executing SQL '%s'.", sql);
        }
    }

    /**
     * Submit a query to be executed.
     */
    <T> T executeQuery(File file, ResultExtractor<T> extractor, String query, Object... parameters) {
        return doWork(file, true, connection -> {
            return executeQuery(connection, extractor, query, parameters);
        });
    }

    /**
     * Submit a query to be executed with the provided connection.
     */
    <T> T executeQuery(Connection connection, ResultExtractor<T> extractor, String query, Object... parameters) {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug(String.format("Executing query '%s'.", query));
        }
        try (PreparedStatement statement = connection.prepareStatement(query)) {
            for (int i = 0; i < parameters.length; i++) {
                statement.setObject(i + 1, parameters[i]);
            }
            return extractor.extract(statement.executeQuery());
        } catch (Exception exception) {
            throw Utils.exception(exception, "Error executing query '%s'.", query);
        }
    }

    /**
     * Submit some work to be executed.
     */
    void doWork(File file, boolean readOnly, Work work) {
        doWork(file, readOnly, (WorkWithResult<Void>) connection -> {
            work.doWork(connection);
            return null;
        });
    }

    /**
     * Submit some work to be executed that need to return something.
     */
    <T> T doWork(File file, boolean readOnly, WorkWithResult<T> work) {
        if (readOnly) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug(String.format("Starting work on file '%s' in readonly mode.", file));
            }
        } else {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug(String.format("Starting work on file '%s' in write mode.", file));
            }
        }
        // let's find or instantiate on the fly a pool connection for the current file
        PooledConnection pooledConnection = getPooledConnection(file);
        // acquiring the proper lock on the pooled connection (read or write lock)
        pooledConnection = readOnly ? pooledConnection.getReadLockOnValidConnection()
                : pooledConnection.getWriteLockOnValidConnection();
        ExtendedConnection connection = pooledConnection.getExtendedConnection();
        try {
            // do the work
            T result = work.doWork(pooledConnection.getExtendedConnection());
            if (!connection.closeInvoked()) {
                // the work didn't close the connection, this is fine unless the connection was retained for future usage
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Close was not invoked on extended connection.");
                }
            }
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug(String.format("Work on file '%s' is done.", file));
            }
            return result;
        } finally {
            // releasing the acquired lock
            if (readOnly) {
                pooledConnection.releaseReadLock();
            } else {
                pooledConnection.releaseWriteLock();
            }
        }
    }

    void replace(File currentFile, File newFile) {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug(String.format("Replacing file '%s' with file '%s'.", currentFile, newFile));
        }
        PooledConnection currentPooledConnection = getPooledConnection(currentFile).getWriteLockOnValidConnection();
        try {
            currentPooledConnection.closeConnection();
            pool.remove(currentFile);
            FileUtils.deleteQuietly(currentFile);
            FileUtils.moveFile(newFile, currentFile);
            if (LOGGER.isInfoEnabled()) {
                LOGGER.info(String.format("File '%s' replaced with file '%s'.", currentFile, newFile));
            }
        } catch (Exception exception) {
            throw Utils.exception(exception, "Error replacing file '%s' with file '%s'.", currentFile, newFile);
        } finally {
            currentPooledConnection.releaseWriteLock();
        }
    }

    void delete(File file) {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug(String.format("Deleting file '%s'.", file));
        }
        if (!file.exists()) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug(String.format("File '%s' doesn't exists.", file));
            }
            return;
        }
        PooledConnection pooledConnection = getPooledConnection(file).getWriteLockOnValidConnection();
        try {
            pooledConnection.closeConnection();
            FileUtils.deleteQuietly(file);
            pool.remove(file);
            if (LOGGER.isInfoEnabled()) {
                LOGGER.info(String.format("File '%s' deleted.", file));
            }
        } catch (Exception exception) {
            throw Utils.exception(exception, "Error deleting file '%s'.", file);
        } finally {
            pooledConnection.releaseWriteLock();
        }
    }

    void rename(File currentFile, File newFile) {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug(String.format("Renaming file '%s' to '%s'.", currentFile, newFile));
        }
        PooledConnection pooledConnection = getPooledConnection(currentFile).getWriteLockOnValidConnection();
        try {
            pooledConnection.closeConnection();
            pool.remove(currentFile);
            FileUtils.moveFile(currentFile, newFile);
            if (LOGGER.isInfoEnabled()) {
                LOGGER.info(String.format("File '%s' renamed to '%s'.", currentFile, newFile));
            }
        } catch (Exception exception) {
            throw Utils.exception(exception, "Renaming file '%s' to '%s'.", currentFile, newFile);
        } finally {
            pooledConnection.releaseWriteLock();
        }
    }

    public Map<File, PooledConnection> getPool() {
        return pool;
    }

    void reapAllConnections() {
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("Reaping all connections.");
        }
        pool.values().forEach(PooledConnection::reapConnection);
    }

    void stopPoolReaper() {
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("Stopping the pool reaper.");
        }
        stopPoolReaper = true;
    }

    private PooledConnection getPooledConnection(File file) {
        try {
            PooledConnection pooledConnection = pool.get(file);
            if (pooledConnection != null) {
                // a connection already exists
                return pooledConnection;
            }
            // creating a new pooled connection for the database file
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug(String.format("Creating pooled connection to file '%s'.", file));
            }
            pooledConnection = new PooledConnection(file);
            pooledConnection.getWriteLock();
            try {
                PooledConnection existing = pool.putIfAbsent(file, pooledConnection);
                if (existing != null) {
                    // someone create a pooled connection for this file in the meantime
                    if (LOGGER.isDebugEnabled()) {
                        LOGGER.debug(
                                String.format("Connection to file '%s' already exists, closing this one.", file));
                    }
                    pooledConnection.closeConnection();
                    return existing;
                }
                // effectively open a connection to the database file
                pooledConnection.init();
                return pooledConnection;
            } finally {
                pooledConnection.releaseWriteLock();
            }
        } catch (Exception exception) {
            throw Utils.exception(exception, "Error opening connection to file '%s'.", file);
        }
    }

    /**
     * Helper class that contains all the info associated to an open connection.
     */
    private final class PooledConnection implements Comparable<PooledConnection> {

        private final File file;
        private Connection connection;

        private final ReentrantReadWriteLock lock;

        private long lastAccess;
        private volatile boolean closed;

        PooledConnection(File file) {
            this.file = file;
            lock = new ReentrantReadWriteLock();
            closed = true;
        }

        void init() {
            connection = openConnection(file);
            lastAccess = System.currentTimeMillis();
            closed = false;
        }

        @Override
        public int compareTo(PooledConnection other) {
            if (this.lastAccess < other.lastAccess) {
                return -1;
            }
            return 1;
        }

        ExtendedConnection getExtendedConnection() {
            lastAccess = System.currentTimeMillis();
            return new ExtendedConnection(connection);
        }

        void reapConnection() {
            getWriteLock();
            closeConnection();
            pool.remove(file);
            releaseWriteLock();
            if (LOGGER.isInfoEnabled()) {
                LOGGER.info(String.format("Connection to file '%s' reaped.", file));
            }
        }

        void closeConnection() {
            if (!closed) {
                // this connection is open let's close it
                try {
                    connection.close();
                    closed = true;
                } catch (Exception exception) {
                    throw Utils.exception("Error closing connection to file '%s'.", file);
                }
                if (LOGGER.isInfoEnabled()) {
                    LOGGER.info(String.format("Connection to file '%s' closed.", file));
                }
            }
        }

        void getReadLock() {
            String logId = "";
            if (LOGGER.isDebugEnabled()) {
                logId = UUID.randomUUID().toString();
                LOGGER.debug(String.format("[%s] Waiting for read lock on file '%s'.", logId, file));
            }
            lock.readLock().lock();
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug(String.format("[%s] Read lock on file '%s' obtained.", logId, file));
            }
        }

        PooledConnection getReadLockOnValidConnection() {
            getReadLock();
            if (!closed) {
                // this connection is ok
                return this;
            }
            releaseReadLock();
            // this connection was closed in the meantime we need to create a new one (trying 10 times)
            for (int i = 0; i < 10; i++) {
                PooledConnection pooledConnection = SqliteConnectionManager.this.getPooledConnection(file);
                // obtain the read lock
                pooledConnection.getReadLock();
                if (!pooledConnection.closed) {
                    return pooledConnection;
                }
            }
            throw Utils.exception("Could not obtain a valid connection to file '%s'.", file);
        }

        void releaseReadLock() {
            lock.readLock().unlock();
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug(String.format("Read lock on file '%s' released.", file));
            }
        }

        void getWriteLock() {
            String logId = "";
            if (LOGGER.isDebugEnabled()) {
                logId = UUID.randomUUID().toString();
                LOGGER.debug(String.format("[%s] Waiting for write lock on file '%s'.", logId, file));
            }
            lock.writeLock().lock();
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug(String.format("[%s] Write lock on file '%s' obtained.", logId, file));
            }
        }

        PooledConnection getWriteLockOnValidConnection() {
            getWriteLock();
            if (!closed) {
                // this connection is ok
                return this;
            }
            releaseWriteLock();
            // this connection was closed in the meantime we need to create a new one (trying 10 times)
            for (int i = 0; i < 10; i++) {
                PooledConnection pooledConnection = SqliteConnectionManager.this.getPooledConnection(file);
                // obtain the write lock
                pooledConnection.getWriteLock();
                if (!pooledConnection.closed) {
                    return pooledConnection;
                }
            }
            throw Utils.exception("Could not obtain a valid connection to file '%s'.", file);
        }

        void releaseWriteLock() {
            lock.writeLock().unlock();
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug(String.format("Write lock on file '%s' released.", file));
            }
        }

        private Connection openConnection(File file) {
            if (LOGGER.isInfoEnabled()) {
                LOGGER.info(String.format("Opening connection to file '%s'.", file));
            }
            Utils.createFileParents(file);
            try {
                return DriverManager.getConnection("jdbc:sqlite:" + file.getPath());
            } catch (Exception exception) {
                throw Utils.exception(exception, "Error opening connection to file '%s'.", file);
            }
        }
    }
}