package de.skuzzle.polly.core.internal.persistence; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.EntityTransaction; import javax.persistence.NoResultException; import javax.persistence.Persistence; import javax.persistence.Query; import org.apache.log4j.Logger; import de.skuzzle.polly.core.util.MillisecondStopwatch; import de.skuzzle.polly.core.util.Stopwatch; import de.skuzzle.polly.sdk.AbstractDisposable; import de.skuzzle.polly.sdk.EntityConverter; import de.skuzzle.polly.sdk.PersistenceManagerV2; import de.skuzzle.polly.sdk.exceptions.DatabaseException; import de.skuzzle.polly.sdk.exceptions.DisposingException; import de.skuzzle.polly.tools.concurrent.ThreadFactoryBuilder; public class PersistenceManagerV2Impl extends AbstractDisposable implements PersistenceManagerV2 { private final static Logger logger = Logger.getLogger(PersistenceManagerV2Impl.class .getName()); private abstract class WriteImpl implements Write { private final EntityManager em; public WriteImpl(EntityManager em) { this.em = em; } @Override public <T> Write all(Iterable<T> list) { for (final T element : list) { em.persist(element); } return this; } @Override public <T> Write single(T obj) { em.persist(obj); return this; } @Override public <T> Write remove(T obj) { em.remove(obj); return this; } @Override public <T> Write removeAll(Iterable<T> elements) { for (final T element : elements) { em.remove(element); } return this; } @Override public Read read() { // return new unlocked read instance return new ReadImpl(this.em) { @Override public void close() { // do nothing } }; } } private abstract class ReadImpl implements Read { private final EntityManager em; public ReadImpl(EntityManager em) { this.em = em; } @Override public <T> T find(Class<T> type, Object key) { logger.trace("Looking up primary key " + key + " in " + type.getName()); final Stopwatch watch = new MillisecondStopwatch(); watch.start(); try { return em.find(type, key); } catch (Exception e) { logger.error("", e); throw e; } finally { long time = watch.stop(); logger.trace("Query time: " + time + "ms"); } } @Override public <T> T findSingle(Class<T> type, String query) { return this.findSingle(type, query, new Param()); } @Override @SuppressWarnings("unchecked") public <T> T findSingle(Class<T> type, String query, Param params) { logger.trace("Executing named query '" + query + "'. Parameters: " + params); final Stopwatch watch = new MillisecondStopwatch(); watch.start(); try { Query q = em.createNamedQuery(query); int i = 1; for (Object param : params.getParams()) { q.setParameter(i++, param); } return (T) q.getSingleResult(); } catch (NoResultException e) { return null; } catch (Exception e) { logger.error("", e); throw e; } finally { long time = watch.stop(); logger.trace("Query time: " + time + "ms"); } } @Override public <T> List<T> findList(Class<T> type, String query) { return this.findList(type, query, new Param()); } @Override @SuppressWarnings("unchecked") public <T> List<T> findList(Class<T> type, String query, Param params) { logger.trace("Executing named query '" + query + "'. Parameters: " + params); final Stopwatch watch = new MillisecondStopwatch(); watch.start(); try { Query q = em.createNamedQuery(query); int i = 1; for (Object param : params.getParams()) { q.setParameter(i++, param); } return q.getResultList(); } catch (Exception e) { logger.error("", e); throw e; } finally { long time = watch.stop(); logger.trace("Query time: " + time + "ms"); } } @Override public <T> List<T> findList(Class<T> type, String query, int limit) { return this.findList(type, query, limit, new Param()); } @Override @SuppressWarnings("unchecked") public <T> List<T> findList(Class<T> type, String query, int limit, Param params) { logger.trace("Executing named query '" + query + "'. Parameters: " + params + ", limit: " + limit); final Stopwatch watch = new MillisecondStopwatch(); watch.start(); try { Query q = em.createNamedQuery(query); q.setMaxResults(limit); int i = 1; for (Object param : params.getParams()) { q.setParameter(i++, param); } return q.getResultList(); } catch (Exception e) { logger.error("", e); throw e; } finally { long time = watch.stop(); logger.trace("Query time: " + time + "ms"); } } @Override public <T> List<T> findList(Class<T> type, String query, int first, int limit) { return this.findList(type, query, first, limit, new Param()); } @Override @SuppressWarnings("unchecked") public <T> List<T> findList(Class<T> type, String query, int first, int limit, Param params) { logger.trace("Executing named query '" + query + "'. Parameters: " + params + ", first: " + first + ", limit:" + limit); final Stopwatch watch = new MillisecondStopwatch(); watch.start(); try { final Query q = em.createNamedQuery(query); q.setFirstResult(first); q.setMaxResults(limit); int i = 1; for (Object param : params.getParams()) { q.setParameter(i++, param); } return q.getResultList(); } catch (Exception e) { logger.error("", e); throw e; } finally { long time = watch.stop(); logger.trace("Query time: " + time + "ms"); } } } private class SynchedRead implements Read { @Override public <T> T find(Class<T> cls, Object key) { try (final Read r = read()) { return r.find(cls, key); } } @Override public <T> List<T> findList(Class<T> type, String query) { try (final Read r = read()) { return r.findList(type, query); } } @Override public <T> List<T> findList(Class<T> type, String query, Param params) { try (final Read r = read()) { return r.findList(type, query, params); } } @Override public <T> List<T> findList(Class<T> type, String query, int limit) { try (final Read r = read()) { return r.findList(type, query, limit); } } @Override public <T> List<T> findList(Class<T> type, String query, int limit, Param params) { try (final Read r = read()) { return r.findList(type, query, limit, params); } } @Override public <T> List<T> findList(Class<T> type, String query, int first, int limit) { try (final Read r = read()) { return r.findList(type, query, first, limit); } } @Override public <T> List<T> findList(Class<T> type, String query, int first, int limit, Param params) { try (final Read r = read()) { return r.findList(type, query, first, limit, params); } } @Override public <T> T findSingle(Class<T> type, String query) { try (final Read r = read()) { return r.findSingle(type, query); } } @Override public <T> T findSingle(Class<T> type, String query, Param params) { try (final Read r = read()) { return r.findSingle(type, query, params); } } @Override public void close() { // Do nothing } } private abstract class ParallelWriteImpl implements Write { protected final Collection<Atomic> actions; public ParallelWriteImpl() { this.actions = new ArrayList<>(); } @Override public <T> Write all(final Iterable<T> list) { this.actions.add(new Atomic() { @Override public void perform(Write write) { write.all(list); } }); return this; } @Override public <T> Write single(final T obj) { this.actions.add(new Atomic() { @Override public void perform(Write write) { write.single(obj); } }); return this; } @Override public <T> Write remove(final T obj) { this.actions.add(new Atomic() { @Override public void perform(Write write) { write.remove(obj); } }); return this; } @Override public <T> Write removeAll(final Iterable<T> elements) { this.actions.add(new Atomic() { @Override public void perform(Write write) { write.all(elements); } }); return this; } @Override public Read read() { throw new UnsupportedOperationException(); } } private final static int LOCK_TIMEOUT = 30; // 30 seconds private EntityManagerFactory emf; private EntityManager em; private EntityTransaction activeTransaction; private final ReadWriteLock locker; private final ExecutorService executor; private final EntityList entities; private final EntityConverterManagerImpl entityConverter; private int enterCounter; public PersistenceManagerV2Impl() { this.locker = new ReentrantReadWriteLock(); this.executor = Executors.newSingleThreadExecutor( new ThreadFactoryBuilder("PERSISTENCE")); //$NON-NLS-1$ this.entities = new EntityList(); this.entityConverter = new EntityConverterManagerImpl(this); } EntityList getEntities() { return this.entities; } @Override public void registerEntity(Class<?> clazz) { logger.debug("Registering new entity: " + clazz.getName()); this.entities.add(clazz); } @Override public void registerEntityConverter(EntityConverter ec) { this.entityConverter.addConverter(ec); } void runAllEntityConverters() throws DatabaseException { this.entityConverter.convertAll(); } public void connect(String persistenceUnit) { logger.info("Connecting to persistence unit '" + persistenceUnit + "'..."); this.emf = Persistence.createEntityManagerFactory(persistenceUnit); this.em = this.emf.createEntityManager(); logger.info("Database connection established."); } /** * Notes that the current thread tried to obtain a write lock. * Returns <code>true</code> if this thread already holds the writelock * @return Whether the thread already held the writelock */ private boolean reenter() { return this.enterCounter++ > 0; } /** * Notes that the current thread released one write lock. Returns <code>true</code> * if the thread released all previously reentered write locks. * @return Whether the thread released all write locks it held. */ private boolean leave() { if (this.enterCounter == 0) { throw new IllegalStateException("thread did not enter"); //$NON-NLS-1$ } return --this.enterCounter == 0; } private boolean threadMayCommit() { return this.enterCounter == 1; } /** * Starts a new transaction which can be used to add, modify or delete entities * within the database. In order to do this, a WriteLock is acquired so that no other * thread can access the database while this transaction is active. */ private void startTransaction() throws DatabaseException { logger.trace("Acquiring write lock..."); try { if (this.locker.writeLock().tryLock(LOCK_TIMEOUT, TimeUnit.SECONDS)) { try { if (!this.reenter()) { logger.trace("Got write lock."); logger.debug("Starting transaction..."); this.activeTransaction = this.em.getTransaction(); this.activeTransaction.begin(); logger.debug("Transaction started."); } else { logger.warn("Thread is reentering! Reusing current transaction"); } } catch (Exception e) { logger.error("Critical error while starting transaction", e); if (this.activeTransaction != null && this.activeTransaction.isActive()) { logger.info("Transaction is active, trying to close it"); try { this.activeTransaction.rollback(); } catch (Exception e1) { logger.fatal("Error while closing active transaction", e1); } } this.enterCounter = 0; this.locker.writeLock().unlock(); throw new DatabaseException("Serious internal database error. " + "Polly should be restarted in order to regain proper operational " + "state"); } } else { logger.error("Could not obtain write lock within reasonable time"); throw new DatabaseException("Timeout while waiting for write access"); } } catch (InterruptedException e) { logger.error("Thread interrupted while waiting for database write lock"); throw new DatabaseException(e); } } private void commitTransaction() throws DatabaseException { logger.debug("Committing transaction..."); final EntityTransaction tx = this.activeTransaction; try { if (tx == null) { throw new DatabaseException("No transaction active."); } if (this.threadMayCommit()) { tx.commit(); logger.debug("Transaction finished successful"); } else { logger.trace("Postponing commit until all write attempts" + " of this thread finish"); } } catch (Exception e) { logger.error("Committing transaction failed.", e); if (tx != null && tx.isActive() && this.threadMayCommit()) { try { logger.debug("Trying to rollback transaction."); tx.rollback(); logger.debug("Rollback successful."); } catch (Exception e1) { logger.fatal("Rollback failed!", e1); } } if (e instanceof DatabaseException) { throw e; } else { throw new DatabaseException("Transaction failed", e); } } finally { logger.trace("Writelock released"); this.leave(); logger.trace(this.enterCounter + " nested write calls left"); locker.writeLock().unlock(); } } @Override public void refresh(Object obj) { this.em.refresh(obj); } @Override public Read read() { logger.trace("Acquiring read lock..."); final Stopwatch watch = new MillisecondStopwatch(); watch.start(); this.locker.readLock().lock(); logger.trace("Got read lock."); return new ReadImpl(this.em) { @Override public void close() { logger.trace("Readlock released"); locker.readLock().unlock(); long time = watch.stop(); logger.trace("Read transaction time: " + time + "ms"); } }; } @Override public void detachAll(Collection<? extends Object> entities) { for (final Object entity : entities) { this.em.detach(entity); } } @Override public Read atomic() { return new SynchedRead(); } @Override public Write write() throws DatabaseException { final Stopwatch watch = new MillisecondStopwatch(); watch.start(); this.startTransaction(); return new WriteImpl(this.em) { @Override public void close() throws DatabaseException { try { commitTransaction(); } finally { long time = watch.stop(); logger.trace("Write transaction time: " + time + "ms"); } } }; } @Override public void writeAtomic(Atomic a) throws DatabaseException { try (final Write w = this.write()) { a.perform(w); } } @Override public void writeAtomicParallel(Atomic a) { this.writeAtomicParallel(a, new TransactionCallback() { @Override public void success() {} @Override public void fail(DatabaseException e) { logger.error("", e); } }); } @Override public void writeAtomicParallel(final Atomic a, final TransactionCallback cb) { this.executor.submit(new Runnable() { @Override public void run() { try (final Write w = write()) { a.perform(w); cb.success(); } catch (DatabaseException e) { cb.fail(e); } } }); } @Override public Write writeParallel() { return this.writeParallel(new TransactionCallback() { @Override public void success() {} @Override public void fail(DatabaseException e) { logger.error("", e); } }); } @Override public Write write(final TransactionCallback cb) { return new ParallelWriteImpl() { @Override public void close() { try (final Write w = write()) { for (final Atomic wr : actions) { wr.perform(w); } } catch (DatabaseException e) { cb.fail(e); } cb.success(); } }; } @Override public Write writeParallel(final TransactionCallback cb) { return new ParallelWriteImpl() { @Override public void close() { executor.submit(new Runnable() { @Override public void run() { try (final Write w = write()) { for (final Atomic wr : actions) { wr.perform(w); } } catch (DatabaseException e) { cb.fail(e); } cb.success(); } }); } }; } @Override protected void actualDispose() throws DisposingException { logger.debug("Shutting down database..."); logger.trace("Shutting down entity manager..."); try { logger.trace("Waiting for all operations to end..."); this.locker.writeLock().lock(); if (this.em.isOpen()) { try { logger.trace("Sending SHUTDOWN command."); this.startTransaction(); final Query q = this.em.createNativeQuery("SHUTDOWN"); q.executeUpdate(); this.commitTransaction(); } catch (Exception e) { logger.error("SHUTDOWN command failed."); } this.em.close(); this.em = null; } logger.trace("Shutting down entity manager factory..."); if (this.emf.isOpen()) { this.emf.close(); this.emf = null; } logger.debug("Database connection closed."); } catch (Exception e) { logger.fatal("Error while shutting down database."); } finally { this.locker.writeLock().unlock(); } } }