Mercurial > dive4elements > framework
view artifact-database/src/main/java/de/intevation/artifactdatabase/DatabaseCleaner.java @ 330:3168af23aec5
Added a CallContext.isInBackground() method to determine if an Artifact or a Collection has started a background thread and is currently locked.
artifacts/trunk@2666 c6561f87-3c4e-4783-a992-168aeb5c3f6f
author | Ingo Weinzierl <ingo.weinzierl@intevation.de> |
---|---|
date | Wed, 07 Sep 2011 13:51:02 +0000 |
parents | 03e508e57b85 |
children | b7831cefbb62 |
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.common.utils.Config; import de.intevation.artifacts.common.utils.StringUtils; import de.intevation.artifacts.Artifact; import de.intevation.artifactdatabase.db.SQL; import de.intevation.artifactdatabase.db.DBConnection; 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 java.util.Set; import java.util.Collections; 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); void killedArtifacts(List<String> identifiers); void killedCollections(List<String> identifiers); } // interface ArtifactReviver public interface LockedIdsProvider { Set<Integer> getLockedIds(); } // interface LockedIdsProvider 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; public static final Set<Integer> EMPTY_IDS = Collections.emptySet(); /** * The SQL statement to select the outdated artifacts. */ public String SQL_OUTDATED; public String SQL_OUTDATED_COLLECTIONS; public String SQL_DELETE_COLLECTION_ITEMS; public String SQL_DELETE_COLLECTION; /** * The SQL statement to delete some artifacts from the database. */ public String SQL_DELETE_ARTIFACT; /** * 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 LockedIdsProvider lockedIdsProvider; /** * The reviver used to bring the dead artifact on last * time back to live to call endOfLife() on them. */ protected ArtifactReviver reviver; protected DBConnection dbConnection; /** * 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, DBConfig config) { setDaemon(true); sleepTime = getSleepTime(); this.context = context; this.reviver = reviver; this.dbConnection = config.getDBConnection(); setupSQL(config.getSQL()); } protected void setupSQL(SQL sql) { SQL_OUTDATED = sql.get("artifacts.outdated"); SQL_OUTDATED_COLLECTIONS = sql.get("collections.outdated"); SQL_DELETE_COLLECTION_ITEMS = sql.get("delete.collection.items"); SQL_DELETE_COLLECTION = sql.get("delete.collection"); SQL_DELETE_ARTIFACT = sql.get("artifacts.delete"); } /** * 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 setLockedIdsProvider(LockedIdsProvider lockedIdsProvider) { this.lockedIdsProvider = lockedIdsProvider; } /** * 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 class IdIdentifier { int id; String identifier; private IdIdentifier(int id, String identifier) { this.id = id; this.identifier = identifier; } } // class IdIdentifier private static final class IdData extends IdIdentifier { byte [] data; String factoryName; public IdData( int id, String factoryName, byte [] data, String identifier ) { super(id, identifier); 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 stmnt = null; ResultSet result = null; DataSource dataSource = dbConnection.getDataSource(); Set<Integer> lockedIds = lockedIdsProvider != null ? lockedIdsProvider.getLockedIds() : EMPTY_IDS; String questionMarks = lockedIds.isEmpty() ? "-666" // XXX: A bit hackish. : StringUtils.repeat('?', lockedIds.size(), ','); List<String> deletedCollections = new ArrayList<String>(); List<String> deletedArtifacts = new ArrayList<String>(); try { connection = dataSource.getConnection(); connection.setAutoCommit(false); fetchIds = connection.prepareStatement( SQL_OUTDATED.replace("$LOCKED_IDS$", questionMarks)); // some dbms like derby do not support LIMIT // in SQL statements. fetchIds.setMaxRows(MAX_ROWS); // Fetch ids of outdated collections stmnt = connection.prepareStatement( SQL_OUTDATED_COLLECTIONS.replace( "$LOCKED_IDS$", questionMarks)); // fill in the locked ids int idx = 1; for (Integer id: lockedIds) { fetchIds.setInt(idx, id); stmnt .setInt(idx, id); ++idx; } ArrayList<IdIdentifier> cs = new ArrayList<IdIdentifier>(); result = stmnt.executeQuery(); while (result.next()) { cs.add(new IdIdentifier( result.getInt(1), result.getString(2))); } result.close(); result = null; stmnt.close(); stmnt = null; // delete collection items stmnt = connection.prepareStatement(SQL_DELETE_COLLECTION_ITEMS); for (IdIdentifier id: cs) { stmnt.setInt(1, id.id); stmnt.execute(); } stmnt.close(); stmnt = null; // delete collections stmnt = connection.prepareStatement(SQL_DELETE_COLLECTION); for (IdIdentifier id: cs) { stmnt.setInt(1, id.id); stmnt.execute(); deletedCollections.add(id.identifier); } stmnt.close(); stmnt = null; connection.commit(); cs = null; // remove artifacts stmnt = connection.prepareStatement(SQL_DELETE_ARTIFACT); for (;;) { List<IdData> ids = new ArrayList<IdData>(); result = fetchIds.executeQuery(); while (result.next()) { ids.add(new IdData( result.getInt(1), result.getString(2), result.getBytes(3), result.getString(4))); } result.close(); result = null; if (ids.isEmpty()) { break; } for (int i = ids.size()-1; i >= 0; --i) { IdData idData = ids.get(i); Artifact artifact = reviver.reviveArtifact( idData.factoryName, idData.data); idData.data = null; stmnt.setInt(1, idData.id); stmnt.execute(); connection.commit(); try { if (artifact != null) { artifact.endOfLife(context); } } catch (Exception e) { logger.error(e.getLocalizedMessage(), e); } deletedArtifacts.add(idData.identifier); } // for all fetched data } } catch (SQLException sqle) { logger.error(sqle.getLocalizedMessage(), sqle); } finally { if (result != null) { try { result.close(); } catch (SQLException sqle) {} } if (stmnt != null) { try { stmnt.close(); } catch (SQLException sqle) {} } if (fetchIds != null) { try { fetchIds.close(); } catch (SQLException sqle) {} } if (connection != null) { try { connection.close(); } catch (SQLException sqle) {} } } if (!deletedCollections.isEmpty()) { reviver.killedCollections(deletedCollections); } if (!deletedArtifacts.isEmpty()) { reviver.killedArtifacts(deletedArtifacts); } if (logger.isDebugEnabled()) { logger.debug( "collections removed: " + deletedCollections.size()); logger.debug( "artifacts removed: " + deletedArtifacts.size()); } } /** * 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 :