view artifact-database/src/main/java/de/intevation/artifactdatabase/DatabaseCleaner.java @ 195:63f555bbdbc0

Fix a foreign key constraint violation when deleting outdated artifacts. artifacts/trunk@1425 c6561f87-3c4e-4783-a992-168aeb5c3f6f
author Sascha L. Teichmann <sascha.teichmann@intevation.de>
date Tue, 08 Mar 2011 10:35:40 +0000
parents 933bbc9fc11f
children b2115f484edb
line wrap: on
line source
/*
 * Copyright (c) 2010 by Intevation GmbH
 *
 * This program is free software under the LGPL (>=v2.1)
 * Read the file LGPL.txt coming with the software for details
 * or visit http://www.gnu.org/licenses/ if it does not exist.
 */

package de.intevation.artifactdatabase;

import de.intevation.artifacts.Artifact;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import java.util.ArrayList;
import java.util.List;

import javax.sql.DataSource;

import org.apache.log4j.Logger;

/**
 * The database cleaner runs in background. It sleep for a configurable
 * while and when it wakes up it removes outdated artifacts from the
 * database. Outdated means that the the last access to the artifact
 * is longer aga then the time to live of this artifact.<br>
 * Before the artifact is finally removed from the system it is
 * revived one last time an the #endOfLife() method of the artifact
 * is called.<br>
 * The artifact implementations may e.g. use this to remove some extrenal
 * resources form the system.
 *
 * @author <a href="mailto:sascha.teichmann@intevation.de">Sascha L. Teichmann</a>
 */
