You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by ah...@apache.org on 2020/10/05 06:15:21 UTC

[isis] 01/01: ISIS-2441: work on ChangedObjects tests (wip)

This is an automated email from the ASF dual-hosted git repository.

ahuber pushed a commit to branch 2441-auditing
in repository https://gitbox.apache.org/repos/asf/isis.git

commit 4d06bf35e462c8cccb34878b448f7a2b723ebf65
Author: Andi Huber <ah...@apache.org>
AuthorDate: Mon Oct 5 08:15:01 2020 +0200

    ISIS-2441: work on ChangedObjects tests (wip)
---
 .../applib/services/TransactionScopeListener.java  |   7 +-
 .../isis/applib/services/iactn/Interaction.java    |   2 +-
 .../applib/services/iactn/InteractionContext.java  |  11 +-
 .../transaction/AuditerDispatchService.java        |   2 +-
 .../transaction/ChangedObjectsService.java         | 218 ++++++++++-----------
 .../transaction/HasEnlistedForAuditing.java        |  26 +--
 .../transaction/HasEnlistedForMetrics.java         |  24 +--
 .../transaction/HasEnlistedForPublishing.java      |  31 ++-
 .../publish/PublisherDispatchServiceDefault.java   |  14 +-
 .../datanucleus5/metrics/MetricsServiceForJdo.java |   6 +-
 .../persistence/IsisTransactionJdo.java            |  11 +-
 .../commons/InteractionBoundaryProbe.java          |   6 +-
 .../entitychangemetrics/ChangedObjectsTest.java    |  21 +-
 13 files changed, 173 insertions(+), 206 deletions(-)

diff --git a/api/applib/src/main/java/org/apache/isis/applib/services/TransactionScopeListener.java b/api/applib/src/main/java/org/apache/isis/applib/services/TransactionScopeListener.java
index 028bf40..85364bb 100644
--- a/api/applib/src/main/java/org/apache/isis/applib/services/TransactionScopeListener.java
+++ b/api/applib/src/main/java/org/apache/isis/applib/services/TransactionScopeListener.java
@@ -36,7 +36,12 @@ public interface TransactionScopeListener {
     default void onTransactionStarted() {
         // default: do nothing
     }
+
+    /** triggered during the pre-commit phase in a transaction*/
+    default void onTransactionEnding() {
+        // default: do nothing
+    }
+    
     
-    void onTransactionEnded();
 }
 // end::refguide[]
diff --git a/api/applib/src/main/java/org/apache/isis/applib/services/iactn/Interaction.java b/api/applib/src/main/java/org/apache/isis/applib/services/iactn/Interaction.java
index 3e2de34..6a0fc7e 100644
--- a/api/applib/src/main/java/org/apache/isis/applib/services/iactn/Interaction.java
+++ b/api/applib/src/main/java/org/apache/isis/applib/services/iactn/Interaction.java
@@ -87,7 +87,7 @@ public class Interaction implements HasUniqueId {
     }
 
     @Getter
