/******************************************************************************* * Copyright (c) 2004, 2010 BREDEX GmbH. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * BREDEX GmbH - initial API and implementation and/or initial documentation *******************************************************************************/ package org.eclipse.jubula.client.core.persistence.locking; import java.util.Collection; import java.util.Date; import java.util.List; import javax.persistence.EntityManager; import javax.persistence.EntityTransaction; import javax.persistence.LockModeType; import javax.persistence.NoResultException; import javax.persistence.PersistenceException; import javax.persistence.Query; import javax.persistence.TemporalType; import org.eclipse.jubula.client.core.i18n.Messages; import org.eclipse.jubula.client.core.model.IPersistentObject; import org.eclipse.jubula.client.core.persistence.PMAlreadyLockedException; import org.eclipse.jubula.client.core.persistence.PMDirtyVersionException; import org.eclipse.jubula.client.core.persistence.PMObjectDeletedException; import org.eclipse.jubula.client.core.persistence.Persistor; import org.eclipse.jubula.tools.internal.constants.StringConstants; import org.eclipse.jubula.tools.internal.exception.JBFatalAbortException; import org.eclipse.jubula.tools.internal.messagehandling.MessageIDs; import org.eclipse.jubula.tools.internal.utils.IsAliveThread; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author BREDEX GmbH * @created 30.11.2005 */ public final class LockManager { /** * <code>DB_GUARD_ID</code> Id of the DB lock entity. This MUST be 1. */ private static final Long DB_GUARD_ID = Long.valueOf(1); /** * @author BREDEX GmbH * @created 01.12.2005 */ public interface DBRunnable { /** * @param sess sess * @return Result */ public Result run(EntityManager sess); } /** result codes */ private enum Result { OK, FAILED, OBJECT_DIRTY, OBJECT_DELETED } /** update interval for the application timestamp (in seconds) */ private static final int UPDATE_TIMESTAMP_SECS = 60; /** time after which a application is considered dead (in seconds) */ private static final int TIMEOUT_APPLICATION = 3 * UPDATE_TIMESTAMP_SECS; /** standard logging */ private static Logger log = LoggerFactory.getLogger(LockManager.class); /** singleton instance */ private static LockManager instance = null; /** the instance used for locking the complete data of the subsystem */ private DbGuardPO m_dbGuard; /** * The representation of the running application in the locking * subsystem. */ private ApplicationPO m_application; /** thread which periodically updates the applications timestamp */ private Thread m_keepAliveThread; /** shall the keepAliveThread continue */ private boolean m_keepRunning = true; /** used by instance() */ private LockManager() { initDbObjects(); } /** * Read the guardian row. This instance will be used for prohibiting * parallel modifications in the lock tables. * Create and persist the application object. */ private void initDbObjects() { EntityManager sess = null; try { sess = Persistor.instance().openSession(); EntityTransaction tx = sess.getTransaction(); tx.begin(); m_dbGuard = sess.find(DbGuardPO.class, DB_GUARD_ID); m_application = new ApplicationPO(Long.MIN_VALUE); sess.persist(m_application); tx.commit(); } catch (PersistenceException e) { throw new JBFatalAbortException(Messages.LockingWontStart, e, MessageIDs.E_DATABASE_GENERAL); } finally { Persistor.instance().dropSessionWithoutLockRelease(sess); } updateTimestamp(); } /** * Runs an action inside a transaction * @param action Runnable holding the transaction * @return the return value of action */ private synchronized Result runInSession(DBRunnable action) { EntityManager sess = null; EntityTransaction tx = null; Result result = Result.FAILED; try { sess = Persistor.instance().openSession(); tx = sess.getTransaction(); tx.begin(); lockDB(sess); result = action.run(sess); tx.commit(); tx = null; } catch (PersistenceException e) { log.error(Messages.FailedToUpdateApplicationTimestamp, e); } finally { if (tx != null) { tx.rollback(); } Persistor.instance().dropSessionWithoutLockRelease(sess); } return result; } /** singleton getter * @return the only instance of the LockManager */ public static LockManager instance() { if (instance == null) { instance = new LockManager(); } return instance; } /** * Updates the timestamp of the application by executing a dml statement. * Note that the instance must be refreshed after the update since HQL * bulk queries don't synchronize the in memory state of POJOs. */ void updateTimestamp() { // check for disposed LockManager if (LockManager.isRunning() && m_application != null) { runInSession(new DBRunnable() { public Result run(EntityManager sess) { Query q = sess .createQuery("update ApplicationPO app set app.timestamp = CURRENT_TIMESTAMP where app.id = :id"); //$NON-NLS-1$ q.setParameter("id", m_application.getId()); //$NON-NLS-1$ if (q.executeUpdate() != 1) { log.error(Messages.UpdateOfTimestampFailed); } return Result.OK; } }); runInSession(new DBRunnable() { @SuppressWarnings("synthetic-access") public Result run(EntityManager sess) { sess.detach(m_application); m_application = sess.find(ApplicationPO.class, m_application.getId()); return Result.OK; } }); } } /** * Find timed out application in the DB, if found remove any locks and the * application itself. * */ @SuppressWarnings("unchecked") void checkForTimeouts() { // check for disposed LockManager if (LockManager.isRunning() && m_application != null) { runInSession(new DBRunnable() { public Result run(EntityManager sess) { Query deadQuery = sess.createQuery("select app from ApplicationPO app where app.timestamp < :deadTime"); //$NON-NLS-1$ Date deadTime = new Date(m_application.getTimestamp() .getTime() - TIMEOUT_APPLICATION * 1000); deadQuery.setParameter( "deadTime", deadTime, TemporalType.TIMESTAMP); //$NON-NLS-1$ List<ApplicationPO> deadApps = deadQuery.getResultList(); for (ApplicationPO appl : deadApps) { removeApp(sess, appl); } return Result.OK; } }); } } /** * @param sess working session to perform lock in * @throws PersistenceException in case of db error, not expected */ public void lockDB(EntityManager sess) throws PersistenceException { m_dbGuard = sess.find(DbGuardPO.class, m_dbGuard.getId()); sess.lock(m_dbGuard, LockModeType.PESSIMISTIC_WRITE); } /** * Starts a thread which update the timestamp of the application * periodically. */ public synchronized void startKeepAlive() { if (m_keepAliveThread == null) { m_keepAliveThread = new IsAliveThread(new Runnable() { public void run() { while (m_keepRunning) { // The timestamp is maintained in the m_application // instance, therefore the order of the following // statements is crucial. updateTimestamp(); checkForTimeouts(); try { Thread.sleep(UPDATE_TIMESTAMP_SECS * 1000); } catch (InterruptedException e) { // just ignore } } } }, "LockManger.KeepAlive"); //$NON-NLS-1$ m_keepAliveThread.start(); } else { log.warn(Messages.KeepAliveAlreadyActive + StringConstants.DOT); } } /** * shutdown the locking subsystems, remove any remaining locks and the * application from the db. * Discard the singleton instance. * */ public synchronized void dispose() { m_keepRunning = false; m_keepAliveThread.interrupt(); runInSession(new DBRunnable() { public Result run(EntityManager sess) { // Shutdown may be called several times depending on the // application running (ITE, testexec, ...). During shutdown // the application might have been deleted before. Therefore // some checking is required. try { if (m_application != null) { removeApp(sess, m_application); m_application = null; } } catch (Throwable t) { log.debug("application already removed", t); //$NON-NLS-1$ } return Result.OK; } }); instance = null; } /** * remove an application PO and its locks from the db * @param sess Session to be used as execution context * @param app The application (and its locks) which shall be removed. */ private void removeApp(EntityManager sess, ApplicationPO app) { Query delQuery = sess.createQuery("delete from DbLockPO lock where lock.application = :app"); //$NON-NLS-1$ delQuery.setParameter("app", app); //$NON-NLS-1$ delQuery.executeUpdate(); sess.remove(sess.getReference(ApplicationPO.class, app.getId())); } /** * Mark a PO as locked for a given session. * @param userSess Lock the PO for this session. * @param po The PO to be locked. * @param checkVersion check if the PO was modified in the db. * @return true if the lock attempt was successful * @throws PMDirtyVersionException if checkVersion is true and the version * of the PO differs from the version of the db instance of thos PO * @throws PMObjectDeletedException if the object was deleted */ public synchronized boolean lockPO(final EntityManager userSess, final IPersistentObject po, final boolean checkVersion) throws PMDirtyVersionException, PMObjectDeletedException { // check for disposed LockManager if (LockManager.isRunning() && m_application != null) { final DBRunnable checkForDirty = new DBRunnable() { public Result run(EntityManager sess) { Result result = Result.OK; try { if (checkVersion) { Query versionQuery = sess .createQuery("select obj.version from " //$NON-NLS-1$ + po.getClass().getSimpleName() + " as obj where obj.id = :poID"); //$NON-NLS-1$ versionQuery.setParameter("poID", po.getId()); //$NON-NLS-1$ Integer version = (Integer) versionQuery.getSingleResult(); if (!po.getVersion().equals(version)) { result = Result.OBJECT_DIRTY; } } else { Query countQuery = sess .createQuery("select count(obj.id) from " //$NON-NLS-1$ + po.getClass().getSimpleName() + " as obj where obj.id = :poID"); //$NON-NLS-1$ countQuery.setParameter("poID", po.getId()); //$NON-NLS-1$ Long count = (Long) countQuery.getSingleResult(); if (count == 0) { result = Result.OBJECT_DELETED; } } } catch (NoResultException nre) { result = Result.OBJECT_DELETED; } return result; } }; final Result runResult = runInSession(checkForDirty); if (runResult == Result.OBJECT_DELETED) { throw new PMObjectDeletedException(po, Messages.LockFailedDueToDeletedPO, MessageIDs.E_DELETED_OBJECT); } if (checkVersion && (runResult == Result.OBJECT_DIRTY)) { throw new PMDirtyVersionException(po, Messages.LockFailedDueToDbOutOfSync, MessageIDs.E_STALE_OBJECT); } return Result.OK == runInSession(new DBRunnable() { public Result run(EntityManager sess) { Result lockOK; Query lockQuery = sess .createQuery("select lock from DbLockPO as lock where lock.poId = :poID"); //$NON-NLS-1$ lockQuery.setParameter("poID", po.getId()); //$NON-NLS-1$ try { DbLockPO lock = (DbLockPO) lockQuery.getSingleResult(); lockOK = (lock.getApplication().equals(m_application) && lock.getSessionId().intValue() == System .identityHashCode(userSess)) ? Result.OK : Result.FAILED; } catch (NoResultException nre) { DbLockPO lock = new DbLockPO(m_application, userSess, po); sess.persist(lock); lockOK = Result.OK; } return lockOK; } }); } return false; } /** * tries to lock a list of persistent objects * @param sess * Session * @param objectsToLock * Set<IPersistentObject> * @param checkVersion * boolean * @throws PMDirtyVersionException * dirty version found * @throws PMObjectDeletedException * object was deleted * @throws PMAlreadyLockedException * object was deleted * @return boolean */ public synchronized boolean lockPOs(EntityManager sess, final Collection< ? extends IPersistentObject> objectsToLock, final boolean checkVersion) throws PMDirtyVersionException, PMObjectDeletedException, PMAlreadyLockedException { IPersistentObject failedPO = null; Result runResult = Result.OK; for (IPersistentObject po : objectsToLock) { boolean lockResult; try { lockResult = lockPO(sess, po, checkVersion); if (!lockResult) { failedPO = po; runResult = Result.FAILED; break; } } catch (PMDirtyVersionException e) { failedPO = po; runResult = Result.OBJECT_DIRTY; break; } catch (PMObjectDeletedException e) { failedPO = po; runResult = Result.OBJECT_DELETED; break; } } if (runResult != Result.OK) { handleLockProblems(failedPO, runResult); } return true; } /** * @param po * IPersistentObject * @param runResult * Result * @throws PMObjectDeletedException * error * @throws PMDirtyVersionException * error * @throws PMAlreadyLockedException * error */ private void handleLockProblems(final IPersistentObject po, final Result runResult) throws PMObjectDeletedException, PMDirtyVersionException, PMAlreadyLockedException { if (runResult == Result.FAILED) { String poName = po != null ? po.getName() : StringConstants.EMPTY; long poId = po != null ? po.getId() : -1; throw new PMAlreadyLockedException(po, "PO " + po + " (name=" + poName + "; id=" + poId + ") locked in db.", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ MessageIDs.E_OBJECT_IN_USE); } if (runResult == Result.OBJECT_DELETED) { throw new PMObjectDeletedException(po, Messages.LockFailedDueToDeletedDOT, MessageIDs.E_DELETED_OBJECT); } if (runResult == Result.OBJECT_DIRTY) { throw new PMDirtyVersionException(po, Messages.LockFailedDueToDbOutOfSync, MessageIDs.E_STALE_OBJECT); } } /** * Release the lock on this PO. * @param po The PO to be released. */ public synchronized void unlockPO(final IPersistentObject po) { runInSession(new DBRunnable() { public Result run(EntityManager sess) { Query removeLockQuery = sess.createQuery("delete from DbLockPO lock where lock.poId = :poId"); //$NON-NLS-1$ removeLockQuery.setParameter("poId", po.getId()); //$NON-NLS-1$ return (removeLockQuery.executeUpdate() > 0) ? Result.OK : Result.FAILED; } }); } /** * @return true if an instance of the LockManager is running */ public static boolean isRunning() { return instance != null; } /** * Release all locks for this Session. * @param s The session for which all locks (if any) shall be released. */ public synchronized void unlockPOs(final EntityManager s) { runInSession(new DBRunnable() { public Result run(EntityManager sess) { Query removeLockQuery = sess.createQuery("delete from DbLockPO lock where lock.sessionId = :sessId"); //$NON-NLS-1$ removeLockQuery.setParameter("sessId", System.identityHashCode(s)); //$NON-NLS-1$ return (removeLockQuery.executeUpdate() > 0) ? Result.OK : Result.FAILED; } }); } /** * This method is for installation support only. It will fail * in a normal runtime environment. * @param em Entity manager for db transaction * */ public static void initDbGuard(EntityManager em) { final DbGuardPO guard = new DbGuardPO(); guard.setId(DB_GUARD_ID); em.merge(guard); } }