ingo@100: /*
ingo@100:  * Copyright (c) 2010 by Intevation GmbH
ingo@100:  *
ingo@100:  * This program is free software under the LGPL (>=v2.1)
ingo@100:  * Read the file LGPL.txt coming with the software for details
ingo@100:  * or visit http://www.gnu.org/licenses/ if it does not exist.
ingo@100:  */
ingo@100: 
sascha@30: package de.intevation.artifactdatabase;
sascha@30: 
sascha@30: import de.intevation.artifacts.Artifact;
sascha@30: 
sascha@30: import java.sql.Connection;
sascha@30: import java.sql.PreparedStatement;
sascha@30: import java.sql.ResultSet;
sascha@93: import java.sql.SQLException;
sascha@93: 
sascha@93: import java.util.ArrayList;
sascha@93: import java.util.List;
sascha@30: 
sascha@30: import javax.sql.DataSource;
sascha@30: 
sascha@30: import org.apache.log4j.Logger;
sascha@30: 
sascha@30: /**
sascha@90:  * The database cleaner runs in background. It sleep for a configurable
sascha@90:  * while and when it wakes up it removes outdated artifacts from the
sascha@90:  * database. Outdated means that the the last access to the artifact
sascha@90:  * is longer aga then the time to live of this artifact.<br>
sascha@90:  * Before the artifact is finally removed from the system it is
sascha@90:  * revived one last time an the #endOfLife() method of the artifact
sascha@90:  * is called.<br>
sascha@90:  * The artifact implementations may e.g. use this to remove some extrenal
sascha@90:  * resources form the system.
sascha@90:  *
ingo@80:  * @author <a href="mailto:sascha.teichmann@intevation.de">Sascha L. Teichmann</a>
sascha@30:  */
sascha@30: public class DatabaseCleaner
sascha@30: extends      Thread
sascha@30: {
sascha@90:     /**
sascha@90:      * Implementors of this interface are able to create a
sascha@90:      * living artifact from a given byte array.
sascha@90:      */
sascha@41:     public interface ArtifactReviver {
sascha@41: 
sascha@90:         /**
sascha@90:          * Called to revive an artifact from a given byte array.
sascha@90:          * @param factoryName The name of the factory which
sascha@90:          * created this artifact.
sascha@90:          * @param bytes The bytes of the serialized artifact.
sascha@90:          * @return The revived artfiact.
sascha@90:          */
sascha@41:         Artifact reviveArtifact(String factoryName, byte [] bytes);
sascha@41: 
sascha@41:     } // interface ArtifactReviver
sascha@41: 
sascha@30:     private static Logger logger = Logger.getLogger(DatabaseCleaner.class);
sascha@30: 
sascha@90:     /**
sascha@90:      * Number of artifacts to be loaded at once. Used to
sascha@90:      * mitigate the problem of a massive denial of service
sascha@90:      * if too many artifacts have died since last cleanup.
sascha@90:      */
sascha@30:     public static final int MAX_ROWS = 50;
sascha@30: 
sascha@90:     /**
sascha@90:      * The SQL statement to select the outdated artifacts.
sascha@90:      */
sascha@30:     public static final String SQL_OUTDATED =
sascha@30:         SQL.get("artifacts.outdated");
sascha@30: 
sascha@90:     /**
sascha@90:      * The SQL statement to delete some artifacts from the database.
sascha@90:      */
sascha@30:     public static final String SQL_DELETE =
sascha@30:         SQL.get("artifacts.delete");
sascha@30: 
sascha@90:     /**
sascha@90:      * XPath to figure out how long the cleaner should sleep between
sascha@90:      * cleanups. This is stored in the global configuration.
sascha@90:      */
sascha@30:     public static final String SLEEP_XPATH =
sascha@30:         "/artifact-database/cleaner/sleep-time/text()";
sascha@30: 
sascha@90:     /**
sascha@90:      * Default nap time between cleanups: 5 minutes.
sascha@90:      */
sascha@30:     public static final long SLEEP_DEFAULT =
sascha@30:         5 * 60 * 1000L; // 5 minutes
sascha@30: 
sascha@90:     /**
sascha@90:      * The configured nap time.
sascha@90:      */
sascha@30:     protected long sleepTime;
sascha@30: 
sascha@90:     /**
sascha@90:      * Internal locking mechanism to prevent some race conditions.
sascha@90:      */
sascha@30:     protected Object sleepLock = new Object();
sascha@30: 
sascha@90:     /**
sascha@90:      * A reference to the global context.
sascha@90:      */
sascha@30:     protected Object context;
sascha@30: 
sascha@90:     /**
sascha@90:      * A specialized Id filter which only delete some artifacts.
sascha@90:      * This is used to prevent deletion of living artifacts.
sascha@90:      */
sascha@32:     protected Id.Filter filter;
sascha@32: 
sascha@90:     /**
sascha@90:      * The reviver used to bring the dead artifact on last
sascha@90:      * time back to live to call endOfLife() on them.
sascha@90:      */
sascha@41:     protected ArtifactReviver reviver;
sascha@41: 
sascha@90:     /**
sascha@90:      * Default constructor.
sascha@90:      */
sascha@30:     public DatabaseCleaner() {
sascha@30:     }
sascha@30: 
sascha@90:     /**
sascha@90:      * Constructor to create a cleaner with a given global context
sascha@91:      * and a given reviver.
sascha@90:      * @param context The global context of the artifact database
sascha@90:      * @param reviver The reviver to awake artifact one last time.
sascha@90:      */
sascha@41:     public DatabaseCleaner(Object context, ArtifactReviver reviver) {
sascha@30:         setDaemon(true);
sascha@30:         sleepTime = getSleepTime();
sascha@30:         this.context = context;
sascha@41:         this.reviver = reviver;
sascha@30:     }
sascha@30: 
sascha@90:     /**
sascha@90:      * Sets the filter that prevents deletion of living artifacts.
sascha@90:      * Living artifacts are artifacts which are currently active
sascha@90:      * inside the artifact database. Deleting them in this state
sascha@90:      * would create severe internal problems.
sascha@90:      * @param filter
sascha@90:      */
sascha@32:     public void setFilter(Id.Filter filter) {
sascha@32:         this.filter = filter;
sascha@32:     }
sascha@32: 
sascha@90:     /**
sascha@90:      * External hook to tell the cleaner to wake up before its
sascha@90:      * regular nap time is over. This is the case when the artifact
sascha@90:      * database finds an artifact which is already outdated.
sascha@90:      */
sascha@30:     public void wakeup() {
sascha@30:         synchronized (sleepLock) {
sascha@30:             sleepLock.notify();
sascha@30:         }
sascha@30:     }
sascha@30: 
sascha@90:     /**
sascha@90:      * Fetches the sleep time from the global configuration.
sascha@90:      * @return the time to sleep between database cleanups in ms.
sascha@90:      */
sascha@30:     protected static long getSleepTime() {
sascha@30:         String sleepTimeString = Config.getStringXPath(SLEEP_XPATH);
sascha@30: 
sascha@30:         if (sleepTimeString == null) {
sascha@30:             return SLEEP_DEFAULT;
sascha@30:         }
sascha@30:         try {
sascha@30:             // sleep at least one second
sascha@30:             return Math.max(Long.parseLong(sleepTimeString), 1000L);
sascha@30:         }
sascha@30:         catch (NumberFormatException nfe) {
sascha@30:             logger.warn("Cleaner sleep time defaults to " + SLEEP_DEFAULT);
sascha@30:         }
sascha@30:         return SLEEP_DEFAULT;
sascha@30:     }
sascha@30: 
sascha@47:     private static final class IdData
sascha@47:     extends                    Id
sascha@32:     {
sascha@30:         byte [] data;
sascha@41:         String  factoryName;
sascha@30: 
sascha@41:         public IdData(int id, String factoryName, byte [] data) {
sascha@32:             super(id);
sascha@41:             this.factoryName = factoryName;
sascha@41:             this.data        = data;
sascha@30:         }
sascha@30:     } // class IdData
sascha@30: 
sascha@30:     /**
sascha@30:      * Cleaning is done in two phases. First we fetch a list of ids
sascha@30:      * of artifacts. If there are artifacts the cleaning is done.
sascha@30:      * Second we load the artifacts one by one one and call there
sascha@30:      * endOfLife() method. In this loop we remove them from database, too.
sascha@30:      * Each deletion is commited to ensure that a sudden failure
sascha@30:      * of the artifact database server does delete artifacts twice
sascha@30:      * or does not delete them at all. After this the first step
sascha@30:      * is repeated.
sascha@30:      */
sascha@30:     protected void cleanup() {
sascha@30:         logger.info("database cleanup");
sascha@30: 
sascha@30:         Connection connection      = null;
sascha@30:         PreparedStatement fetchIds = null;
sascha@30:         PreparedStatement deleteId = null;
sascha@30:         ResultSet         result   = null;
sascha@30: 
sascha@30:         int removedArtifacts = 0;
sascha@30: 
sascha@30:         DataSource dataSource = DBConnection.getDataSource();
sascha@30:         try {
sascha@30:             connection = dataSource.getConnection();
sascha@30:             connection.setAutoCommit(false);
sascha@30:             fetchIds = connection.prepareStatement(SQL_OUTDATED);
sascha@30:             deleteId = connection.prepareStatement(SQL_DELETE);
sascha@30: 
sascha@30:             // some dbms like derby do not support LIMIT
sascha@30:             // in SQL statements.
sascha@30:             fetchIds.setMaxRows(MAX_ROWS);
sascha@30: 
sascha@30:             for (;;) {
sascha@32:                 List ids = new ArrayList();
sascha@30: 
sascha@30:                 result = fetchIds.executeQuery();
sascha@30: 
sascha@30:                 while (result.next()) {
sascha@30:                     ids.add(new IdData(
sascha@47:                         result.getInt(1),
sascha@41:                         result.getString(2),
sascha@41:                         result.getBytes(3)));
sascha@30:                 }
sascha@30: 
sascha@30:                 result.close(); result = null;
sascha@30: 
sascha@30:                 if (ids.isEmpty()) {
sascha@30:                     break;
sascha@30:                 }
sascha@30: 
sascha@32:                 if (filter != null) {
sascha@32:                     ids = filter.filterIds(ids);
sascha@32:                 }
sascha@32: 
sascha@30:                 for (int i = ids.size()-1; i >= 0; --i) {
sascha@30:                     IdData idData = (IdData)ids.get(i);
sascha@41:                     Artifact artifact = reviver.reviveArtifact(
sascha@41:                         idData.factoryName, idData.data);
sascha@30:                     idData.data = null;
sascha@30: 
sascha@30:                     deleteId.setInt(1, idData.id);
sascha@30:                     deleteId.execute();
sascha@30:                     connection.commit();
sascha@30: 
sascha@30:                     try {
sascha@30:                         if (artifact != null) {
sascha@30:                             artifact.endOfLife(context);
sascha@30:                         }
sascha@30:                     }
sascha@30:                     catch (Exception e) {
sascha@30:                         logger.error(e.getLocalizedMessage(), e);
sascha@30:                     }
sascha@30:                 } // for all fetched data
sascha@30: 
sascha@30:                 removedArtifacts += ids.size();
sascha@30:             }
sascha@30:         }
sascha@30:         catch (SQLException sqle) {
sascha@30:             logger.error(sqle.getLocalizedMessage(), sqle);
sascha@30:         }
sascha@30:         finally {
sascha@30:             if (result != null) {
sascha@30:                 try { result.close(); }
sascha@30:                 catch (SQLException sqle) {}
sascha@30:             }
sascha@30:             if (fetchIds != null) {
sascha@30:                 try { fetchIds.close(); }
sascha@30:                 catch (SQLException sqle) {}
sascha@30:             }
sascha@30:             if (deleteId != null) {
sascha@30:                 try { deleteId.close(); }
sascha@30:                 catch (SQLException sqle) {}
sascha@30:             }
sascha@30:             if (connection != null) {
sascha@30:                 try { connection.close(); }
sascha@30:                 catch (SQLException sqle) {}
sascha@30:             }
sascha@30:         }
sascha@30: 
sascha@30:         logger.info("artifacts removed: " + removedArtifacts);
sascha@30:     }
sascha@30: 
sascha@90:     /**
sascha@90:      * The main code of the cleaner. It sleeps for the configured
sascha@90:      * nap time, cleans up the database, sleeps again and so on.
sascha@90:      */
sascha@90:     @Override
sascha@30:     public void run() {
sascha@30:         logger.info("sleep time: " + sleepTime + "ms");
sascha@30:         for (;;) {
sascha@30:             cleanup();
sascha@48:             long startTime = System.currentTimeMillis();
sascha@48: 
sascha@30:             try {
sascha@30:                 synchronized (sleepLock) {
sascha@30:                     sleepLock.wait(sleepTime);
sascha@30:                 }
sascha@30:             }
sascha@30:             catch (InterruptedException ie) {
sascha@30:             }
sascha@48: 
sascha@48:             long stopTime = System.currentTimeMillis();
sascha@48: 
sascha@48:             if (logger.isDebugEnabled()) {
sascha@48:                 logger.debug("Cleaner slept " + (stopTime - startTime) + "ms");
sascha@48:             }
sascha@30:         } // for (;;)
sascha@30:     }
sascha@30: }
sascha@90: // vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 :