-    private Command command;                        // <.>
+    private final Command command;                  // <.>
 
     @Override
     public UUID getUniqueId() {                     // <.>
diff --git a/api/applib/src/main/java/org/apache/isis/applib/services/iactn/InteractionContext.java b/api/applib/src/main/java/org/apache/isis/applib/services/iactn/InteractionContext.java
index b6579ea..157e265 100644
--- a/api/applib/src/main/java/org/apache/isis/applib/services/iactn/InteractionContext.java
+++ b/api/applib/src/main/java/org/apache/isis/applib/services/iactn/InteractionContext.java
@@ -31,14 +31,11 @@ import org.apache.isis.applib.annotation.DomainService;
 import org.apache.isis.applib.annotation.IsisInteractionScope;
 import org.apache.isis.applib.annotation.OrderPrecedence;
 import org.apache.isis.applib.services.TransactionScopeListener;
-import org.apache.isis.applib.services.command.Command;
-import org.apache.isis.applib.services.inject.ServiceInjector;
 import org.apache.isis.applib.services.metrics.MetricsService;
 
 import lombok.Getter;
 import lombok.RequiredArgsConstructor;
 import lombok.val;
-import lombok.extern.log4j.Log4j2;
 
 /**
  * This service (API and implementation) provides access to context information about any {@link Interaction}.
@@ -55,7 +52,7 @@ import lombok.extern.log4j.Log4j2;
 @Qualifier("Default")
 @IsisInteractionScope
 @RequiredArgsConstructor(onConstructor_ = {@Inject})
-@Log4j2
+//@Log4j2
 public class InteractionContext implements TransactionScopeListener, DisposableBean {
 
     // end::refguide[]
@@ -79,11 +76,11 @@ public class InteractionContext implements TransactionScopeListener, DisposableB
 
 
     @Override
-    public void onTransactionEnded() {
+    public void onTransactionEnding() {
         val command = getInteraction().getCommand();
         command.updater().setSystemStateChanged(
-                command.isSystemStateChanged() ||
-                        metricsService.numberObjectsDirtied() > 0);
+                command.isSystemStateChanged() 
+                || metricsService.numberObjectsDirtied() > 0);
     }
 
     @Override
diff --git a/core/runtime/src/main/java/org/apache/isis/core/runtime/persistence/transaction/AuditerDispatchService.java b/core/runtime/src/main/java/org/apache/isis/core/runtime/persistence/transaction/AuditerDispatchService.java
index a15f934..da44da5 100644
--- a/core/runtime/src/main/java/org/apache/isis/core/runtime/persistence/transaction/AuditerDispatchService.java
+++ b/core/runtime/src/main/java/org/apache/isis/core/runtime/persistence/transaction/AuditerDispatchService.java
@@ -53,7 +53,7 @@ import lombok.val;
 public class AuditerDispatchService {
     
     @Inject private List<AuditerService> auditerServices;
-    @Inject private javax.inject.Provider<ChangedObjectsService> changedObjectsProvider;
+    @Inject private javax.inject.Provider<HasEnlistedForAuditing> changedObjectsProvider;
     @Inject private UserService userService;
     @Inject private ClockService clockService;
     @Inject private TransactionService transactionService;
diff --git a/core/runtime/src/main/java/org/apache/isis/core/runtime/persistence/transaction/ChangedObjectsService.java b/core/runtime/src/main/java/org/apache/isis/core/runtime/persistence/transaction/ChangedObjectsService.java
index ee657bc..212f125 100644
--- a/core/runtime/src/main/java/org/apache/isis/core/runtime/persistence/transaction/ChangedObjectsService.java
+++ b/core/runtime/src/main/java/org/apache/isis/core/runtime/persistence/transaction/ChangedObjectsService.java
@@ -21,7 +21,6 @@ package org.apache.isis.core.runtime.persistence.transaction;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.Function;
-import java.util.stream.Stream;
 
 import javax.inject.Named;
 
@@ -30,18 +29,22 @@ import org.springframework.context.annotation.Primary;
 import org.springframework.core.annotation.Order;
 import org.springframework.stereotype.Service;
 
+import org.apache.isis.applib.annotation.IsisInteractionScope;
 import org.apache.isis.applib.annotation.OrderPrecedence;
 import org.apache.isis.applib.annotation.PublishingChangeKind;
 import org.apache.isis.applib.services.HasUniqueId;
 import org.apache.isis.applib.services.TransactionScopeListener;
+import org.apache.isis.commons.internal.base._Lazy;
 import org.apache.isis.commons.internal.collections._Maps;
 import org.apache.isis.commons.internal.collections._Sets;
+import org.apache.isis.commons.internal.exceptions._Exceptions;
 import org.apache.isis.core.metamodel.spec.ManagedObject;
 import org.apache.isis.core.metamodel.spec.ManagedObjects.EntityUtil;
-import org.apache.isis.core.metamodel.spec.ObjectSpecification;
 import org.apache.isis.core.metamodel.spec.feature.Contributed;
 import org.apache.isis.core.metamodel.spec.feature.ObjectAssociation;
 
+import lombok.Getter;
+import lombok.NonNull;
 import lombok.val;
 
 // tag::refguide[]
@@ -50,8 +53,10 @@ import lombok.val;
 @Order(OrderPrecedence.EARLY)
 @Primary
 @Qualifier("Default")
+@IsisInteractionScope
 //@Log4j2
-public class ChangedObjectsService implements TransactionScopeListener {
+public class ChangedObjectsService 
+implements TransactionScopeListener, HasEnlistedForAuditing, HasEnlistedForPublishing, HasEnlistedForMetrics {
 
     // end::refguide[]
     /**
@@ -72,13 +77,15 @@ public class ChangedObjectsService implements TransactionScopeListener {
      *  Will be null until {@link #getChangedObjectProperties()} is called, thereafter contains the actual changes.
      * </p>
      */
-    private Set<AuditEntry> changedObjectProperties;
+    private final _Lazy<Set<AuditEntry>> changedObjectPropertiesRef = _Lazy.threadSafe(this::capturePostValuesAndDrain);
 
 
     // used for publishing
-    private final Map<ManagedObject,PublishingChangeKind> changeKindByEnlistedAdapter = _Maps.newLinkedHashMap();
+    @Getter(onMethod_ = {@Override})
+    private final Map<ManagedObject, PublishingChangeKind> changeKindByEnlistedAdapter = _Maps.newLinkedHashMap();
+    
 
-    public boolean isEnlisted(ManagedObject adapter) {
+    public boolean isEnlisted(final @NonNull ManagedObject adapter) {
         return changeKindByEnlistedAdapter.containsKey(adapter);
     }
 
@@ -93,19 +100,12 @@ public class ChangedObjectsService implements TransactionScopeListener {
      * <p>
      * Supported by the JDO object store; check documentation for support in other objectstores.
      */
-    public void enlistCreated(final ManagedObject adapter) {
-
+    public void enlistCreated(final @NonNull ManagedObject adapter) {
         if(shouldIgnore(adapter)) {
             return;
         }
-
         enlistForPublishing(adapter, PublishingChangeKind.CREATE);
-
-        final Stream<ObjectAssociation> properties = adapter.getSpecification()
-                .streamAssociations(Contributed.EXCLUDED)
-                .filter(ObjectAssociation.Predicates.PROPERTIES);
-
-        enlist(adapter, properties, aap->PreAndPostValues.pre(IsisTransactionPlaceholder.NEW));
+        enlistForAuditing(adapter, aap->PreAndPostValues.pre(IsisTransactionPlaceholder.NEW));
     }
 
 
@@ -119,19 +119,12 @@ public class ChangedObjectsService implements TransactionScopeListener {
      * <p>
      * Supported by the JDO object store; check documentation for support in other objectstores.
      */
-    public void enlistUpdating(final ManagedObject adapter) {
-
+    public void enlistUpdating(final @NonNull ManagedObject adapter) {
         if(shouldIgnore(adapter)) {
             return;
         }
-
         enlistForPublishing(adapter, PublishingChangeKind.UPDATE);
-
-        final Stream<ObjectAssociation> properties = adapter.getSpecification()
-                .streamAssociations(Contributed.EXCLUDED)
-                .filter(ObjectAssociation.Predicates.PROPERTIES);
-
-        enlist(adapter, properties, aap->PreAndPostValues.pre(aap.getPropertyValue()));
+        enlistForAuditing(adapter, aap->PreAndPostValues.pre(aap.getPropertyValue()));
     }
 
     /**
@@ -145,30 +138,63 @@ public class ChangedObjectsService implements TransactionScopeListener {
      * <p>
      * Supported by the JDO object store; check documentation for support in other objectstores.
      */
-    public void enlistDeleting(final ManagedObject adapter) {
-
+    public void enlistDeleting(final @NonNull ManagedObject adapter) {
         if(shouldIgnore(adapter)) {
             return;
         }
-
         final boolean enlisted = enlistForPublishing(adapter, PublishingChangeKind.DELETE);
         if(!enlisted) {
             return;
         }
+        enlistForAuditing(adapter, aap->PreAndPostValues.pre(aap.getPropertyValue()));
+    }
 
-        final Stream<ObjectAssociation> properties = adapter.getSpecification()
-                .streamAssociations(Contributed.EXCLUDED)
-                .filter(ObjectAssociation.Predicates.PROPERTIES);
 
-        enlist(adapter, properties, aap->PreAndPostValues.pre(aap.getPropertyValue()));
+    @Override
+    public Set<AuditEntry> getChangedObjectProperties() {
+        return changedObjectPropertiesRef.get();
+    }
 
+    @Override
+    public int numberObjectPropertiesModified() {
+        return changedObjectPropertiesRef.get().size();
+    }
+    
+    @Override
+    public int numberObjectsDirtied() {
+        return changeKindByEnlistedAdapter.size();
+    }
+    
+    protected boolean shouldIgnore(final @NonNull ManagedObject adapter) {
+        val spec = adapter.getSpecification();
+        return HasUniqueId.class.isAssignableFrom(spec.getCorrespondingClass());
     }
 
+    // end::refguide[]
+    
+    /**
+     * @apiNote intended to be called at the end of a transaction by the framework internally
+     */
+    @Override
+    public void onTransactionEnding() {
+        System.err.println("PRE COMMIT");
+        enlistedObjectProperties.clear();
+        changeKindByEnlistedAdapter.clear();
+        changedObjectPropertiesRef.clear();
+    }
+
+    // -- HELPER
 
+    static String asString(Object object) {
+        return object != null? object.toString(): null;
+    }
+    
     /**
      * @return <code>true</code> if successfully enlisted, <code>false</code> if was already enlisted
      */
-    private boolean enlistForPublishing(final ManagedObject adapter, final PublishingChangeKind current) {
+    private boolean enlistForPublishing(
+            final @NonNull ManagedObject adapter, 
+            final @NonNull PublishingChangeKind current) {
         final PublishingChangeKind previous = changeKindByEnlistedAdapter.get(adapter);
         if(previous == null) {
             changeKindByEnlistedAdapter.put(adapter, current);
@@ -200,92 +226,20 @@ public class ChangedObjectsService implements TransactionScopeListener {
         return previous == null;
     }
 
-    /**
-     * Intended to be called at the end of the transaction.  Use {@link #onTransactionEnded()} once fully read.
-     */
-    public Set<AuditEntry> getChangedObjectProperties() {
-        return changedObjectProperties != null
-                ? changedObjectProperties
-                        : (changedObjectProperties = capturePostValuesAndDrain(enlistedObjectProperties));
-    }
-
-    private Set<AuditEntry> capturePostValuesAndDrain(final Map<AdapterAndProperty, PreAndPostValues> changedObjectProperties) {
-
-        final Map<AdapterAndProperty, PreAndPostValues> processedObjectProperties = _Maps.newLinkedHashMap();
-
-        while(!changedObjectProperties.isEmpty()) {
-
-            final Set<AdapterAndProperty> keys = _Sets.newLinkedHashSet(changedObjectProperties.keySet());
-            for (val adapterAndProperty : keys) {
-
-                val preAndPostValues = changedObjectProperties.remove(adapterAndProperty);
-
-                ManagedObject adapter = adapterAndProperty.getAdapter();
-                if(EntityUtil.isDestroyed(adapter)) {
-                    // don't touch the object!!!
-                    // JDO, for example, will complain otherwise...
-                    preAndPostValues.setPost(IsisTransactionPlaceholder.DELETED);
-                } else {
-                    preAndPostValues.setPost(adapterAndProperty.getPropertyValue());
-                }
-
-                // if we encounter the same objectProperty again, this will simply overwrite it
-                processedObjectProperties.put(adapterAndProperty, preAndPostValues);
-            }
-        }
-
-        return processedObjectProperties.entrySet().stream()
-                .filter(PreAndPostValues::shouldAudit)
-                .map(entry->AuditEntry.of(entry.getKey(), entry.getValue()))
-                .collect(_Sets.toUnmodifiable());
-
-    }
-
-    protected boolean shouldIgnore(final ManagedObject adapter) {
-        final ObjectSpecification adapterSpec = adapter.getSpecification();
-        final Class<?> adapterClass = adapterSpec.getCorrespondingClass();
-        return HasUniqueId.class.isAssignableFrom(adapterClass);
-    }
-
-
-    public Map<ManagedObject, PublishingChangeKind> getChangeKindByEnlistedAdapter() {
-        return changeKindByEnlistedAdapter;
-    }
-
-    public int numberObjectsDirtied() {
-        return changeKindByEnlistedAdapter.size();
-    }
-
-    public int numberObjectPropertiesModified() {
-        if(changedObjectProperties == null) {
-            // normally done during auditing, but in case none of the objects in this xactn are audited...
-            getChangedObjectProperties();
-        }
-        return changedObjectProperties.size();
-    }
-
-    /**
-     * Intended to be called at the end of a transaction.
-     *
-     * TODO: we ought to use Spring's @TransactionScope rather than roll-our-own.
-     */
-    @Override
-    public void onTransactionEnded() {
-        enlistedObjectProperties.clear();
-        changeKindByEnlistedAdapter.clear();
-        changedObjectProperties = null;
-    }
-
-
-    static String asString(Object object) {
-        return object != null? object.toString(): null;
-    }
-
-    private void enlist(
+    private void enlistForAuditing(
             final ManagedObject adapter, 
-            final Stream<ObjectAssociation> properties, 
             final Function<AdapterAndProperty, PreAndPostValues> pre) {
-        properties
+        
+        if(changedObjectPropertiesRef.isMemoized()) {
+            throw _Exceptions.illegalState("Cannot enlist additional changes for auditing, "
+                    + "since changedObjectPropertiesRef was already prepared (memoized) for auditing.");
+        }
+        
+        System.err.println("ENLIST " + adapter);
+        
+        adapter.getSpecification()
+        .streamAssociations(Contributed.EXCLUDED)
+        .filter(ObjectAssociation.Predicates.PROPERTIES)
         .filter(property->!property.isNotPersisted())
         .map(property->AdapterAndProperty.of(adapter, property))
         .filter(aap->!enlistedObjectProperties.containsKey(aap)) // already enlisted, so ignore
@@ -293,6 +247,38 @@ public class ChangedObjectsService implements TransactionScopeListener {
             enlistedObjectProperties.put(aap, pre.apply(aap));
         });
     }
+    
+    /** 
+     * For any enlisted Object Properties collects those, that are meant for auditing. 
+     * then clears enlisted objects.
+     */
+    private Set<AuditEntry> capturePostValuesAndDrain() {
+        
+        System.err.println("capturePostValuesAndDrain");
+        
+        val postValues = enlistedObjectProperties.entrySet().stream()
+        .peek(this::updatePostOn)
+        .filter(PreAndPostValues::shouldAudit)
+        .map(entry->AuditEntry.of(entry.getKey(), entry.getValue()))
+        .collect(_Sets.toUnmodifiable());
+        
+        enlistedObjectProperties.clear();
+        
+        return postValues;
 
+    }
 
+    private final void updatePostOn(Map.Entry<AdapterAndProperty, PreAndPostValues> enlistedEntry) {
+        val adapterAndProperty = enlistedEntry.getKey();
+        val preAndPostValues = enlistedEntry.getValue();
+        val adapter = adapterAndProperty.getAdapter();
+        if(EntityUtil.isDestroyed(adapter)) {
+            // don't touch the object!!!
+            // JDO, for example, will complain otherwise...
+            preAndPostValues.setPost(IsisTransactionPlaceholder.DELETED);
+        } else {
+            preAndPostValues.setPost(adapterAndProperty.getPropertyValue());
+        }
+    }
+    
 }
diff --git a/api/applib/src/main/java/org/apache/isis/applib/services/TransactionScopeListener.java b/core/runtime/src/main/java/org/apache/isis/core/runtime/persistence/transaction/HasEnlistedForAuditing.java
similarity index 56%
copy from api/applib/src/main/java/org/apache/isis/applib/services/TransactionScopeListener.java
copy to core/runtime/src/main/java/org/apache/isis/core/runtime/persistence/transaction/HasEnlistedForAuditing.java
index 028bf40..8c37069 100644
--- a/api/applib/src/main/java/org/apache/isis/applib/services/TransactionScopeListener.java
+++ b/core/runtime/src/main/java/org/apache/isis/core/runtime/persistence/transaction/HasEnlistedForAuditing.java
@@ -16,27 +16,13 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
+package org.apache.isis.core.runtime.persistence.transaction;
 
-package org.apache.isis.applib.services;
+import java.util.Set;
 
-import org.apache.isis.applib.annotation.IsisInteractionScope;
+//TODO[ISIS-2441] intermediate for refactoring keep?
+public interface HasEnlistedForAuditing {
+
+    Set<AuditEntry> getChangedObjectProperties();
 
-/**
- * Domain services that need to be aware of transaction boundaries can
- * implement this interface.
- * 
- * @apiNote Implementing services most likely need to be scoped in a way that
- * binds the scope to the current thread (eg. {@link IsisInteractionScope})
- *  
- * @since 2.0 (renamed from WithTransactionScope)
- */
-// tag::refguide[]
-public interface TransactionScopeListener {
-    
-    default void onTransactionStarted() {
-        // default: do nothing
-    }
-    
-    void onTransactionEnded();
 }
-// end::refguide[]
diff --git a/api/applib/src/main/java/org/apache/isis/applib/services/TransactionScopeListener.java b/core/runtime/src/main/java/org/apache/isis/core/runtime/persistence/transaction/HasEnlistedForMetrics.java
similarity index 56%
copy from api/applib/src/main/java/org/apache/isis/applib/services/TransactionScopeListener.java
copy to core/runtime/src/main/java/org/apache/isis/core/runtime/persistence/transaction/HasEnlistedForMetrics.java
index 028bf40..3305866 100644
--- a/api/applib/src/main/java/org/apache/isis/applib/services/TransactionScopeListener.java
+++ b/core/runtime/src/main/java/org/apache/isis/core/runtime/persistence/transaction/HasEnlistedForMetrics.java
@@ -16,27 +16,11 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
+package org.apache.isis.core.runtime.persistence.transaction;
 
-package org.apache.isis.applib.services;
+//TODO[ISIS-2441] intermediate for refactoring keep?
+public interface HasEnlistedForMetrics {
 
-import org.apache.isis.applib.annotation.IsisInteractionScope;
+    int numberObjectsDirtied();
 
-/**
- * Domain services that need to be aware of transaction boundaries can
- * implement this interface.
- * 
- * @apiNote Implementing services most likely need to be scoped in a way that
- * binds the scope to the current thread (eg. {@link IsisInteractionScope})
- *  
- * @since 2.0 (renamed from WithTransactionScope)
- */
-// tag::refguide[]
-public interface TransactionScopeListener {
-    
-    default void onTransactionStarted() {
-        // default: do nothing
-    }
-    
-    void onTransactionEnded();
 }
-// end::refguide[]
diff --git a/api/applib/src/main/java/org/apache/isis/applib/services/TransactionScopeListener.java b/core/runtime/src/main/java/org/apache/isis/core/runtime/persistence/transaction/HasEnlistedForPublishing.java
similarity index 56%
copy from api/applib/src/main/java/org/apache/isis/applib/services/TransactionScopeListener.java
copy to core/runtime/src/main/java/org/apache/isis/core/runtime/persistence/transaction/HasEnlistedForPublishing.java
index 028bf40..90267e6 100644
--- a/api/applib/src/main/java/org/apache/isis/applib/services/TransactionScopeListener.java
+++ b/core/runtime/src/main/java/org/apache/isis/core/runtime/persistence/transaction/HasEnlistedForPublishing.java
@@ -16,27 +16,18 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
+package org.apache.isis.core.runtime.persistence.transaction;
 
-package org.apache.isis.applib.services;
+import java.util.Map;
 
-import org.apache.isis.applib.annotation.IsisInteractionScope;
+import org.apache.isis.applib.annotation.PublishingChangeKind;
+import org.apache.isis.core.metamodel.spec.ManagedObject;
+
+//TODO[ISIS-2441] intermediate for refactoring keep?
+public interface HasEnlistedForPublishing {
+
+    Map<ManagedObject, PublishingChangeKind> getChangeKindByEnlistedAdapter();
+
+    int numberObjectPropertiesModified();
 
-/**
- * Domain services that need to be aware of transaction boundaries can
- * implement this interface.
- * 
- * @apiNote Implementing services most likely need to be scoped in a way that
- * binds the scope to the current thread (eg. {@link IsisInteractionScope})
- *  
- * @since 2.0 (renamed from WithTransactionScope)
- */
-// tag::refguide[]
-public interface TransactionScopeListener {
-    
-    default void onTransactionStarted() {
-        // default: do nothing
-    }
-    
-    void onTransactionEnded();
 }
-// end::refguide[]
diff --git a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/publish/PublisherDispatchServiceDefault.java b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/publish/PublisherDispatchServiceDefault.java
index 7a052f1..45bd189 100644
--- a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/publish/PublisherDispatchServiceDefault.java
+++ b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/publish/PublisherDispatchServiceDefault.java
@@ -19,7 +19,6 @@
 
 package org.apache.isis.core.runtimeservices.publish;
 
-import java.sql.Timestamp;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -48,11 +47,10 @@ import org.apache.isis.commons.internal.collections._Maps;
 import org.apache.isis.core.metamodel.facets.object.publishedobject.PublishedObjectFacet;
 import org.apache.isis.core.metamodel.services.publishing.PublisherDispatchService;
 import org.apache.isis.core.metamodel.spec.ManagedObject;
-import org.apache.isis.core.runtime.persistence.transaction.ChangedObjectsService;
+import org.apache.isis.core.runtime.persistence.transaction.HasEnlistedForPublishing;
 
 import lombok.RequiredArgsConstructor;
 import lombok.val;
-import lombok.extern.log4j.Log4j2;
 
 /**
  * Wrapper around {@link PublisherService}.  Is a no-op if there is no injected service.
@@ -63,14 +61,14 @@ import lombok.extern.log4j.Log4j2;
 @Primary
 @Qualifier("Default")
 @IsisInteractionScope
-@Log4j2
 @RequiredArgsConstructor
+//@Log4j2
 public class PublisherDispatchServiceDefault implements PublisherDispatchService {
 
     @Inject final List<PublisherService> publisherServices;
     @Inject final ClockService clockService;
     @Inject final UserService userService;
-    @Inject final Provider<ChangedObjectsService> changedObjectsProvider;
+    @Inject final Provider<HasEnlistedForPublishing> changedObjectsProvider;
     @Inject final Provider<InteractionContext> interactionContextProvider;
     @Inject final Provider<MetricsService> metricsServiceProvider;
     
@@ -113,7 +111,7 @@ public class PublisherDispatchServiceDefault implements PublisherDispatchService
             final int numberObjectPropertiesModified,
             final Map<ManagedObject, PublishingChangeKind> changeKindByPublishedAdapter) {
 
-        final Interaction interaction = interactionContextProvider.get().getInteraction();
+        val interaction = interactionContextProvider.get().getInteraction();
         val uniqueId = interaction.getUniqueId();
 
         if(uniqueId == null) {
@@ -122,8 +120,8 @@ public class PublisherDispatchServiceDefault implements PublisherDispatchService
         }
 
         final int nextEventSequence = interaction.next(Interaction.Sequence.INTERACTION.id());
-        final String userName = userService.getUser().getName();
-        final Timestamp timestamp = clockService.nowAsJavaSqlTimestamp();
+        val userName = userService.getUser().getName();
+        val timestamp = clockService.nowAsJavaSqlTimestamp();
 
         return new PublishedObjectsDefault(
                     uniqueId, nextEventSequence,
diff --git a/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/metrics/MetricsServiceForJdo.java b/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/metrics/MetricsServiceForJdo.java
index 061c4a2..07ee345 100644
--- a/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/metrics/MetricsServiceForJdo.java
+++ b/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/metrics/MetricsServiceForJdo.java
@@ -35,7 +35,7 @@ import org.apache.isis.applib.annotation.IsisInteractionScope;
 import org.apache.isis.applib.annotation.OrderPrecedence;
 import org.apache.isis.applib.services.TransactionScopeListener;
 import org.apache.isis.applib.services.metrics.MetricsService;
-import org.apache.isis.core.runtime.persistence.transaction.ChangedObjectsService;
+import org.apache.isis.core.runtime.persistence.transaction.HasEnlistedForMetrics;
 
 @Service
 @Named("isisJdoDn5.MetricsServiceForJdo")
@@ -47,7 +47,7 @@ import org.apache.isis.core.runtime.persistence.transaction.ChangedObjectsServic
 public class MetricsServiceForJdo 
 implements MetricsService, InstanceLifecycleListener, LoadLifecycleListener, TransactionScopeListener {
 
-    @Inject private javax.inject.Provider<ChangedObjectsService> changedObjectsProvider;
+    @Inject private javax.inject.Provider<HasEnlistedForMetrics> changedObjectsProvider;
     
     private LongAdder numberLoaded = new LongAdder();
 
@@ -71,7 +71,7 @@ implements MetricsService, InstanceLifecycleListener, LoadLifecycleListener, Tra
      * a transaction-scoped service; since that isn't yet supported by the framework, we have to manually reset).
      */
     @Override
-    public void onTransactionEnded() {
+    public void onTransactionEnding() {
         numberLoaded.reset();
     }
     
diff --git a/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/persistence/IsisTransactionJdo.java b/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/persistence/IsisTransactionJdo.java
index 9b77289..2dbd415 100644
--- a/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/persistence/IsisTransactionJdo.java
+++ b/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/persistence/IsisTransactionJdo.java
@@ -304,8 +304,8 @@ public class IsisTransactionJdo implements Transaction {
     }
 
     /**
+     * Called by both {@link #commit()} and {@link #flush()}:
      * <p>
-     * Called by both {@link #commit()} and by {@link #flush()}:
      * <table>
      * <tr>
      * <th>called from</th>
@@ -330,7 +330,7 @@ public class IsisTransactionJdo implements Transaction {
         // it's possible that in executing these commands that more will be created.
         // so we keep flushing until no more are available (ISIS-533)
         //
-        // this is a do...while rather than a while... just for backward compatibilty
+        // this is a do...while rather than a while... just for backward compatibility
         // with previous algorithm that always went through the execute phase at least once.
         //
         do {
@@ -376,17 +376,17 @@ public class IsisTransactionJdo implements Transaction {
 
         try {
             
+            doFlush();
+            
             auditerDispatchService.audit();
             publisherDispatchService.publishObjects();
-            
-            doFlush();
 
         } catch (final RuntimeException ex) {
             setAbortCause(new IsisTransactionManagerException(ex));
             throw ex;
         } finally {
             for (TransactionScopeListener listener : transactionScopeListeners) {
-                listener.onTransactionEnded();
+                listener.onTransactionEnding();
             }
         }
     }
@@ -404,6 +404,7 @@ public class IsisTransactionJdo implements Transaction {
         }
 
         setState(State.COMMITTED);
+        
     }
 
     // -- abortCause, markAsAborted
diff --git a/regressiontests/stable/src/test/java/org/apache/isis/testdomain/commons/InteractionBoundaryProbe.java b/regressiontests/stable/src/test/java/org/apache/isis/testdomain/commons/InteractionBoundaryProbe.java
index 360019c..4e981a0 100644
--- a/regressiontests/stable/src/test/java/org/apache/isis/testdomain/commons/InteractionBoundaryProbe.java
+++ b/regressiontests/stable/src/test/java/org/apache/isis/testdomain/commons/InteractionBoundaryProbe.java
@@ -59,8 +59,8 @@ public class InteractionBoundaryProbe implements TransactionScopeListener {
 
     /** TRANSACTION END BOUNDARY */
     @Override
-    public void onTransactionEnded() {
-        kvStoreForTesting.incrementCounter(InteractionBoundaryProbe.class, "txEnded");
+    public void onTransactionEnding() {
+        kvStoreForTesting.incrementCounter(InteractionBoundaryProbe.class, "txEnding");
     }
     
     // -- ACCESS TO COUNTERS
@@ -78,7 +78,7 @@ public class InteractionBoundaryProbe implements TransactionScopeListener {
     }
     
     public static long totalTransactionsEnded(KVStoreForTesting kvStoreForTesting) {
-        return kvStoreForTesting.getCounter(InteractionBoundaryProbe.class, "txEnded");
+        return kvStoreForTesting.getCounter(InteractionBoundaryProbe.class, "txEnding");
     }
 
     // -- ASSERTIONS (INTERACTIONAL)
diff --git a/regressiontests/stable/src/test/java/org/apache/isis/testdomain/entitychangemetrics/ChangedObjectsTest.java b/regressiontests/stable/src/test/java/org/apache/isis/testdomain/entitychangemetrics/ChangedObjectsTest.java
index e9b94ed..871cd42 100644
--- a/regressiontests/stable/src/test/java/org/apache/isis/testdomain/entitychangemetrics/ChangedObjectsTest.java
+++ b/regressiontests/stable/src/test/java/org/apache/isis/testdomain/entitychangemetrics/ChangedObjectsTest.java
@@ -52,7 +52,8 @@ import lombok.val;
                 InteractionBoundaryProbe.class
         }, 
         properties = {
-                "logging.level.org.apache.isis.testdomain.util.rest.KVStoreForTesting=DEBUG"
+                "logging.level.org.apache.isis.testdomain.util.rest.KVStoreForTesting=DEBUG",
+                "logging.level.org.apache.isis.persistence.jdo.datanucleus5.persistence.IsisTransactionJdo=DEBUG"
         })
 @TestPropertySource({
     IsisPresets.SilenceWicket
@@ -69,6 +70,8 @@ class ChangedObjectsTest extends InteractionTestAbstract {
 
     @BeforeEach
     void setUp() {
+        
+        System.err.println("===BEFORE SETUP");
 
         // cleanup
         fixtureScripts.runPersona(JdoTestDomainPersona.PurgeAll);
@@ -79,6 +82,8 @@ class ChangedObjectsTest extends InteractionTestAbstract {
         // each test runs in its own interaction context (check)
         val testRunNr = kvStoreForTesting.incrementCounter(ChangedObjectsTest.class, "test-run");
         assertEquals(testRunNr, InteractionBoundaryProbe.totalInteractionsStarted(kvStoreForTesting));
+        
+        System.err.println("===AFTER SETUP");
     }
     
     @Test
@@ -88,10 +93,14 @@ class ChangedObjectsTest extends InteractionTestAbstract {
         val book = getBookSample();
         val inventoryManager = factoryService.create(JdoInventoryManager.class);
         
+        System.err.println("=1==BEFORE  TX");
+        
         // spawns its own transactional boundary (check)
         val product = assertTransactional( 
                 ()->wrapper.wrap(inventoryManager).updateProductPrice(book, 12.));
         
+        System.err.println("=1==AFTER  TX");
+        
         assertEquals(12., product.getPrice(), 1E-3);
         
         // previous transaction has committed, so ChangedObjectsService should have cleared its data  
@@ -105,10 +114,14 @@ class ChangedObjectsTest extends InteractionTestAbstract {
         // given
         val book = getBookSample();
 
+        System.err.println("=2==BEFORE  TX");
+        
         // spawns its own transactional boundary (check) 
         val product = (JdoProduct) assertTransactional( 
                 ()->invokeAction(JdoInventoryManager.class, "updateProductPrice", _Lists.of(book, 12.)));
         
+        System.err.println("=2==AFTER  TX");
+        
         assertEquals(12., product.getPrice(), 1E-3);                
         
         // previous transaction has committed, so ChangedObjectsService should have cleared its data  
@@ -118,10 +131,16 @@ class ChangedObjectsTest extends InteractionTestAbstract {
     // -- HELPER
     
     private JdoBook getBookSample() {
+        
+        System.err.println("===BEFORE BOOK");
+        
         // spawns its own transactional boundary (check)
         val books = assertTransactional(()->repositoryService.allInstances(JdoBook.class));
         assertEquals(1, books.size());
         val book = books.listIterator().next();
+        
+        System.err.println("===AFTER BOOK");
+        
         return book;
     }