You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@causeway.apache.org by da...@apache.org on 2023/03/03 18:38:39 UTC

[causeway] 01/01: CAUSEWAY-3366: wip, adding in onReady and onStarted callbacks

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

danhaywood pushed a commit to branch CAUSEWAY-3366
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit 356bf204b632a6320601c4de021d4663c25a1371
Author: danhaywood <da...@haywood-associates.co.uk>
AuthorDate: Fri Mar 3 18:22:56 2023 +0000

    CAUSEWAY-3366: wip, adding in onReady and onStarted callbacks
---
 .../causeway/applib/services/command/Command.java  | 16 ++++-
 .../services/publishing/log/CommandLogger.java     | 17 ++++-
 .../services/publishing/spi/CommandSubscriber.java | 29 ++++++--
 .../interaction/session/CausewayInteraction.java   | 19 +++--
 .../metamodel/execution/InteractionInternal.java   |  3 +
 .../publish/command/CommandPublishingFacet.java    | 17 -----
 .../services/publishing/CommandPublisher.java      | 16 ++++-
 .../command/CommandExecutorServiceDefault.java     |  8 +--
 .../executor/MemberExecutorServiceDefault.java     | 38 ++++++++--
 .../publish/CommandPublisherDefault.java           | 40 ++++++++++-
 .../core/runtimeservices/publish/_Xray.java        | 32 +++++++--
 .../session/InteractionServiceDefault.java         |  4 +-
 .../commandlog/applib/dom/BackgroundService.java   |  1 -
 .../subscriber/CommandSubscriberForCommandLog.java | 81 +++++++++++++++++++++-
 .../subscriber/CommandSubscriberForTesting.java    | 27 ++++++--
 15 files changed, 288 insertions(+), 60 deletions(-)

diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/command/Command.java b/api/applib/src/main/java/org/apache/causeway/applib/services/command/Command.java
index f03d85f9ca..cab8bbd14a 100644
--- a/api/applib/src/main/java/org/apache/causeway/applib/services/command/Command.java
+++ b/api/applib/src/main/java/org/apache/causeway/applib/services/command/Command.java
@@ -236,18 +236,28 @@ public class Command implements HasInteractionId, HasUsername, HasCommandDto {
     public static enum CommandPublishingPhase {
         /** initial state: do not publish (yet) */
         ONHOLD,
-        /** publishing is enabled */
+        /**
+         * publishing is enabled, and the command will be executed.
+         */
         READY,
-        /** publishing has completed */
+        /**
+         * The command has started to be executed.
+         */
+        STARTED,
+        /**
+         * The command has completed its execution.
+         */
         COMPLETED;
         public boolean isOnhold() {return this==ONHOLD;}
         public boolean isReady() {return this==READY;}
+        public boolean isStarted() {return this==STARTED;}
         public boolean isCompleted() {return this==COMPLETED;}
     }
 
     /**
      * Whether this command has been enabled for publishing,
-     * that is {@link CommandSubscriber}s will be notified when this Command completes.
+     * that is {@link CommandSubscriber}s will be notified when this Command becomes {@link CommandPublishingPhase#READY ready},
+     * has {@link CommandPublishingPhase#STARTED started}, and when it {@link CommandPublishingPhase#COMPLETED completes}.
      */
     @Getter private CommandPublishingPhase publishingPhase = CommandPublishingPhase.ONHOLD;
 
diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/publishing/log/CommandLogger.java b/api/applib/src/main/java/org/apache/causeway/applib/services/publishing/log/CommandLogger.java
index ce062b3c73..3760ca8431 100644
--- a/api/applib/src/main/java/org/apache/causeway/applib/services/publishing/log/CommandLogger.java
+++ b/api/applib/src/main/java/org/apache/causeway/applib/services/publishing/log/CommandLogger.java
@@ -53,13 +53,28 @@ public class CommandLogger implements CommandSubscriber {
         return log.isDebugEnabled();
     }
 
+    @Override
+    public void onReady(Command command) {
+        on("ready", command);
+    }
+
+    @Override
+    public void onStarted(Command command) {
+        on("started", command);
+    }
+
     @Override
     public void onCompleted(final Command command) {
 
+        on("completed", command);
+    }
+
+    private static void on(String verb, Command command) {
         val commandDto = command.getCommandDto();
         val xml = CommandDtoUtils.dtoMapper().toString(commandDto);
 
-        log.debug("completed: {} \n{}",
+        log.debug("{}: {} \n{}",
+                verb,
                 command.getLogicalMemberIdentifier(),
                 xml);
     }
diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/publishing/spi/CommandSubscriber.java b/api/applib/src/main/java/org/apache/causeway/applib/services/publishing/spi/CommandSubscriber.java
index b412dbcc38..f4ca48fd65 100644
--- a/api/applib/src/main/java/org/apache/causeway/applib/services/publishing/spi/CommandSubscriber.java
+++ b/api/applib/src/main/java/org/apache/causeway/applib/services/publishing/spi/CommandSubscriber.java
@@ -22,19 +22,40 @@ import org.apache.causeway.applib.services.command.Command;
 import org.apache.causeway.commons.having.HasEnabling;
 
 /**
- * Part of the <i>Publishing SPI</i>. A component to receive {@link Command}s 
+ * Part of the <i>Publishing SPI</i>. A component to receive {@link Command}s
  * (with publishing enabled) that just completed.
- *  
+ *
  * @since 2.0 {@index}
  */
 public interface CommandSubscriber extends HasEnabling {
 
     /**
-     * Notifies that the command has completed.
+     * Notifies that the command will be published, and has transitioned to {@link org.apache.causeway.applib.services.command.Command.CommandPublishingPhase#READY}.
+     *
+     * <p>
+     *     This is an opportunity for implementations to process the command,
+     *     for example to persist an initial representation of it.
+     * </p>
+     */
+    void onReady(Command command);
+
+    /**
+     * Notifies that the command has started to execute, and has transitioned to {@link org.apache.causeway.applib.services.command.Command.CommandPublishingPhase#STARTED}.
+     *
+     * <p>
+     *     This is an opportunity for implementations to process the command,
+     *     for example to update any persisted representation of it.
+     * </p>
+     */
+    void onStarted(Command command);
+
+
+    /**
+     * Notifies that the command has completed and has transitioned to {@link org.apache.causeway.applib.services.command.Command.CommandPublishingPhase#COMPLETED}
      *
      * <p>
      *     This is an opportunity for implementations to process the command,
-     *     for example to persist a representation of it.
+     *     for example to update any persisted representations of it.
      * </p>
      */
     void onCompleted(Command command);
diff --git a/core/interaction/src/main/java/org/apache/causeway/core/interaction/session/CausewayInteraction.java b/core/interaction/src/main/java/org/apache/causeway/core/interaction/session/CausewayInteraction.java
index 9d3b12aa28..c8fc97a9f0 100644
--- a/core/interaction/src/main/java/org/apache/causeway/core/interaction/session/CausewayInteraction.java
+++ b/core/interaction/src/main/java/org/apache/causeway/core/interaction/session/CausewayInteraction.java
@@ -36,6 +36,7 @@ import org.apache.causeway.commons.internal.base._Casts;
 import org.apache.causeway.commons.internal.collections._Lists;
 import org.apache.causeway.commons.internal.exceptions._Exceptions;
 import org.apache.causeway.core.metamodel.execution.InteractionInternal;
+import org.apache.causeway.core.metamodel.services.publishing.CommandPublisher;
 
 import lombok.Getter;
 import lombok.NonNull;
@@ -99,9 +100,10 @@ implements InteractionInternal {
             final ActionInvocation actionInvocation,
             final ClockService clockService,
             final MetricsService metricsService,
+            final CommandPublisher commandPublisher,
             final Command command) {
 
-        pushAndStart(actionInvocation, clockService, metricsService, command);
+        pushAndStart(actionInvocation, clockService, metricsService, commandPublisher, command);
         try {
             return executeInternal(memberExecutor, actionInvocation);
         } finally {
@@ -109,9 +111,14 @@ implements InteractionInternal {
         }
     }
 
-    private void pushAndStart(final ActionInvocation actionInvocation, final ClockService clockService, final MetricsService metricsService, final Command command) {
+    private void pushAndStart(
+            final ActionInvocation actionInvocation,
+            final ClockService clockService,
+            final MetricsService metricsService,
+            final CommandPublisher commandPublisher,
+            final Command command) {
         push(actionInvocation);
-        start(actionInvocation, clockService, metricsService, command);
+        start(actionInvocation, clockService, metricsService, commandPublisher, command);
     }
 
     @Override
@@ -120,10 +127,11 @@ implements InteractionInternal {
             final PropertyEdit propertyEdit,
             final ClockService clockService,
             final MetricsService metricsService,
+            final CommandPublisher commandPublisher,
             final Command command) {
 
         push(propertyEdit);
-        start(propertyEdit, clockService, metricsService, command);
+        start(propertyEdit, clockService, metricsService, commandPublisher, command);
         try {
             return executeInternal(memberExecutor, propertyEdit);
         } finally {
@@ -186,6 +194,7 @@ implements InteractionInternal {
             final Execution<?,?> execution,
             final ClockService clockService,
             final MetricsService metricsService,
+            final CommandPublisher commandPublisher,
             final Command command) {
         // set the startedAt (and update command if this is the top-most member execution)
         // (this isn't done within Interaction#execute(...) because it requires the DTO
@@ -193,7 +202,9 @@ implements InteractionInternal {
         val startedAt = execution.start(clockService, metricsService);
         if(command.getStartedAt() == null) {
             command.updater().setStartedAt(startedAt);
+            command.updater().setPublishingPhase(Command.CommandPublishingPhase.STARTED);
         }
+        commandPublisher.start(command);
     }
 
     /**
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/execution/InteractionInternal.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/execution/InteractionInternal.java
index d10a6f860f..bf67058c81 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/execution/InteractionInternal.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/execution/InteractionInternal.java
@@ -29,6 +29,7 @@ import org.apache.causeway.applib.services.iactn.Interaction;
 import org.apache.causeway.applib.services.iactn.PropertyEdit;
 import org.apache.causeway.applib.services.metrics.MetricsService;
 import org.apache.causeway.applib.services.wrapper.WrapperFactory;
+import org.apache.causeway.core.metamodel.services.publishing.CommandPublisher;
 
 import lombok.NonNull;
 import lombok.val;
@@ -61,6 +62,7 @@ extends Interaction {
             final ActionInvocation actionInvocation,
             final ClockService clockService,
             final MetricsService metricsService,
+            final CommandPublisher commandPublisher,
             final Command command);
 
     /**
@@ -77,6 +79,7 @@ extends Interaction {
             final PropertyEdit propertyEdit,
             final ClockService clockService,
             final MetricsService metricsService,
+            final CommandPublisher commandPublisher,
             final Command command);
 
 
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/command/CommandPublishingFacet.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/command/CommandPublishingFacet.java
index dfaf4c2ce1..58d58db465 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/command/CommandPublishingFacet.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/command/CommandPublishingFacet.java
@@ -49,21 +49,4 @@ public interface CommandPublishingFacet extends Facet {
     public static boolean isPublishingEnabled(final @NonNull FacetHolder facetHolder) {
         return facetHolder.containsFacet(CommandPublishingFacet.class);
     }
-
-    /**
-     * Will set the command's CommandPublishingPhase to READY,
-     * if command and objectMember have a matching member-id
-     * and if the facetHoler has a CommandPublishingFacet (has commandPublishing=ENABLED).
-     */
-    public static void prepareCommandForPublishing(
-            final @NonNull Command command,
-            final @NonNull InteractionHead interactionHead,
-            final @NonNull ObjectMember objectMember,
-            final @NonNull FacetHolder facetHolder) {
-
-        if(IdentifierUtil.isCommandForMember(command, interactionHead, objectMember)
-                && isPublishingEnabled(facetHolder)) {
-            command.updater().setPublishingPhase(CommandPublishingPhase.READY);
-        }
-    }
 }
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/publishing/CommandPublisher.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/publishing/CommandPublisher.java
index d8fd9e2bf8..cecd04379c 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/publishing/CommandPublisher.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/publishing/CommandPublisher.java
@@ -30,8 +30,20 @@ import lombok.NonNull;
 public interface CommandPublisher {
 
     /**
-     * &quot;Completes&quot; the command, meaning that all {@link CommandSubscriber}s
-     * are notified throuhg {@link CommandSubscriber#onCompleted(Command)}.
+     * Notifies all {@link CommandSubscriber}s (through {@link CommandSubscriber#onReady(Command)}) that the
+     * {@link Command} has been created/is ready for execution
+     */
+    void ready(@NonNull Command command);
+
+    /**
+     * Notifies all {@link CommandSubscriber}s (through {@link CommandSubscriber#onStarted(Command)}) that the
+     * {@link Command} has started.
+     */
+    void start(@NonNull Command command);
+
+    /**
+     * Notifies all {@link CommandSubscriber}s (through {@link CommandSubscriber#onCompleted(Command)}) that the
+     * {@link Command} has completed.
      */
     void complete(@NonNull Command command);
 
diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/command/CommandExecutorServiceDefault.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/command/CommandExecutorServiceDefault.java
index 115d1f38be..5b64abe069 100644
--- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/command/CommandExecutorServiceDefault.java
+++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/command/CommandExecutorServiceDefault.java
@@ -38,7 +38,6 @@ import org.apache.causeway.applib.services.command.Command;
 import org.apache.causeway.applib.services.command.CommandExecutorService;
 import org.apache.causeway.applib.services.command.CommandOutcomeHandler;
 import org.apache.causeway.applib.services.iactnlayer.InteractionLayerTracker;
-import org.apache.causeway.applib.services.iactnlayer.InteractionService;
 import org.apache.causeway.applib.services.sudo.SudoService;
 import org.apache.causeway.applib.services.xactn.TransactionService;
 import org.apache.causeway.applib.util.schema.CommandDtoUtils;
@@ -82,10 +81,9 @@ public class CommandExecutorServiceDefault implements CommandExecutorService {
     @Inject final SudoService sudoService;
     @Inject final ClockService clockService;
     @Inject final TransactionService transactionService;
-    @Inject final InteractionLayerTracker iInteractionLayerTracker;
+    @Inject final InteractionLayerTracker interactionLayerTracker;
     @Inject final SchemaValueMarshaller valueMarshaller;
 
-    @Inject @Getter final InteractionService interactionService;
     @Inject @Getter final SpecificationLoader specificationLoader;
 
     @Override
@@ -123,7 +121,7 @@ public class CommandExecutorServiceDefault implements CommandExecutorService {
             final CommandDto dto,
             final CommandOutcomeHandler commandOutcomeHandler) {
 
-        val interaction = iInteractionLayerTracker.currentInteractionElseFail();
+        val interaction = interactionLayerTracker.currentInteractionElseFail();
         val command = interaction.getCommand();
         if(command.getCommandDto() != dto) {
             command.updater().setCommandDto(dto);
@@ -171,7 +169,7 @@ public class CommandExecutorServiceDefault implements CommandExecutorService {
     private void copyStartedAtFromInteractionExecution(
             final CommandOutcomeHandler commandOutcomeHandler) {
 
-        val interaction = iInteractionLayerTracker.currentInteractionElseFail();
+        val interaction = interactionLayerTracker.currentInteractionElseFail();
         val currentExecution = interaction.getCurrentExecution();
 
         val startedAt = currentExecution != null
diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefault.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefault.java
index 30f4cbae10..12e41e74d2 100644
--- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefault.java
+++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/executor/MemberExecutorServiceDefault.java
@@ -18,7 +18,6 @@
  */
 package org.apache.causeway.core.runtimeservices.executor;
 
-import java.lang.reflect.Method;
 import java.util.Optional;
 
 import javax.annotation.Priority;
@@ -26,6 +25,9 @@ import javax.inject.Inject;
 import javax.inject.Named;
 import javax.inject.Provider;
 
+import org.apache.causeway.core.metamodel.facets.actions.action.invocation.IdentifierUtil;
+import org.apache.causeway.core.metamodel.services.publishing.CommandPublisher;
+import org.apache.causeway.core.metamodel.spec.feature.ObjectMember;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.stereotype.Service;
 
@@ -51,7 +53,6 @@ import org.apache.causeway.core.metamodel.consent.InteractionInitiatedBy;
 import org.apache.causeway.core.metamodel.execution.InteractionInternal;
 import org.apache.causeway.core.metamodel.execution.MemberExecutorService;
 import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
-import org.apache.causeway.core.metamodel.facets.members.publish.command.CommandPublishingFacet;
 import org.apache.causeway.core.metamodel.facets.members.publish.execution.ExecutionPublishingFacet;
 import org.apache.causeway.core.metamodel.facets.properties.property.modify.PropertySetterOrClearFacetForDomainEventAbstract.EditingVariant;
 import org.apache.causeway.core.metamodel.interactions.InteractionHead;
@@ -77,6 +78,8 @@ import lombok.SneakyThrows;
 import lombok.val;
 import lombok.extern.log4j.Log4j2;
 
+import static org.apache.causeway.core.metamodel.facets.members.publish.command.CommandPublishingFacet.isPublishingEnabled;
+
 @Service
 @Named(CausewayModuleCoreRuntimeServices.NAMESPACE + ".MemberExecutorServiceDefault")
 @Priority(PriorityPrecedence.EARLY)
@@ -96,6 +99,7 @@ implements MemberExecutorService {
     private final @Getter Provider<ExecutionPublisher> executionPublisherProvider;
     private final @Getter MetamodelEventService metamodelEventService;
     private final @Getter TransactionService transactionService;
+    private final Provider<CommandPublisher> commandPublisherProvider;
 
     private MetricsService metricsService() {
         return metricsServiceProvider.get();
@@ -137,7 +141,7 @@ implements MemberExecutorService {
         val interaction = getInteractionElseFail();
         val command = interaction.getCommand();
 
-        CommandPublishingFacet.prepareCommandForPublishing(command, head, owningAction, facetHolder);
+        prepareCommandForPublishing(command, head, owningAction, facetHolder);
 
         val xrayHandle = _Xray.enterActionInvocation(interactionLayerTracker, interaction, owningAction, head, argumentAdapters);
 
@@ -156,7 +160,7 @@ implements MemberExecutorService {
         val memberExecutor = actionExecutorFactory.createExecutor(owningAction, head, argumentAdapters);
 
         // sets up startedAt and completedAt on the execution, also manages the execution call graph
-        interaction.execute(memberExecutor, actionInvocation, clockService, metricsService(), command);
+        interaction.execute(memberExecutor, actionInvocation, clockService, metricsService(), commandPublisherProvider.get(), command);
 
         // handle any exceptions
         val priorExecution = interaction.getPriorExecutionOrThrowIfAnyException(actionInvocation);
@@ -195,6 +199,7 @@ implements MemberExecutorService {
         return result;
     }
 
+
     @Override
     public ManagedObject setOrClearProperty(
             final @NonNull OneToOneAssociation owningProperty,
@@ -211,7 +216,7 @@ implements MemberExecutorService {
             return head.getTarget();
         }
 
-        CommandPublishingFacet.prepareCommandForPublishing(command, head, owningProperty, facetHolder);
+        prepareCommandForPublishing(command, head, owningProperty, facetHolder);
 
         val xrayHandle = _Xray.enterPropertyEdit(interactionLayerTracker, interaction, owningProperty, head, newValueAdapter);
 
@@ -227,7 +232,7 @@ implements MemberExecutorService {
                         interactionInitiatedBy, editingVariant);
 
         // sets up startedAt and completedAt on the execution, also manages the execution call graph
-        val targetPojo = interaction.execute(executor, propertyEdit, clockService, metricsService(), command);
+        val targetPojo = interaction.execute(executor, propertyEdit, clockService, metricsService(), commandPublisherProvider.get(), command);
 
         // handle any exceptions
         final Execution<?, ?> priorExecution = interaction.getPriorExecution();
@@ -313,4 +318,25 @@ implements MemberExecutorService {
                 : null;
     }
 
+
+    /**
+     * Will set the command's CommandPublishingPhase to READY,
+     * if command and objectMember have a matching member-id
+     * and if the facetHolder has a CommandPublishingFacet (has commandPublishing=ENABLED).
+     */
+    void prepareCommandForPublishing(
+            final @NonNull Command command,
+            final @NonNull InteractionHead interactionHead,
+            final @NonNull ObjectMember objectMember,
+            final @NonNull FacetHolder facetHolder) {
+
+        if(IdentifierUtil.isCommandForMember(command, interactionHead, objectMember)
+                && isPublishingEnabled(facetHolder)) {
+            command.updater().setPublishingPhase(Command.CommandPublishingPhase.READY);
+        }
+
+        commandPublisherProvider.get().ready(command);
+
+    }
+
 }
diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/CommandPublisherDefault.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/CommandPublisherDefault.java
index e56288f116..c3f2836f52 100644
--- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/CommandPublisherDefault.java
+++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/CommandPublisherDefault.java
@@ -64,17 +64,53 @@ public class CommandPublisherDefault implements CommandPublisher {
                 .filter(HasEnabling::isEnabled);
     }
 
+    @Override
+    public void ready(@NonNull Command command) {
+
+        val handle = _Xray.enterCommandReadyPublishing(
+                interactionServiceProvider.get(),
+                command,
+                enabledSubscribers,
+                ()->getCannotPublishReason(command));
+
+        if(canPublish(command)) {
+            log.debug("about to PUBLISH command {}: {} to {}", "ready", command, enabledSubscribers);
+            enabledSubscribers.forEach(subscriber -> subscriber.onReady(command));
+            command.updater().setPublishingPhase(CommandPublishingPhase.READY);
+        }
+
+        _Xray.exitPublishing(handle);
+    }
+
+    @Override
+    public void start(@NonNull Command command) {
+
+        val handle = _Xray.enterCommandStartedPublishing(
+                interactionServiceProvider.get(),
+                command,
+                enabledSubscribers,
+                ()->getCannotPublishReason(command));
+
+        if(canPublish(command)) {
+            log.debug("about to PUBLISH command {}: {} to {}", "started", command, enabledSubscribers);
+            enabledSubscribers.forEach(subscriber -> subscriber.onStarted(command));
+            command.updater().setPublishingPhase(CommandPublishingPhase.STARTED);
+        }
+
+        _Xray.exitPublishing(handle);
+    }
+
     @Override
     public void complete(final @NonNull Command command) {
 
-        val handle = _Xray.enterCommandPublishing(
+        val handle = _Xray.enterCommandCompletedPublishing(
                 interactionServiceProvider.get(),
                 command,
                 enabledSubscribers,
                 ()->getCannotPublishReason(command));
 
         if(canPublish(command)) {
-            log.debug("about to PUBLISH command: {} to {}", command, enabledSubscribers);
+            log.debug("about to PUBLISH command {}: {} to {}", "completed", command, enabledSubscribers);
             enabledSubscribers.forEach(subscriber -> subscriber.onCompleted(command));
             command.updater().setPublishingPhase(CommandPublishingPhase.COMPLETED); // one shot
         }
diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/_Xray.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/_Xray.java
index edbd162bc2..a4789bf742 100644
--- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/_Xray.java
+++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/_Xray.java
@@ -47,12 +47,36 @@ final class _Xray {
 
     // -- COMMAND
 
-    static SequenceHandle enterCommandPublishing(
+    static SequenceHandle enterCommandReadyPublishing(
             final @NonNull InteractionLayerTracker iaTracker,
             final @Nullable Command command,
             final @NonNull Can<CommandSubscriber> enabledSubscribers,
             final @NonNull Supplier<String> cannotPublishReasonSupplier) {
 
+        return enterCommandPublishing(iaTracker, command, enabledSubscribers, cannotPublishReasonSupplier, "created");
+    }
+
+    static SequenceHandle enterCommandStartedPublishing(
+            final @NonNull InteractionLayerTracker iaTracker,
+            final @Nullable Command command,
+            final @NonNull Can<CommandSubscriber> enabledSubscribers,
+            final @NonNull Supplier<String> cannotPublishReasonSupplier) {
+
+        return enterCommandPublishing(iaTracker, command, enabledSubscribers, cannotPublishReasonSupplier, "started");
+
+    }
+
+    static SequenceHandle enterCommandCompletedPublishing(
+            final @NonNull InteractionLayerTracker iaTracker,
+            final @Nullable Command command,
+            final @NonNull Can<CommandSubscriber> enabledSubscribers,
+            final @NonNull Supplier<String> cannotPublishReasonSupplier) {
+
+        return enterCommandPublishing(iaTracker, command, enabledSubscribers, cannotPublishReasonSupplier, "completed");
+    }
+
+
+    private static SequenceHandle enterCommandPublishing(InteractionLayerTracker iaTracker, Command command, Can<CommandSubscriber> enabledSubscribers, Supplier<String> cannotPublishReasonSupplier, String verb) {
         if(!XrayUi.isXrayEnabled()) {
             return null;
         }
@@ -60,10 +84,11 @@ final class _Xray {
         val cannotPublishReason = cannotPublishReasonSupplier.get();
         val canPublish = cannotPublishReason==null;
         val enteringLabel = canPublish
-                ? String.format("publishing command to %d subscriber(s):\n%s",
+                ? String.format("publishing command %s to %d subscriber(s):\n%s",
+                        verb,
                         enabledSubscribers.size(),
                         toText(command))
-                : String.format("not publishing command:\n%s", cannotPublishReason);
+                : String.format("not publishing command %s:\n%s", verb, cannotPublishReason);
 
         val handleIfAny = XrayUtil.createSequenceHandle(iaTracker, "cmd-publisher");
         handleIfAny.ifPresent(handle->{
@@ -85,7 +110,6 @@ final class _Xray {
         });
 
         return handleIfAny.orElse(null);
-
     }
 
     // -- EXECUTION
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 f7bcfafcec..79b517cd9e 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
@@ -19,7 +19,6 @@
 package org.apache.causeway.core.runtimeservices.session;
 
 import java.io.File;
-import java.sql.Timestamp;
 import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
@@ -374,6 +373,7 @@ implements
     }
 
     private void preInteractionClosed(final CausewayInteraction interaction) {
+
         completeAndPublishCurrentCommand();
 
         RuntimeException flushException = null;
@@ -441,7 +441,7 @@ implements
             // the guard is in case we're here as the result of a redirect following a previous exception;just ignore.
 
             val priorInteractionExecution = interaction.getPriorExecution();
-            final Timestamp completedAt =
+            val completedAt =
                     priorInteractionExecution != null
                     ?
                         // copy over from the most recent (which will be the top-level) interaction
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/BackgroundService.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/BackgroundService.java
index 7af674425a..5eba5af1d2 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/BackgroundService.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/BackgroundService.java
@@ -112,7 +112,6 @@ public class BackgroundService {
     public static class PersistCommandExecutorService implements ExecutorService {
 
         @Inject CommandLogEntryRepository<? extends CommandLogEntry> commandLogEntryRepository;
-        @Inject InteractionService interactionService;
 
         private final static JavaSqlJaxbAdapters.TimestampToXMLGregorianCalendarAdapter gregorianCalendarAdapter  = new JavaSqlJaxbAdapters.TimestampToXMLGregorianCalendarAdapter();;
 
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandSubscriberForCommandLog.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandSubscriberForCommandLog.java
index 50f8c56a78..13f48d5b5e 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandSubscriberForCommandLog.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/subscriber/CommandSubscriberForCommandLog.java
@@ -22,6 +22,9 @@ import javax.annotation.Priority;
 import javax.inject.Inject;
 import javax.inject.Named;
 
+import org.apache.causeway.applib.services.clock.ClockService;
+import org.apache.causeway.applib.services.repository.RepositoryService;
+import org.apache.causeway.schema.cmd.v2.CommandDto;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.stereotype.Service;
 
@@ -57,7 +60,9 @@ public class CommandSubscriberForCommandLog implements CommandSubscriber {
     static final String LOGICAL_TYPE_NAME = CausewayModuleExtCommandLogApplib.NAMESPACE + ".CommandSubscriberForCommandLog";
 
     final CommandLogEntryRepository<? extends CommandLogEntry> commandLogEntryRepository;
+    final RepositoryService repositoryService;
     final CausewayConfiguration causewayConfiguration;
+    final ClockService clockService;
 
     @Override
     public boolean isEnabled() {
@@ -65,8 +70,7 @@ public class CommandSubscriberForCommandLog implements CommandSubscriber {
     }
 
     @Override
-    public void onCompleted(final Command command) {
-
+    public void onReady(Command command) {
         if (!isEnabled()) {
             return;
         }
@@ -74,6 +78,7 @@ public class CommandSubscriberForCommandLog implements CommandSubscriber {
         val existingCommandLogEntryIfAny =
                 commandLogEntryRepository.findByInteractionId(command.getInteractionId());
         if(existingCommandLogEntryIfAny.isPresent()) {
+
             val commandLogEntry = existingCommandLogEntryIfAny.get();
             switch (commandLogEntry.getExecuteIn()) {
                 case FOREGROUND:
@@ -95,10 +100,80 @@ public class CommandSubscriberForCommandLog implements CommandSubscriber {
                     // need to do anything else.
                     break;
             }
+
         } else {
-            val parentInteractionId = command.getParentInteractionId();
+            val parentInteractionId = command.getParentInteractionId(); // will be null in most (all?) cases
             commandLogEntryRepository.createEntryAndPersist(command, parentInteractionId, ExecuteIn.FOREGROUND);
         }
+
+    }
+
+    @Override
+    public void onStarted(Command command) {
+        if (!isEnabled()) {
+            return;
+        }
+
+        val existingCommandLogEntryIfAny =
+                commandLogEntryRepository.findByInteractionId(command.getInteractionId());
+        existingCommandLogEntryIfAny.ifPresent(commandLogEntry -> {
+            commandLogEntry.setStartedAt(clockService.getClock().nowAsJavaSqlTimestamp());
+        });
+
+    }
+
+    @Override
+    public void onCompleted(final Command command) {
+
+        if (!isEnabled()) {
+            return;
+        }
+
+        val existingCommandLogEntryIfAny =
+                commandLogEntryRepository.findByInteractionId(command.getInteractionId());
+
+        val onlyIfSystemChanged = causewayConfiguration.getExtensions().getCommandLog().getPublishPolicy().isOnlyIfSystemChanged();
+        if (onlyIfSystemChanged && !command.isSystemStateChanged()) {
+
+            // we don't need the CommandLogEntry after all.
+            existingCommandLogEntryIfAny.ifPresent(repositoryService::remove);
+
+        } else {
+
+            existingCommandLogEntryIfAny.ifPresent(commandLogEntry -> {
+                CommandDto commandDto = commandLogEntry.getCommandDto();
+                commandLogEntry
+                        .setResult(null);
+            });
+            if(existingCommandLogEntryIfAny.isPresent()) {
+                val commandLogEntry = existingCommandLogEntryIfAny.get();
+                switch (commandLogEntry.getExecuteIn()) {
+                    case FOREGROUND:
+                        // this isn't expected to happen ... we just log the fact if it does
+                        if(log.isWarnEnabled()) {
+                            val existingCommandDto = existingCommandLogEntryIfAny.get().getCommandDto();
+
+                            val existingCommandDtoXml = Try.call(()->CommandDtoUtils.dtoMapper().toString(existingCommandDto))
+                                    .getValue().orElse("Dto to Xml failure");
+                            val commandDtoXml = Try.call(()->CommandDtoUtils.dtoMapper().toString(command.getCommandDto()))
+                                    .getValue().orElse("Dto to Xml failure");
+
+                            log.warn("existing: \n{}", existingCommandDtoXml);
+                            log.warn("proposed: \n{}", commandDtoXml);
+                        }
+                        break;
+                    case BACKGROUND:
+                        // this is expected behaviour; the command was already persisted when initially scheduled; we don't
+                        // need to do anything else.
+                        break;
+                }
+            } else {
+                val parentInteractionId = command.getParentInteractionId();
+                commandLogEntryRepository.createEntryAndPersist(command, parentInteractionId, ExecuteIn.FOREGROUND);
+            }
+
+        }
+
     }
 
 }
diff --git a/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/publishing/subscriber/CommandSubscriberForTesting.java b/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/publishing/subscriber/CommandSubscriberForTesting.java
index 6f78b4d89f..8940bc1d53 100644
--- a/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/publishing/subscriber/CommandSubscriberForTesting.java
+++ b/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/publishing/subscriber/CommandSubscriberForTesting.java
@@ -53,17 +53,32 @@ implements CommandSubscriber {
         log.info("about to initialize");
     }
 
+    @Override
+    public void onReady(Command command) {
+        on("readyCommands", command);
+        log.debug("publish ready command {}", ()->command.getCommandDto());
+    }
+
+    @Override
+    public void onStarted(Command command) {
+        on("startedCommands", command);
+        log.debug("publish started command {}", ()->command.getCommandDto());
+    }
+
     @Override
     public void onCompleted(Command command) {
+        on("completedCommands", command);
+        log.debug("publish completed command {}", ()->command.getCommandDto());
+    }
+
 
+    private void on(String verb, Command command) {
         @SuppressWarnings("unchecked")
-        val publishedCommands =
-        (List<Command>) kvStore.get(this, "publishedCommands").orElseGet(ArrayList::new);
+        val commands = (List<Command>) kvStore.get(this, verb).orElseGet(ArrayList::new);
 
-        publishedCommands.add(command);
+        commands.add(command);
 
-        kvStore.put(this, "publishedCommands", publishedCommands);
-        log.debug("publish command {}", ()->command.getCommandDto());
+        kvStore.put(this, verb, commands);
     }
 
     // -- UTILITIES
@@ -71,7 +86,7 @@ implements CommandSubscriber {
     @SuppressWarnings("unchecked")
     public static Can<Command> getPublishedCommands(KVStoreForTesting kvStore) {
         return Can.ofCollection(
-                (List<Command>) kvStore.get(CommandSubscriberForTesting.class, "publishedCommands")
+                (List<Command>) kvStore.get(CommandSubscriberForTesting.class, "completedCommands")
                 .orElse(null));
     }