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 ef04ebe85..5e844e893 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 @@ -115,34 +115,26 @@ public abstract class AbstractTransaction implements StrolchTransaction { this.commands = new ArrayList<>(); this.flushedCommands = new ArrayList<>(); this.lockedElements = new HashSet<>(); - this.closeStrategy = TransactionCloseStrategy.ROLLBACK; + this.closeStrategy = TransactionCloseStrategy.DO_NOTHING; this.txResult = new TransactionResult(getRealmName(), System.nanoTime(), new Date()); this.txResult.setState(TransactionState.OPEN); } @Override - public boolean isOpen() { - return this.txResult.getState() == TransactionState.OPEN; + public TransactionState getState() { + return this.txResult.getState(); } - @Override public boolean isRollingBack() { - return this.txResult.getState() == TransactionState.ROLLING_BACK; + return this.txResult.getState().isRollingBack(); } - @Override public boolean isCommitting() { - return this.txResult.getState() == TransactionState.COMMITTING; + return this.txResult.getState().isCommitting(); } - @Override - public boolean isCommitted() { - return this.txResult.getState() == TransactionState.COMMITTED; - } - - @Override - public boolean isRolledBack() { - return this.txResult.getState() == TransactionState.ROLLED_BACK; + public boolean isClosing() { + return this.txResult.getState().isClosing(); } @Override @@ -159,6 +151,11 @@ public abstract class AbstractTransaction implements StrolchTransaction { return certificate; } + @Override + public TransactionCloseStrategy getCloseStrategy() { + return this.closeStrategy; + } + private void setCloseStrategy(TransactionCloseStrategy closeStrategy) { this.closeStrategy = closeStrategy; } @@ -168,6 +165,11 @@ public abstract class AbstractTransaction implements StrolchTransaction { this.closeStrategy.close(this); } + @Override + public void doNothingOnClose() { + setCloseStrategy(TransactionCloseStrategy.DO_NOTHING); + } + @Override public void commitOnClose() { setCloseStrategy(TransactionCloseStrategy.COMMIT); @@ -551,19 +553,80 @@ public abstract class AbstractTransaction implements StrolchTransaction { } } + @Override + public void autoCloseableDoNothing() throws StrolchTransactionException { + long start = System.nanoTime(); + try { + this.txResult.setState(TransactionState.CLOSING); + + if (!this.commands.isEmpty()) { + String msg = "Current close strategy {0} is readonly and thus does not support doing commands!"; + msg = MessageFormat.format(msg, this.closeStrategy); + throw fail(msg); + } + + long auditTrailDuration = writeAuditTrail(); + + // rollback and release any resources + rollback(this.txResult); + + handleDoNothing(start, auditTrailDuration); + this.txResult.setState(TransactionState.CLOSED); + } catch (Exception e) { + handleFailure(start, e); + this.txResult.setState(TransactionState.FAILED); + } finally { + releaseElementLocks(); + } + } + protected abstract void writeChanges(TransactionResult txResult) throws Exception; protected abstract void rollback(TransactionResult txResult) throws Exception; protected abstract void commit() throws Exception; + private void handleDoNothing(long start, long auditTrailDuration) { + + long end = System.nanoTime(); + long txDuration = end - this.txResult.getStartNanos(); + long closeDuration = end - start; + + this.txResult.setTxDuration(txDuration); + this.txResult.setCloseDuration(closeDuration); + + StringBuilder sb = new StringBuilder(); + sb.append("TX user="); + sb.append(this.certificate.getUsername()); + + sb.append(", realm="); //$NON-NLS-1$ + sb.append(getRealmName()); + + sb.append(", took="); //$NON-NLS-1$ + sb.append(StringHelper.formatNanoDuration(txDuration)); + + sb.append(", action="); + sb.append(this.action); + + if (closeDuration >= 100000000L) { + sb.append(", close="); //$NON-NLS-1$ + sb.append(StringHelper.formatNanoDuration(closeDuration)); + } + + if (isAuditTrailEnabled() && auditTrailDuration >= 100000000L) { + sb.append(", auditTrail="); //$NON-NLS-1$ + sb.append(StringHelper.formatNanoDuration(auditTrailDuration)); + } + + logger.info(sb.toString()); + } + private void handleCommit(long start, long auditTrailDuration, long observerUpdateDuration) { long end = System.nanoTime(); long txDuration = end - this.txResult.getStartNanos(); long closeDuration = end - start; - this.txResult.setState(TransactionState.COMMITTED); this.txResult.setTxDuration(txDuration); this.txResult.setCloseDuration(closeDuration); @@ -603,7 +666,6 @@ public abstract class AbstractTransaction implements StrolchTransaction { long txDuration = end - this.txResult.getStartNanos(); long closeDuration = end - start; - this.txResult.setState(TransactionState.ROLLED_BACK); this.txResult.setTxDuration(txDuration); this.txResult.setCloseDuration(closeDuration); 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 b0088363a..8a93162fe 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 @@ -132,6 +132,20 @@ public interface StrolchTransaction extends AutoCloseable { */ public PersistenceHandler getPersistenceHandler(); + /** + * Returns the currently set {@link TransactionCloseStrategy} + * + * @return the currently set {@link TransactionCloseStrategy} + */ + public TransactionCloseStrategy getCloseStrategy(); + + /** + * DO NOT CALL THIS METHOD. If the currently set close strategy is {@link TransactionCloseStrategy#DO_NOTHING}, then + * when the transaction is closed, this method is called no changes to the model is performed but locks on objects + * are released and any other resources are released + */ + public void autoCloseableDoNothing() throws StrolchTransactionException; + /** * DO NOT CALL THIS METHOD. If the currently set close strategy is {@link TransactionCloseStrategy#COMMIT}, then * when the transaction is closed, this method is called and all registered {@link Command} are performed, locks on @@ -166,6 +180,11 @@ public interface StrolchTransaction extends AutoCloseable { @Override public void close() throws StrolchTransactionException; + /** + * Sets the {@link TransactionCloseStrategy} to {@link TransactionCloseStrategy#DO_NOTHING} + */ + public void doNothingOnClose(); + /** * Sets the {@link TransactionCloseStrategy} to {@link TransactionCloseStrategy#COMMIT} */ @@ -198,29 +217,26 @@ public interface StrolchTransaction extends AutoCloseable { public void flush(); /** - * @return true if the transaction is still open, i.e. not being closed or rolling back, committing, etc. + * @return the current state of the transaction + * + * @see TransactionState */ - public boolean isOpen(); + public TransactionState getState(); /** - * @return true if the transaction is in the process of rolling back all changes + * @return if the current state of the {@link StrolchTransaction} is {@link TransactionState#ROLLING_BACK} */ public boolean isRollingBack(); /** - * @return true if the transaction is in the process of committing the changes + * @return if the current state of the {@link StrolchTransaction} is {@link TransactionState#COMMITTING} */ public boolean isCommitting(); /** - * @return if the transaction has committed all changes + * @return if the current state of the {@link StrolchTransaction} is {@link TransactionState#CLOSING} */ - public boolean isCommitted(); - - /** - * @return if the transaction has rolled back all changes - */ - public boolean isRolledBack(); + public boolean isClosing(); /** * If the given argument is true, then no observer updates are performed diff --git a/li.strolch.agent/src/main/java/li/strolch/persistence/api/TransactionCloseStrategy.java b/li.strolch.agent/src/main/java/li/strolch/persistence/api/TransactionCloseStrategy.java index a1c08ac7f..9ed9ae307 100644 --- a/li.strolch.agent/src/main/java/li/strolch/persistence/api/TransactionCloseStrategy.java +++ b/li.strolch.agent/src/main/java/li/strolch/persistence/api/TransactionCloseStrategy.java @@ -17,16 +17,68 @@ package li.strolch.persistence.api; import li.strolch.exception.StrolchException; +/** + * Defines what happens when a {@link StrolchTransaction} is closed. Strolch transactions are auto closeable which means + * when used in a try-with-resource block, then they are automatically closed. Depending on the use case, these + * transactions might write something to the DB, or only read the model, or even have to roll back, this is controlled + * by using these enums. + * + * @author Robert von Burg + */ public enum TransactionCloseStrategy { + /** + *

+ * This is for read only transactions. If this strategy is used, then no changes will be written to the model. Use + * {@link StrolchTransaction#doNothingOnClose()} to simply close the transaction, releasing any resources + *

+ * + *

+ * Note: When using this strategy, then the transaction will throw exceptions if you try to add commands to + * the transaction. + *

+ */ + DO_NOTHING() { + + @Override + public boolean isReadonly() { + return true; + } + + @Override + public void close(StrolchTransaction tx) throws StrolchException { + tx.autoCloseableDoNothing(); + } + }, + + /** + * The main close strategy type where changes are written to the model. Use + * {@link StrolchTransaction#commitOnClose()} to commit any changes i.e. commands added to the transaction + */ COMMIT() { + + @Override + public boolean isReadonly() { + return false; + } + @Override public void close(StrolchTransaction tx) throws StrolchException { tx.autoCloseableCommit(); } }, + /** + * In exceptional cases one might not want any changes to be written to the model, thus calling + * {@link StrolchTransaction#rollbackOnClose()} will have the transaction roll back all changes + */ ROLLBACK() { + + @Override + public boolean isReadonly() { + return true; + } + @Override public void close(StrolchTransaction tx) throws StrolchException { tx.autoCloseableRollback(); @@ -34,4 +86,6 @@ public enum TransactionCloseStrategy { }; public abstract void close(StrolchTransaction tx) throws StrolchException; + + public abstract boolean isReadonly(); } \ No newline at end of file diff --git a/li.strolch.agent/src/main/java/li/strolch/persistence/api/TransactionState.java b/li.strolch.agent/src/main/java/li/strolch/persistence/api/TransactionState.java index 6405bfe4d..d0dab5b0a 100644 --- a/li.strolch.agent/src/main/java/li/strolch/persistence/api/TransactionState.java +++ b/li.strolch.agent/src/main/java/li/strolch/persistence/api/TransactionState.java @@ -15,11 +15,39 @@ */ package li.strolch.persistence.api; +/** + * The different states of a {@link StrolchTransaction} + * + * @author Robert von Burg + */ public enum TransactionState { OPEN, // + CLOSING, // COMMITTING, // ROLLING_BACK, // + CLOSED, // COMMITTED, // ROLLED_BACK, FAILED; + + /** + * @return true if this is {@link #ROLLING_BACK} + */ + public boolean isRollingBack() { + return this == ROLLING_BACK; + } + + /** + * @return true if this is {@link #COMMITTING} + */ + public boolean isCommitting() { + return this == COMMITTING; + } + + /** + * @return true if this is {@link #CLOSING} + */ + public boolean isClosing() { + return this == CLOSING; + } } diff --git a/li.strolch.service/src/test/java/li/strolch/service/TxTest.java b/li.strolch.service/src/test/java/li/strolch/service/TxTest.java new file mode 100644 index 000000000..8691a556b --- /dev/null +++ b/li.strolch.service/src/test/java/li/strolch/service/TxTest.java @@ -0,0 +1,177 @@ +package li.strolch.service; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import li.strolch.command.AddResourceCommand; +import li.strolch.model.ModelGenerator; +import li.strolch.model.Order; +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 TxTest extends AbstractRealmServiceTest { + + @Test + public void shouldCommit() { + + runServiceInAllRealmTypes(CommitService.class, new ServiceArgument()); + } + + @Test + public void shouldRollback() { + + runServiceInAllRealmTypes(RollbackService.class, new ServiceArgument()); + } + + @Test + public void shouldDoNothing() { + + runServiceInAllRealmTypes(ReadonlyService.class, new ServiceArgument()); + } + + @Test + public void shouldNotAllowCommandsOnDoNothibg() { + + runServiceInAllRealmTypes(ReadonlyFailService.class, new ServiceArgument()); + } + + public static class CommitService 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)); + + tx.commitOnClose(); + } + + // now make sure the new resource exists + 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 RollbackService 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)); + + 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 find expected resource with id " + id); + } + } + + return ServiceResult.success(); + } + } + + public static class ReadonlyService extends AbstractService { + private static final long serialVersionUID = 1L; + + @Override + protected ServiceResult getResultInstance() { + return new ServiceResult(); + } + + @Override + protected ServiceResult internalDoService(ServiceArgument arg) throws Exception { + + try (StrolchTransaction tx = openTx(arg.realm)) { + Resource yellowBall = tx.getResourceBy("Ball", "yellow"); + assertNotNull("Expected to find the yellow ball", yellowBall); + + Order myCarOrder = tx.getOrderBy("ProductionOrder", "myCarOrder"); + assertNotNull("Expected to find the my car order", myCarOrder); + } + + return ServiceResult.success(); + } + } + + public static class ReadonlyFailService 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); + + boolean txFailed = false; + try (StrolchTransaction tx = openTx(arg.realm)) { + AddResourceCommand addResCmd = new AddResourceCommand(getContainer(), tx); + addResCmd.setResource(resource); + tx.addCommand(addResCmd); + } catch (Exception e) { + // expected + txFailed = true; + } + + assertTrue("TX should have failed!", txFailed); + + return ServiceResult.success(); + } + } +}