diff --git a/li.strolch.agent/src/main/java/li/strolch/persistence/api/AbstractTransaction.java b/li.strolch.agent/src/main/java/li/strolch/persistence/api/AbstractTransaction.java index 1afee4c43..ef04ebe85 100644 --- a/li.strolch.agent/src/main/java/li/strolch/persistence/api/AbstractTransaction.java +++ b/li.strolch.agent/src/main/java/li/strolch/persistence/api/AbstractTransaction.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.Date; import java.util.HashSet; import java.util.List; +import java.util.ListIterator; import java.util.Set; import li.strolch.agent.api.AuditTrail; @@ -88,6 +89,7 @@ public abstract class AbstractTransaction implements StrolchTransaction { private TransactionResult txResult; private List commands; + private List flushedCommands; private Set lockedElements; private AuditingOrderMap orderMap; @@ -111,6 +113,7 @@ public abstract class AbstractTransaction implements StrolchTransaction { this.certificate = certificate; this.commands = new ArrayList<>(); + this.flushedCommands = new ArrayList<>(); this.lockedElements = new HashSet<>(); this.closeStrategy = TransactionCloseStrategy.ROLLBACK; this.txResult = new TransactionResult(getRealmName(), System.nanoTime(), new Date()); @@ -463,12 +466,24 @@ public abstract class AbstractTransaction implements StrolchTransaction { return getResourceMap().getBy(this, refP, assertExists); } + @Override + public void flush() { + try { + validateCommands(); + doCommands(); + writeChanges(this.txResult); + } catch (Exception e) { + this.closeStrategy = TransactionCloseStrategy.ROLLBACK; + + String msg = "Strolch Transaction for realm {0} failed due to {1}"; //$NON-NLS-1$ + msg = MessageFormat.format(msg, getRealmName(), e.getMessage()); + throw new StrolchTransactionException(msg, e); + } + } + @Override public void autoCloseableCommit() { long start = System.nanoTime(); - if (logger.isDebugEnabled()) { - logger.info(MessageFormat.format("Committing TX for realm {0}...", getRealmName())); //$NON-NLS-1$ - } try { this.txResult.setState(TransactionState.COMMITTING); @@ -485,6 +500,8 @@ public abstract class AbstractTransaction implements StrolchTransaction { handleCommit(start, auditTrailDuration, updateObserversDuration); + this.txResult.setState(TransactionState.COMMITTED); + } catch (Exception e) { this.txResult.setState(TransactionState.ROLLING_BACK); try { @@ -505,6 +522,8 @@ public abstract class AbstractTransaction implements StrolchTransaction { handleFailure(start, e); } + this.txResult.setState(TransactionState.FAILED); + String msg = "Strolch Transaction for realm {0} failed due to {1}"; //$NON-NLS-1$ msg = MessageFormat.format(msg, getRealmName(), e.getMessage()); throw new StrolchTransactionException(msg, e); @@ -519,11 +538,14 @@ public abstract class AbstractTransaction implements StrolchTransaction { long start = System.nanoTime(); logger.warn(MessageFormat.format("Rolling back TX for realm {0}...", getRealmName())); //$NON-NLS-1$ try { + this.txResult.setState(TransactionState.ROLLING_BACK); undoCommands(); rollback(this.txResult); handleRollback(start); + this.txResult.setState(TransactionState.ROLLED_BACK); } catch (Exception e) { handleFailure(start, e); + this.txResult.setState(TransactionState.FAILED); } finally { releaseElementLocks(); } @@ -759,8 +781,12 @@ public abstract class AbstractTransaction implements StrolchTransaction { * so chance of a runtime exception should be small */ private void doCommands() { - for (Command command : this.commands) { + ListIterator iter = this.commands.listIterator(); + while (iter.hasNext()) { + Command command = iter.next(); command.doCommand(); + this.flushedCommands.add(command); + iter.remove(); } } @@ -769,8 +795,11 @@ public abstract class AbstractTransaction implements StrolchTransaction { * performing the commands */ private void undoCommands() { - for (Command command : this.commands) { + ListIterator iter = this.flushedCommands.listIterator(this.flushedCommands.size()); + while (iter.hasPrevious()) { + Command command = iter.previous(); command.undo(); + iter.remove(); } } } diff --git a/li.strolch.agent/src/main/java/li/strolch/persistence/api/StrolchTransaction.java b/li.strolch.agent/src/main/java/li/strolch/persistence/api/StrolchTransaction.java index 33af1fe93..b0088363a 100644 --- a/li.strolch.agent/src/main/java/li/strolch/persistence/api/StrolchTransaction.java +++ b/li.strolch.agent/src/main/java/li/strolch/persistence/api/StrolchTransaction.java @@ -17,7 +17,6 @@ package li.strolch.persistence.api; import java.util.List; -import ch.eitchnet.privilege.model.Certificate; import li.strolch.agent.api.AuditTrail; import li.strolch.agent.api.OrderMap; import li.strolch.agent.api.ResourceMap; @@ -46,6 +45,7 @@ import li.strolch.model.query.OrderQuery; import li.strolch.model.query.ResourceQuery; import li.strolch.runtime.StrolchConstants; import li.strolch.service.api.Command; +import ch.eitchnet.privilege.model.Certificate; /** *

@@ -186,6 +186,17 @@ public interface StrolchTransaction extends AutoCloseable { */ public StrolchTransactionException fail(String exceptionMessage); + /** + *

+ * Performs all registered commands + *

+ * + *

+ * This method does not release any locks, nor does it notify any observers + *

+ */ + public void flush(); + /** * @return true if the transaction is still open, i.e. not being closed or rolling back, committing, etc. */ diff --git a/li.strolch.persistence.postgresql/src/main/java/li/strolch/persistence/postgresql/PostgresqlDao.java b/li.strolch.persistence.postgresql/src/main/java/li/strolch/persistence/postgresql/PostgresqlDao.java index 885847d4f..e033ba9a4 100644 --- a/li.strolch.persistence.postgresql/src/main/java/li/strolch/persistence/postgresql/PostgresqlDao.java +++ b/li.strolch.persistence.postgresql/src/main/java/li/strolch/persistence/postgresql/PostgresqlDao.java @@ -385,9 +385,11 @@ public abstract class PostgresqlDao implements Strolch } void commit(TransactionResult txResult) { + // even though we support rollback we can clear the commands here even if we performed them because the DB transaction will be rolled back for (DaoCommand command : this.commands) { command.doComand(txResult); } + this.commands.clear(); } void rollback() { diff --git a/li.strolch.service/src/test/java/li/strolch/service/FlushTxTest.java b/li.strolch.service/src/test/java/li/strolch/service/FlushTxTest.java new file mode 100644 index 000000000..dd81ada02 --- /dev/null +++ b/li.strolch.service/src/test/java/li/strolch/service/FlushTxTest.java @@ -0,0 +1,197 @@ +package li.strolch.service; + +import li.strolch.command.AddResourceCommand; +import li.strolch.command.RemoveResourceCommand; +import li.strolch.command.UpdateResourceCommand; +import li.strolch.model.ModelGenerator; +import li.strolch.model.Resource; +import li.strolch.persistence.api.StrolchTransaction; +import li.strolch.service.api.AbstractService; +import li.strolch.service.api.ServiceArgument; +import li.strolch.service.api.ServiceResult; +import li.strolch.service.test.AbstractRealmServiceTest; + +import org.junit.Test; + +import ch.eitchnet.utils.dbc.DBC; + +public class FlushTxTest extends AbstractRealmServiceTest { + + @Test + public void shouldFlushSuccessfully1() { + + runServiceInAllRealmTypes(FlushingCommandsService1.class, new ServiceArgument()); + } + + @Test + public void shouldFlushSuccessfully2() { + + runServiceInAllRealmTypes(FlushingCommandsService2.class, new ServiceArgument()); + } + + @Test + public void shouldRollbackSuccessfully() { + + runServiceInAllRealmTypes(RollbackAfterFlushCommandsService.class, new ServiceArgument()); + } + + public static class FlushingCommandsService1 extends AbstractService { + private static final long serialVersionUID = 1L; + + @Override + protected ServiceResult getResultInstance() { + return new ServiceResult(); + } + + @Override + protected ServiceResult internalDoService(ServiceArgument arg) throws Exception { + + String id = "flushSuccessfully"; + Resource resource = ModelGenerator.createResource(id, id, id); + + try (StrolchTransaction tx = openTx(arg.realm)) { + + DBC.PRE.assertNull("Did not expect resource with id " + id, tx.getResourceBy(id, id)); + + AddResourceCommand addResCmd = new AddResourceCommand(getContainer(), tx); + addResCmd.setResource(resource); + tx.addCommand(addResCmd); + tx.flush(); + DBC.PRE.assertNotNull("Expected resource with id " + id, tx.getResourceBy(id, id)); + + RemoveResourceCommand rmResCmd = new RemoveResourceCommand(getContainer(), tx); + rmResCmd.setResource(resource); + tx.addCommand(rmResCmd); + tx.flush(); + DBC.PRE.assertNull("Expect to remove resource with id " + id, tx.getResourceBy(id, id)); + + tx.commitOnClose(); + } + + // now make sure the new resource does not exist + try (StrolchTransaction tx = openTx(arg.realm)) { + + Resource res = tx.getResourceBy(id, id); + if (res != null) { + throw tx.fail("Did not expect resource with id " + id); + } + } + + return ServiceResult.success(); + } + } + + public static class FlushingCommandsService2 extends AbstractService { + private static final long serialVersionUID = 1L; + + @Override + protected ServiceResult getResultInstance() { + return new ServiceResult(); + } + + @Override + protected ServiceResult internalDoService(ServiceArgument arg) throws Exception { + + String id = "flushSuccessfully"; + Resource resource = ModelGenerator.createResource(id, id, id); + + try (StrolchTransaction tx = openTx(arg.realm)) { + + DBC.PRE.assertNull("Did not expect resource with id " + id, tx.getResourceBy(id, id)); + + AddResourceCommand addResCmd = new AddResourceCommand(getContainer(), tx); + addResCmd.setResource(resource); + tx.addCommand(addResCmd); + tx.flush(); + DBC.PRE.assertNotNull("Expected resource with id " + id, tx.getResourceBy(id, id)); + + Resource res = tx.getResourceBy(id, id); + + UpdateResourceCommand updateResCmd = new UpdateResourceCommand(getContainer(), tx); + updateResCmd.setResource(res); + tx.addCommand(updateResCmd); + + tx.commitOnClose(); + } + + // now make sure the new resource does exist + try (StrolchTransaction tx = openTx(arg.realm)) { + + Resource res = tx.getResourceBy(id, id); + if (res == null) { + throw tx.fail("Did not find expected resource with id " + id); + } + } + + return ServiceResult.success(); + } + } + + public static class RollbackAfterFlushCommandsService extends AbstractService { + private static final long serialVersionUID = 1L; + + @Override + protected ServiceResult getResultInstance() { + return new ServiceResult(); + } + + @Override + protected ServiceResult internalDoService(ServiceArgument arg) throws Exception { + + String id = "flushSuccessfully2"; + Resource resource = ModelGenerator.createResource(id, id, id); + + try (StrolchTransaction tx = openTx(arg.realm)) { + + DBC.PRE.assertNull("Did not expect resource with id " + id, tx.getResourceBy(id, id)); + + AddResourceCommand addResCmd = new AddResourceCommand(getContainer(), tx); + addResCmd.setResource(resource); + tx.addCommand(addResCmd); + tx.flush(); + DBC.PRE.assertNotNull("Expected resource with id " + id, tx.getResourceBy(id, id)); + + // now force a rollback + tx.rollbackOnClose(); + } + + // now make sure the new resource does not exist + try (StrolchTransaction tx = openTx(arg.realm)) { + + Resource res = tx.getResourceBy(id, id); + if (res != null) { + throw tx.fail("Did not expect resource with id after rolling back previous TX " + id); + } + } + + // now do it over, but use throw + try (StrolchTransaction tx = openTx(arg.realm)) { + + DBC.PRE.assertNull("Did not expect resource with id " + id, tx.getResourceBy(id, id)); + + AddResourceCommand addResCmd = new AddResourceCommand(getContainer(), tx); + addResCmd.setResource(resource); + tx.addCommand(addResCmd); + tx.flush(); + DBC.PRE.assertNotNull("Expected resource with id " + id, tx.getResourceBy(id, id)); + + // now force a rollback + throw tx.fail("Oh snap, something went wrong!"); + + } catch (Exception e) { + // expected + } + + // now make sure the new resource does not exist + try (StrolchTransaction tx = openTx(arg.realm)) { + + Resource res = tx.getResourceBy(id, id); + if (res != null) { + throw tx.fail("Did not expect resource with id after rolling back previous TX " + id); + } + } + + return ServiceResult.success(); + } + } +}