public class DatabaseCleaner
extends      Thread
{
    /**
     * Implementors of this interface are able to create a
     * living artifact from a given byte array.
     */
    public interface ArtifactReviver {

        /**
         * Called to revive an artifact from a given byte array.
         * @param factoryName The name of the factory which
         * created this artifact.
         * @param bytes The bytes of the serialized artifact.
         * @return The revived artfiact.
         */
        Artifact reviveArtifact(String factoryName, byte [] bytes);

    } // interface ArtifactReviver

    private static Logger logger = Logger.getLogger(DatabaseCleaner.class);

    /**
     * Number of artifacts to be loaded at once. Used to
     * mitigate the problem of a massive denial of service
     * if too many artifacts have died since last cleanup.
     */
    public static final int MAX_ROWS = 50;

    /**
     * The SQL statement to select the outdated artifacts.
     */
    public static final String SQL_OUTDATED =
        SQL.get("artifacts.outdated");

    /**
     * The SQL statement to delete some artifacts from the database.
     */
    public static final String SQL_DELETE =
        SQL.get("artifacts.delete");

    /**
     * XPath to figure out how long the cleaner should sleep between
     * cleanups. This is stored in the global configuration.
     */
    public static final String SLEEP_XPATH =
        "/artifact-database/cleaner/sleep-time/text()";

    /**
     * Default nap time between cleanups: 5 minutes.
     */
    public static final long SLEEP_DEFAULT =
        5 * 60 * 1000L; // 5 minutes

    /**
     * The configured nap time.
     */
    protected long sleepTime;

    /**
     * Internal locking mechanism to prevent some race conditions.
     */
    protected Object sleepLock = new Object();

    /**
     * A reference to the global context.
     */
    protected Object context;

    /**
     * A specialized Id filter which only delete some artifacts.
     * This is used to prevent deletion of living artifacts.
     */
    protected Id.Filter filter;

    /**
     * The reviver used to bring the dead artifact on last
     * time back to live to call endOfLife() on them.
     */
    protected ArtifactReviver reviver;

    /**
     * Default constructor.
     */
    public DatabaseCleaner() {
    }

    /**
     * Constructor to create a cleaner with a given global context
     * and a given reviver.
     * @param context The global context of the artifact database
     * @param reviver The reviver to awake artifact one last time.
     */
    public DatabaseCleaner(Object context, ArtifactReviver reviver) {
        setDaemon(true);
        sleepTime = getSleepTime();
        this.context = context;
        this.reviver = reviver;
    }

    /**
     * Sets the filter that prevents deletion of living artifacts.
     * Living artifacts are artifacts which are currently active
     * inside the artifact database. Deleting them in this state
     * would create severe internal problems.
     * @param filter
     */
    public void setFilter(Id.Filter filter) {
        this.filter = filter;
    }

    /**
     * External hook to tell the cleaner to wake up before its
     * regular nap time is over. This is the case when the artifact
     * database finds an artifact which is already outdated.
     */
    public void wakeup() {
        synchronized (sleepLock) {
            sleepLock.notify();
        }
    }

    /**
     * Fetches the sleep time from the global configuration.
     * @return the time to sleep between database cleanups in ms.
     */
    protected static long getSleepTime() {
        String sleepTimeString = Config.getStringXPath(SLEEP_XPATH);

        if (sleepTimeString == null) {
            return SLEEP_DEFAULT;
        }
        try {
            // sleep at least one second
            return Math.max(Long.parseLong(sleepTimeString), 1000L);
        }
        catch (NumberFormatException nfe) {
            logger.warn("Cleaner sleep time defaults to " + SLEEP_DEFAULT);
        }
        return SLEEP_DEFAULT;
    }

    private static final class IdData
    extends                    Id
    {
        byte [] data;
        String  factoryName;

        public IdData(int id, String factoryName, byte [] data) {
            super(id);
            this.factoryName = factoryName;
            this.data        = data;
        }
    } // class IdData

    /**
     * Cleaning is done in two phases. First we fetch a list of ids
     * of artifacts. If there are artifacts the cleaning is done.
     * Second we load the artifacts one by one one and call there
     * endOfLife() method. In this loop we remove them from database, too.
     * Each deletion is commited to ensure that a sudden failure
     * of the artifact database server does delete artifacts twice
     * or does not delete them at all. After this the first step
     * is repeated.
     */
    protected void cleanup() {
        logger.info("database cleanup");

        Connection connection      = null;
        PreparedStatement fetchIds = null;
        PreparedStatement deleteId = null;
        ResultSet         result   = null;

        int removedArtifacts = 0;

        DataSource dataSource = DBConnection.getDataSource();
        try {
            connection = dataSource.getConnection();
            connection.setAutoCommit(false);
            fetchIds = connection.prepareStatement(SQL_OUTDATED);
            deleteId = connection.prepareStatement(SQL_DELETE);

            // some dbms like derby do not support LIMIT
            // in SQL statements.
            fetchIds.setMaxRows(MAX_ROWS);

            for (;;) {
                List ids = new ArrayList();

                result = fetchIds.executeQuery();

                while (result.next()) {
                    ids.add(new IdData(
                        result.getInt(1),
                        result.getString(2),
                        result.getBytes(3)));
                }

                result.close(); result = null;

                if (ids.isEmpty()) {
                    break;
                }

                if (filter != null) {
                    ids = filter.filterIds(ids);
                }

                for (int i = ids.size()-1; i >= 0; --i) {
                    IdData idData = (IdData)ids.get(i);
                    Artifact artifact = reviver.reviveArtifact(
                        idData.factoryName, idData.data);
                    idData.data = null;

                    deleteId.setInt(1, idData.id);
                    deleteId.execute();
                    connection.commit();

                    try {
                        if (artifact != null) {
                            artifact.endOfLife(context);
                        }
                    }
                    catch (Exception e) {
                        logger.error(e.getLocalizedMessage(), e);
                    }
                } // for all fetched data

                removedArtifacts += ids.size();
            }
        }
        catch (SQLException sqle) {
            logger.error(sqle.getLocalizedMessage(), sqle);
        }
        finally {
            if (result != null) {
                try { result.close(); }
                catch (SQLException sqle) {}
            }
            if (fetchIds != null) {
                try { fetchIds.close(); }
                catch (SQLException sqle) {}
            }
            if (deleteId != null) {
                try { deleteId.close(); }
                catch (SQLException sqle) {}
            }
            if (connection != null) {
                try { connection.close(); }
                catch (SQLException sqle) {}
            }
        }

        logger.info("artifacts removed: " + removedArtifacts);
    }

    /**
     * The main code of the cleaner. It sleeps for the configured
     * nap time, cleans up the database, sleeps again and so on.
     */
    @Override
    public void run() {
        logger.info("sleep time: " + sleepTime + "ms");
        for (;;) {
            cleanup();
            long startTime = System.currentTimeMillis();

            try {
                synchronized (sleepLock) {
                    sleepLock.wait(sleepTime);
                }
            }
            catch (InterruptedException ie) {
            }

            long stopTime = System.currentTimeMillis();

            if (logger.isDebugEnabled()) {
                logger.debug("Cleaner slept " + (stopTime - startTime) + "ms");
            }
        } // for (;;)
    }
}
// vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 :

http://dive4elements.wald.intevation.org