CAUSEWAY-3799: in InteractionServiceDefault#preInteractionClosed, do make sure we close down the interaction

... even if the transaction was completed under our feet
diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java
index b7536c6..47a7f38 100644
--- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java
+++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java
@@ -237,6 +237,10 @@ public void closeInteractionLayers() {
                 interactionLayerStack.get().size(),
                 _Probe.currentThreadId());
 
+        //
+        // TODO: Be aware that this method could theoretically throw an exception, if the flush fails in
+        //  preInteractionClosed.  Elsewhere where we make this call it's not clear that this is correct
+        //
         closeInteractionLayerStackDownToStackSize(0);
     }
 
@@ -266,6 +270,12 @@ public <R> R call(
         try {
             return callInternal(callable);
         } finally {
+            //
+            // TODO: this method could theoretically throw an exception, if the flush fails in
+            //  preInteractionClosed.  It]m uncertain what to do here.  The callable executed in the try block
+            //  may be returning a Try, which could encode a failure that way.  Having this method also possibly
+            //  throw an exception seems incorrect.
+            //
             closeInteractionLayerStackDownToStackSize(stackSizeWhenEntering);
         }
     }
@@ -281,6 +291,12 @@ public void run(
         try {
             runInternal(runnable);
         } finally {
+            //
+            // TODO: this method could theoretically throw an exception, if the flush fails in
+            //  preInteractionClosed.  It]m uncertain what to do here.  The callable executed in the try block
+            //  may be returning a Try, which could encode a failure that way.  Having this method also possibly
+            //  throw an exception seems incorrect.
+            //
             closeInteractionLayerStackDownToStackSize(stackSizeWhenEntering);
         }
     }
@@ -363,43 +379,57 @@ private boolean isAtTopLevel() {
     @SneakyThrows
     private void preInteractionClosed(final CausewayInteraction interaction) {
 
+        Throwable flushException = null;
+
         // a bit of a hacky guard
         //
-        // the suspicion is that if a background command execution encounters a deadlock then (in
-        // CommandExecutorServiceDefault) the top-level xactn will end up being rolled back.
         //
-        // we've seen additional changes being made in a new/implicit (?) xactn which furthermore are never committed;
-        // we end up with a connection back in Hikari's conn pool with open locks.  If those changes are as a
-        // result of the transaction having completed, then this original method would have resulted in a command
-        // being persisted
+        // we check if the transaction is already completed (rolled back/committed).  This isn't meant to be the case,
+        // but the suspicion is that if a background command execution encounters a deadlock then (in
+        // CommandExecutorServiceDefault) then it might be resulting in top-level xactn will end up being rolled back.
+        //
+        // The relevant code is in TransactionService#callWithinCurrentTransactionElseCreateNew(...), used by
+        // CommandExecutorServiceDefault but also used quite heavily elsewhere.  In normal circumstances I suspect
+        // everything works out fine, but if there's a deadlock then perhaps we get this different flow
+        //
+        // the consequences of an incorrect design can be SEVERE.  In previous versions of the code base we've seen
+        // additional changes being made in a new/implicit (?) xactn which furthermore are never committed; we end up
+        // with a connection back in Hikari's conn pool with open locks, blocking the entire system as those locks are
+        // on CommandLogEntry.  Or, another problem found was seemingly polluting the threadLocals, resulting in an
+        // nio-http-exec thread always failing with an error of: "No JDO PersistenceManager bound to thread, and
+        // configuration does not allow creation of non-transactional one here" (see PersistenceManagerFactoryUtils).
+        // Both of these errors require the app to be restarted.
+        //
+        // To be clear, it's not yet certain that we've found the underlying issue, but if the following warning is
+        // detected in the logs, then that might be a good thing.
         //
         if(transactionServiceSpring.currentTransactionState().isComplete()) {
-            // hmm... someone went and completed (rolled back/committed) our transaction from us
-            //
+
+            // something completed the transaction under our feet; was it a deadlock perhaps?
             log.warn("preInteractionClosed: skipping as a precaution because current transaction has been completed already");
-            return;
-        }
 
-        Throwable flushException = null;
-        val mustAbort = transactionServiceSpring.currentTransactionState().mustAbort();
-        if(!mustAbort) {
-            try {
-                transactionServiceSpring.flushTransaction();
-                // publish only when flush was successful
-                completeAndPublishCurrentCommand();
-            } catch (Throwable e) {
-                //[CAUSEWAY-3262] if flush fails rethrow later, when interaction was closed ...
-                flushException = e;
-                transactionServiceSpring.requestRollback(interaction);
+        } else {
+            val mustAbort = transactionServiceSpring.currentTransactionState().mustAbort();
+            if(!mustAbort) {
+                try {
+                    transactionServiceSpring.flushTransaction();
+                    // publish only when flush was successful
+                    completeAndPublishCurrentCommand();
+                } catch (Throwable e) {
+                    //[CAUSEWAY-3262] if flush fails rethrow later, when interaction was closed ...
+                    flushException = e;
+                    transactionServiceSpring.requestRollback(interaction);
+                }
             }
+            // the net effect of this is to call either txManager.rollback(txStatus) or txManager.commit(txStatus) depending upon
+            // whether txStatus.setRollbackOnly(...) was ever called.
+            // anything has called setRollbackOnly so far.
+            transactionServiceSpring.onClose(interaction);
         }
 
-        // will either rollback or commit the actual transaction depending upon whether
-        // anything has called setRollbackOnly so far.
-        transactionServiceSpring.onClose(interaction);
-
-        interactionScopeLifecycleHandler.onTopLevelInteractionPreDestroy(); // cleanup the InteractionScope (Spring scope)
-        interactionScopeLifecycleHandler.onTopLevelInteractionClosed(); // cleanup the InteractionScope (Spring scope)
+        // cleanup the InteractionScope (Spring scope)
+        interactionScopeLifecycleHandler.onTopLevelInteractionPreDestroy();
+        interactionScopeLifecycleHandler.onTopLevelInteractionClosed();
         interaction.close(); // do this last
 
         if(flushException!=null) {
@@ -419,17 +449,23 @@ private void closeInteractionLayerStackDownToStackSize(final int downToStackSize
                 _Probe.currentThreadId());
 
         val stack = interactionLayerStack.get();
-        while(stack.size()>downToStackSize) {
-        	if(isAtTopLevel()) {
-        		// keep the stack unmodified yet, to allow for callbacks to properly operate
-        		preInteractionClosed(_Casts.uncheckedCast(stack.peek().getInteraction()));
-        	}
-        	_Xray.closeInteractionLayer(stack);
-            stack.pop();
-        }
-        if(downToStackSize == 0) {
-            // cleanup thread-local
-            interactionLayerStack.remove();
+        try {
+            while(stack.size()>downToStackSize) {
+                if(isAtTopLevel()) {
+                    // keep the stack unmodified yet, to allow for callbacks to properly operate
+
+                    preInteractionClosed(_Casts.uncheckedCast(stack.peek().getInteraction()));
+                }
+                _Xray.closeInteractionLayer(stack);
+                stack.pop();
+            }
+        } finally {
+            // preInteractionClosed above could conceivably throw an exception, so we'll tidy up our threadlocal
+            // here to ensure everything is cleaned up
+            if(downToStackSize == 0) {
+                // cleanup thread-local
+                interactionLayerStack.remove();
+            }
         }
     }