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

danhaywood pushed a commit to branch ISIS-3267
in repository https://gitbox.apache.org/repos/asf/isis.git

commit d7985998534a892a66140969dacfdc0ebe8833b5
Author: Dan Haywood <[email protected]>
AuthorDate: Wed Nov 2 18:53:18 2022 +0000

    ISIS-3267 : adds BackgroundService
    
    also changes design of CommandLogEntry, persist parentInteractionId rather 
than parent reference, and reinstate executeIn enum attribute
---
 .../causeway/applib/services/command/Command.java  |   2 +-
 .../adoc/modules/ROOT/partials/module-nav.adoc     |   8 +-
 .../adoc/modules/commandlog/pages/about.adoc       | 247 +++++++++++++++++++-
 extensions/core/commandlog/applib/pom.xml          |   4 +
 .../applib/CausewayModuleExtCommandLogApplib.java  |   8 +
 .../commandlog/applib/dom/BackgroundService.java   | 248 +++++++++++++++++++++
 .../commandlog/applib/dom/CommandLogEntry.java     |  93 ++++++--
 .../applib/dom/CommandLogEntryRepository.java      |  22 +-
 .../commandlog/applib/dom/ExecuteIn.java           |  37 +++
 .../applib/job/RunBackgroundCommandsJob.java       |  95 ++++++++
 .../subscriber/CommandSubscriberForCommandLog.java |  40 ++--
 .../BackgroundService_IntegTestAbstract.java       | 224 +++++++++++++++++++
 .../commandlog/jdo/dom/CommandLogEntry.java        |  39 ++--
 ...{CommandLog_IntegTest.java => AppManifest.java} |  55 ++---
 ...gTest.java => BackgroundService_IntegTest.java} |  37 +--
 .../jdo/integtests/CommandLog_IntegTest.java       |  29 +--
 .../commandlog/jpa/dom/CommandLogEntry.java        |  64 ++----
 ...{CommandLog_IntegTest.java => AppManifest.java} |  56 ++---
 ...gTest.java => BackgroundService_IntegTest.java} |  38 +---
 .../jpa/integtests/CommandLog_IntegTest.java       |  30 +--
 extensions/core/commandlog/pom.xml                 |  30 +++
 .../applib/CausewayInteractionHandler.java         |   1 +
 22 files changed, 1099 insertions(+), 308 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 e89a0257a6..6cc380de18 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
@@ -295,7 +295,7 @@ public class Command implements HasInteractionId, 
HasUsername, HasCommandDto {
             val dtoInteractionId = commandDto.getInteractionId();
 
             if(!commandInteractionId.equals(dtoInteractionId)) {
-                log.warn("setting CommandDto on a Command has side-effects if "
+                log.debug("setting CommandDto on a Command has side-effects if 
"
                         + "their InteractionIds don't match; forcing 
CommandDto's Id to be same as Command's");
                 commandDto.setInteractionId(commandInteractionId);
             }
diff --git a/extensions/adoc/modules/ROOT/partials/module-nav.adoc 
b/extensions/adoc/modules/ROOT/partials/module-nav.adoc
index bd02e417f2..8438fccb95 100644
--- a/extensions/adoc/modules/ROOT/partials/module-nav.adoc
+++ b/extensions/adoc/modules/ROOT/partials/module-nav.adoc
@@ -1,21 +1,21 @@
 
-* Core:
+* Core
 
 
 include::userguide:ROOT:partial$extensions.adoc[]
 
-* Security:
+* Security
 
 include::security:ROOT:partial$extensions.adoc[]
 
 
 
-* Restful Objects:
+* Restful Objects
 
 include::vro:ROOT:partial$extensions.adoc[]
 
 
-* Wicket viewer:
+* Wicket viewer
 
 include::vw:ROOT:partial$extensions.adoc[]
 
diff --git 
a/extensions/core/commandlog/adoc/modules/commandlog/pages/about.adoc 
b/extensions/core/commandlog/adoc/modules/commandlog/pages/about.adoc
index 0a5375734d..f9ad99ca03 100644
--- a/extensions/core/commandlog/adoc/modules/commandlog/pages/about.adoc
+++ b/extensions/core/commandlog/adoc/modules/commandlog/pages/about.adoc
@@ -4,9 +4,252 @@
 :Notice: Licensed to the Apache Software Foundation (ASF) under one or more 
contributor license agreements. See the NOTICE file distributed with this work 
for additional information regarding copyright ownership. The ASF licenses this 
file to you under the Apache License, Version 2.0 (the "License"); you may not 
use this file except in compliance with the License. You may obtain a copy of 
the License at. http://www.apache.org/licenses/LICENSE-2.0 . Unless required by 
applicable law or ag [...]
 
 
-The _commandlog_ module provides an implementation that persists 
xref:refguide:applib:index/services/command/Command.adoc[Command]s using either 
the xref:pjpa:ROOT:about.adoc[JPA/EclipseLink] or 
xref:pjdo:ROOT:about.adoc[JDO/DataNucleus] object store.
+The _commandlog_ module provides an implementation of 
xref:refguide:applib:index/services/publishing/spi/CommandSubscriber.adoc[] SPI 
that persists 
xref:refguide:applib:index/services/command/Command.adoc[Command]s using either 
the xref:pjpa:ROOT:about.adoc[JPA/EclipseLink] or 
xref:pjdo:ROOT:about.adoc[JDO/DataNucleus] object store.
+This can be useful for auditing.
+
+
+
+include::docs:mavendeps:partial$setup-and-configure-dependencyManagement.adoc[leveloffset=+1]
+
+In addition, add a section for secman's own BOM:
+
+[source,xml,subs="attributes+"]
+.pom.xml
+----
+<dependencyManagement>
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.causeway.extensions</groupId>
+            <artifactId>causeway-extensions-commandlog</artifactId>
+            <scope>import</scope>
+            <type>pom</type>
+            <version>{page-causewayrel}</version>
+        </dependency>
+    </dependencies>
+</dependencyManagement>
+----
+
+[#dependencies]
+== Dependencies
+
+In the webapp module of your application, add the following dependency:
+
+[source,xml]
+.pom.xml
+----
+<dependencies>
+    <dependency>
+        <groupId>org.apache.causeway.extensions</groupId>
+        
<artifactId>causeway-extensions-commandlog-persistence-XXX</artifactId>    
<!--.-->
+    </dependency>
+</dependencies>
+----
+<.> specify either `causeway-extensions-commandlog-persistence-jpa` or 
`causeway-extensions-commandlog-persistence-jdo`, as required
+
+
+[[_update-appmanifest]]
+== Update AppManifest
+
+
+In your application's `AppManifest` (top-level Spring `@Configuration` used to 
bootstrap the app), import the CommandLog modules.
+You will also need to import the fixture module; SecMan uses fixture scripts 
to seed its entities:
+
+[source,java]
+.AppManifest.java
+----
+@Configuration
+@Import({
+        ...
+        CausewayModuleExtCommandLogPersistenceXxx.class,        // <.>
+        ...
+})
+public class AppManifest {
+}
+----
+
+<.> specify either `CausewayModuleExtCommandLogPersistenceJdo` or 
`CausewayModuleExtCommandLogPersistenceJpa`, as required
+
+
+[#configure-properties]
+== Configuration Properties
+
+Add the database schema used by the CommandLog entities to the configuration 
file:
+
+[source,yaml]
+.application.yml
+----
+causeway:
+  persistence:
+    schema:
+      auto-create-schemas: causewayExtCommandLog
+----
+
+Optionally, modify the configuration properties for CommandLog itself:
+
+[source,yaml]
+.application.yml
+----
+causeway:
+  extensions:
+    command-log:
+      publish-policy: "always"    # <.>
+----
+
+
+<.> the alternative is `"only-if-system-changed"`, which suppresses the 
persisting of `CommandLogEntry`s for commands where no other system state was 
changed (for example a finder action with safe semantics).
++
+See 
xref:refguide:config:sections/causeway.extensions.adoc#causeway.extensions.command-log.publish-policy[causeway.extensions.command-log.publish-policy]
 configuration property for more details.
+
+
+== Background Commands
+
+Sometimes we might want to execute an action not immediately in the current 
users's thread of control, but instead to perform it in the background; for 
example any long-running process.
+
+One way to accomplish this is to use 
xref:refguide:applib:index/services/wrapper/WrapperFactory.adoc#asyncWrap_T_AsyncControl[WrapperFactory#asyncWrap(...)],
 where the command is executed by another thread obtained from a thread pool 
(`ForkJoinPool.commonPool()`).
+This works well, but has the slight risk that it is not transactionally safe - 
the async thread executes in its own interaction/transaction, and so might fail 
even though the initiating command succeeds; or vice versa.
+
+An alternative approach is to use the 
xref:refguide:extensions:index/commandlog/applib/dom/BackgroundService.adoc[BackgroundService].
+This persists the command as an `CommandLogEntry` instance, indicating that it 
is to be executed in the background.
+Then, a separate thread - eg scheduling using Quartz - can pick up the queued 
`CommandLogEntry` and execute it.
+
+=== Submitting Actions
+
+For example, suppose we have a long-running action to export all the invoices 
we have received from a supplier, perhaps to be sent to some other system.
+Assuming that the `exportInvoices()` action is a regular action on the 
`Supplier` domain class, we would use:
+
+[source,java]
+.example usage of `BackgroundService` to invoke a regular action
+----
+@Action
+public void exportInvoices(Supplier supplier) {
+    backgroundService.execute(supplier).exportInvoices();
+}
+----
+
+If instead this functionality is implemented as a mixin, we would use 
something like:
+
+[source,java]
+.example usage of `BackgroundService` to invoke a mixin action:
+----
+@Action
+public void exportInvoices(Supplier supplier) {
+    backgroundService.executeMixin(Supplier_exportInvoices.class, 
supplier).act();
+}
+----
+
+The action being invoked must be part of the Causeway metamodel, in other 
words it cannot be marked uses
+xref:refguide:applib:index/annotation/Programmatic.adoc[] or 
xref:refguide:applib:index/annotation/Domain_Exclude.adoc[].
+
+By default all the usual hide/disable/validate rules will be checked, but 
there are also methods to allow these rules to be skipped.
+
+Behind the scenes this service uses 
xref:refguide:applib:index/services/wrapper/WrapperFactory.adoc#asyncWrap_T_AsyncControl[WrapperFactory#asyncWrap(...)]
 using 
xref:refguide:applib:index/services/wrapper/control/AsyncControl.adoc#with_ExecutorService[AsyncControl#with(ExecutorService)]
 to pass an implementation of `ExecutorService` that persists the command as a 
`CommandLogEntry` instance.
+
+If you require more fine-grained control, you can always just use the 
xref:refguide:applib:index/services/wrapper/WrapperFactory.adoc[WrapperFactory] 
async method yourself.
+The `ExecutorService` to use is 
xref:refguide:extensions:index/commandlog/applib/dom/BackgroundService_PersistCommandExecutorService.adoc[BackgroundService.PersistCommandExecutorService].
+This is a Spring `@Service` and so can be obtained through injection.
+
+=== Executing Actions using the Quartz scheduler
+
+Once a command has been persisted as a `CommandLogEntry`, we require some 
other process to actually execute the command.
+The _commandlog_ module includes the `RunBackgroundCommandsJob`, a 
https://www.quartz-scheduler.org/[Quartz] job that does exactly this.
+Each time it is called it will query for any background commands that have not 
been started, and will execute each (using the 
xref:refguide:applib:index/services/command/CommandExecutorService.adoc[CommandExecutorService]).
+
+The job is marked as non re-entrant, so it doesn't matter how often it is 
called; we recommend a 10 second delay usually works fine.
+
+To configure Quartz, add the following to your `AppManifest`:
+
+[source,java]
+.AppManifest.java
+----
+public class AppManifest {
+
+    @Bean(name = "RunBackgroundCommandsJob")                                // 
<.>
+    public JobDetailFactoryBean jobDetail() {
+        val jobDetailFactory = new JobDetailFactoryBean();
+        jobDetailFactory.setJobClass(RunBackgroundCommandsJob.class);
+        jobDetailFactory.setDurability(true);
+        return jobDetailFactory;
+    }
+
+    @Bean
+    public SimpleTriggerFactoryBean trigger(
+            final @Qualifier("RunBackgroundCommandsJob") JobDetail job) {   // 
<1>
+        val trigger = new SimpleTriggerFactoryBean();
+        trigger.setJobDetail(job);
+        trigger.setStartDelay(60_000);                                      // 
<.>
+        trigger.setRepeatInterval(10_000);                                  // 
<.>
+        trigger.setRepeatCount(REPEAT_INDEFINITELY);
+        return trigger;
+    }
+
+    // ...
+}
+----
+
+<.> name and qualify the job (so will not interfere with any other Quartz jobs 
you may have defined)
+<.> 60 secs to wait for the app to be ready
+<.> check every 10 seconds
+
+
+
+==== Disabling Quartz
+
+The _commandlog_ module automatically references the 
https://www.quartz-scheduler.org/[Quartz] library.
+If you don't want to use this functionality and want to exclude quartz, then 
add an explicit dependency on the _commandlog_ applib but exclude the quartz 
dependency within it:
+
+[source,xml]
+.pom.xml
+----
+<dependencies>
+    <dependency>
+        <groupId>org.apache.causeway.extensions</groupId>
+        <artifactId>causeway-extensions-commandlog-applib</artifactId>
+        <exclusions>
+            <exclusion>
+                <groupId>org.quartz-scheduler</groupId>                 
<!--.-->
+                <artifactId>quartz</artifactId>
+            </exclusion>
+        </exclusions>
+    </dependency>
+</dependencies>
+----
+<.> exclude reference to quartz
+
+
+
+== Notes
+
+Conceptually a *command* represents the _intention_ to execute an action or to 
edit a property ("before" the change), while an *interaction execution* 
represents the actual execution itself ("after" the change).
+
+The 
xref:refguide:applib:index/services/publishing/spi/CommandSubscriber.adoc[] SPI 
and 
xref:refguide:applib:index/services/publishing/spi/ExecutionSubscriber.adoc[] 
SPI allow either to be subscribed to.
+From an auditing perspective, their behaviour is quite similar:
+
+* even though a command represents the _intention_ to invoke an action, its 
xref:refguide:applib:index/services/publishing/spi/CommandSubscriber.adoc[CommandSubscriber]
 SPI is only called once the action/property edit has been completed.
+
+* the 
xref:refguide:applib:index/services/publishing/spi/ExecutionSubscriber.adoc[] 
is called as soon as the action has completed.
+In most interactions there will only be a single action called within the 
interaction, hence these two subscribers will be called at almost the same time 
with very similar payloads.
+
+However, there can be some subtle differences:
+
+* the xref:refguide:applib:index/services/wrapper/WrapperFactory.adoc[] 
service allows actions to be invoked "as if" through the user interface.
+Therefore one action can execute another can execute another, creating a 
nested call graph of executions.
++
+The  
xref:refguide:applib:index/services/publishing/spi/ExecutionSubscriber.adoc[] 
is called after each and every execution as it completes, so will be called 
several times.
+
+* In contrast, the 
xref:refguide:applib:index/services/publishing/spi/CommandSubscriber.adoc[CommandSubscriber]
 is called only once, for the top-level (outermost) action.
+
+
+
+
+== See also
+
+* xref:refguide:applib:index/services/wrapper/WrapperFactory.adoc[] service
+* 
xref:refguide:extensions:index/commandlog/applib/dom/BackgroundService.adoc[BackgroundService]
 service
+* xref:refguide:applib:index/services/publishing/spi/CommandSubscriber.adoc[] 
SPI
+* 
xref:refguide:applib:index/services/publishing/spi/ExecutionSubscriber.adoc[] 
SPI
+* xref:executionlog:about.adoc[] extension
+
 
-WARNING: TODO: v2 - to write up...
 
 
 
diff --git a/extensions/core/commandlog/applib/pom.xml 
b/extensions/core/commandlog/applib/pom.xml
index 045a5ed6be..cb17f38f2b 100644
--- a/extensions/core/commandlog/applib/pom.xml
+++ b/extensions/core/commandlog/applib/pom.xml
@@ -68,6 +68,10 @@
             <artifactId>causeway-core-runtimeservices</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>org.quartz-scheduler</groupId>
+            <artifactId>quartz</artifactId>
+        </dependency>
 
         <!-- TESTING -->
 
diff --git 
a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/CausewayModuleExtCommandLogApplib.java
 
b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/CausewayModuleExtCommandLogApplib.java
index 368b07b936..96843e6c1e 100644
--- 
a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/CausewayModuleExtCommandLogApplib.java
+++ 
b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/CausewayModuleExtCommandLogApplib.java
@@ -18,6 +18,8 @@
  */
 package org.apache.causeway.extensions.commandlog.applib;
 
+import org.apache.causeway.extensions.commandlog.applib.dom.BackgroundService;
+import 
org.apache.causeway.extensions.commandlog.applib.job.RunBackgroundCommandsJob;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Import;
 
@@ -45,9 +47,15 @@ import 
org.apache.causeway.extensions.commandlog.applib.subscriber.CommandSubscr
         CommandLogEntry_openResultObject.class,
         CommandLogEntry_siblingCommands.class,
 
+        // @Component's
+        RunBackgroundCommandsJob.class,
+
         // @Service's
         CommandSubscriberForCommandLog.class,
         CommandLogEntry.TableColumnOrderDefault.class,
+
+        BackgroundService.class,
+        BackgroundService.PersistCommandExecutorService.class,
 })
 public class CausewayModuleExtCommandLogApplib {
 
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
new file mode 100644
index 0000000000..088edc8d58
--- /dev/null
+++ 
b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/BackgroundService.java
@@ -0,0 +1,248 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.extensions.commandlog.applib.dom;
+
+import lombok.val;
+
+import java.sql.Timestamp;
+import java.util.Collection;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.*;
+
+import javax.inject.Inject;
+
+import org.apache.causeway.applib.jaxb.JavaSqlJaxbAdapters;
+import org.apache.causeway.applib.services.bookmark.Bookmark;
+import org.apache.causeway.applib.services.command.Command;
+import org.apache.causeway.applib.services.iactnlayer.InteractionService;
+import org.apache.causeway.applib.services.wrapper.WrapperFactory;
+import org.apache.causeway.applib.services.wrapper.callable.AsyncCallable;
+import org.apache.causeway.applib.services.wrapper.control.AsyncControl;
+import org.apache.causeway.schema.cmd.v2.CommandDto;
+import org.apache.causeway.schema.common.v2.PeriodDto;
+import org.springframework.stereotype.Service;
+
+/**
+ * Allows the execution of action invocations or property edits to be deferred 
so that they can be executed later in
+ * another thread of execution.
+ *
+ * <p>
+ *     Typically this other thread of execution would be scheduled from quartz 
or similar.  The
+ *     {@link 
org.apache.causeway.extensions.commandlog.applib.job.RunBackgroundCommandsJob} 
provides a ready-made
+ *     implementation to do this for quartz.
+ * </p>
+ *
+ * @see WrapperFactory
+ * @since 2.0 {@index}
+ */
+@Service
+public class BackgroundService {
+
+    @Inject WrapperFactory wrapperFactory;
+    @Inject PersistCommandExecutorService persistCommandExecutorService;
+
+    /**
+     * Wraps the domain object in a proxy whereby any actions invoked through 
the proxy will instead be persisted as a
+     * {@link ExecuteIn#BACKGROUND background} {@link CommandLogEntry command 
log entry}.
+     *
+     * @see #executeMixin(Class, Object) - to invoke actions that are 
implemented as mixins
+     */
+    public <T> T execute(T object) {
+        return wrapperFactory.asyncWrap(object, 
AsyncControl.returningVoid().withCheckRules()
+                .with(persistCommandExecutorService)
+        );
+    }
+    /**
+     * Wraps the domain object in a proxy whereby any actions invoked through 
the proxy will instead be persisted as a
+     * {@link ExecuteIn#BACKGROUND background} {@link CommandLogEntry command 
log entry}.
+     *
+     * @see #executeMixin(Class, Object) - to invoke actions that are 
implemented as mixins
+     */
+    public <T> T executeSkipRules(T object) {
+        return wrapperFactory.asyncWrap(object, 
AsyncControl.returningVoid().withSkipRules()
+                .with(persistCommandExecutorService)
+        );
+    }
+
+    /**
+     * Wraps a mixin object in a proxy whereby invoking that mixin will 
instead be persisted as a
+     * {@link ExecuteIn#BACKGROUND background} {@link CommandLogEntry command 
log entry}.
+     *
+     * @see #execute(Object) - to invoke actions that are implemented directly 
within the object
+     */
+    public <T> T executeMixin(Class<T> mixinClass, Object mixedIn) {
+        return wrapperFactory.asyncWrapMixin(mixinClass, mixedIn, 
AsyncControl.returningVoid().withCheckRules()
+                .with(persistCommandExecutorService)
+        );
+    }
+
+    /**
+     * Wraps a mixin object in a proxy whereby invoking that mixin will 
instead be persisted as a
+     * {@link ExecuteIn#BACKGROUND background} {@link CommandLogEntry command 
log entry}.
+     *
+     * @see #execute(Object) - to invoke actions that are implemented directly 
within the object
+     */
+    public <T> T executeMixinSkipRules(Class<T> mixinClass, Object mixedIn) {
+        return wrapperFactory.asyncWrapMixin(mixinClass, mixedIn, 
AsyncControl.returningVoid().withSkipRules()
+                .with(persistCommandExecutorService)
+        );
+    }
+
+    @Service
+    public static class PersistCommandExecutorService implements 
ExecutorService {
+
+        @Inject CommandLogEntryRepository<? extends CommandLogEntry> 
commandLogEntryRepository;
+        @Inject InteractionService interactionService;
+
+        private final static 
JavaSqlJaxbAdapters.TimestampToXMLGregorianCalendarAdapter 
gregorianCalendarAdapter  = new 
JavaSqlJaxbAdapters.TimestampToXMLGregorianCalendarAdapter();;
+
+        @Override
+        public <T> Future<T> submit(Callable<T> task) {
+            val callable = (AsyncCallable<T>) task;
+            val commandDto = callable.getCommandDto();
+
+            // we'll mutate the commandDto in line with the callable, then
+            // create the CommandLogEntry from that commandDto
+            val childInteractionId = UUID.randomUUID();
+            commandDto.setInteractionId(childInteractionId.toString());
+
+            // copy details from requested interaction context into the 
commandDto
+            val interactionContext = callable.getInteractionContext();
+            val timestamp = 
interactionContext.getClock().nowAsJavaSqlTimestamp();
+            
commandDto.setTimestamp(gregorianCalendarAdapter.marshal(timestamp));
+
+            val username = interactionContext.getUser().getName();
+            commandDto.setUsername(username);
+
+            val periodDto = new PeriodDto();
+            periodDto.setStartedAt(null);
+            periodDto.setCompletedAt(null);
+            commandDto.setTimings(periodDto);
+
+            val childCommand = newCommand(commandDto);
+
+            commandLogEntryRepository.createEntryAndPersist(childCommand, 
callable.getParentInteractionId(), ExecuteIn.BACKGROUND);
+
+            // a more sophisticated implementation could perhaps return a 
Future that supports these methods by
+            // querying the CommandLogEntryRepository
+            return new Future<T>() {
+                @Override
+                public boolean cancel(boolean mayInterruptIfRunning) {
+                    throw new IllegalStateException("Not implemented");
+                }
+                @Override
+                public boolean isCancelled() {
+                    throw new IllegalStateException("Not implemented");
+                }
+
+                @Override
+                public boolean isDone() {
+                    throw new IllegalStateException("Not implemented");
+                }
+
+                @Override
+                public T get() {
+                    throw new IllegalStateException("Not implemented");
+                }
+
+                @Override
+                public T get(long timeout, TimeUnit unit) {
+                    throw new IllegalStateException("Not implemented");
+                }
+            };
+        }
+
+        private static Command newCommand(CommandDto commandDto) {
+            return new Command(UUID.fromString(commandDto.getInteractionId())) 
{
+                @Override public String getUsername() {return 
commandDto.getUsername();}
+                @Override public Timestamp getTimestamp() {return 
gregorianCalendarAdapter.unmarshal(commandDto.getTimestamp());}
+                @Override public CommandDto getCommandDto() {return 
commandDto;}
+                @Override public String getLogicalMemberIdentifier() {return 
commandDto.getMember().getLogicalMemberIdentifier();}
+                @Override public Bookmark getTarget() {return 
Bookmark.forOidDto(commandDto.getTargets().getOid().get(0));}
+                @Override public Timestamp getStartedAt() {return 
gregorianCalendarAdapter.unmarshal(commandDto.getTimings().getStartedAt());}
+                @Override public Timestamp getCompletedAt() {return 
gregorianCalendarAdapter.unmarshal(commandDto.getTimings().getCompletedAt());}
+                @Override public Bookmark getResult() {return null;}
+                @Override public Throwable getException() {return null;}
+            };
+        }
+
+
+        @Override
+        public <T> Future<T> submit(Runnable task, T result) {
+            throw new IllegalStateException("Not implemented");
+        }
+
+        @Override
+        public Future<?> submit(Runnable task) {
+            throw new IllegalStateException("Not implemented");
+        }
+
+        @Override
+        public void execute(Runnable command) {
+            throw new IllegalStateException("Not implemented");
+        }
+
+        @Override
+        public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> 
tasks) {
+            throw new IllegalStateException("Not implemented");
+        }
+
+        @Override
+        public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> 
tasks, long timeout, TimeUnit unit) throws InterruptedException {
+            throw new IllegalStateException("Not implemented");
+        }
+
+        @Override
+        public <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws 
InterruptedException, ExecutionException {
+            throw new IllegalStateException("Not implemented");
+        }
+
+        @Override
+        public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long 
timeout, TimeUnit unit) throws InterruptedException, ExecutionException, 
TimeoutException {
+            throw new IllegalStateException("Not implemented");
+        }
+
+        @Override
+        public void shutdown() {
+            throw new IllegalStateException("Not implemented");
+        }
+
+        @Override
+        public List<Runnable> shutdownNow() {
+            throw new IllegalStateException("Not implemented");
+        }
+
+        @Override
+        public boolean awaitTermination(long timeout, TimeUnit unit) throws 
InterruptedException {
+            throw new IllegalStateException("Not implemented");
+        }
+
+        @Override
+        public boolean isShutdown() {
+            return false;
+        }
+
+        @Override
+        public boolean isTerminated() {
+            return false;
+        }
+
+    }
+}
diff --git 
a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntry.java
 
b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntry.java
index f21c15e2eb..297653ee9f 100644
--- 
a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntry.java
+++ 
b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntry.java
@@ -23,31 +23,18 @@ import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.math.BigDecimal;
 import java.time.format.DateTimeFormatter;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Objects;
-import java.util.UUID;
+import java.util.*;
 import java.util.function.Consumer;
 
 import javax.annotation.Priority;
+import javax.inject.Inject;
 import javax.inject.Named;
 import javax.validation.constraints.Digits;
 
+import org.apache.causeway.applib.annotation.*;
+import org.apache.causeway.commons.internal.base._Casts;
 import org.springframework.stereotype.Service;
 
-import org.apache.causeway.applib.annotation.DomainObject;
-import org.apache.causeway.applib.annotation.DomainObjectLayout;
-import org.apache.causeway.applib.annotation.Editing;
-import org.apache.causeway.applib.annotation.MemberSupport;
-import org.apache.causeway.applib.annotation.ObjectSupport;
-import org.apache.causeway.applib.annotation.Optionality;
-import org.apache.causeway.applib.annotation.Parameter;
-import org.apache.causeway.applib.annotation.PriorityPrecedence;
-import org.apache.causeway.applib.annotation.Programmatic;
-import org.apache.causeway.applib.annotation.Property;
-import org.apache.causeway.applib.annotation.PropertyLayout;
-import org.apache.causeway.applib.annotation.Publishing;
-import org.apache.causeway.applib.annotation.Where;
 import org.apache.causeway.applib.jaxb.JavaSqlXMLGregorianCalendarMarshalling;
 import org.apache.causeway.applib.mixins.system.DomainChangeRecord;
 import org.apache.causeway.applib.mixins.system.HasInteractionId;
@@ -70,6 +57,7 @@ import org.apache.causeway.schema.cmd.v2.MapDto;
 
 import lombok.NoArgsConstructor;
 import lombok.experimental.UtilityClass;
+import lombok.val;
 
 /**
  * A persistent representation of a {@link Command}, being the intention to 
edit a property or invoke an action.
@@ -114,7 +102,7 @@ implements Comparable<CommandLogEntry>, DomainChangeRecord, 
HasCommandDto {
     @UtilityClass
     public static class Nq {
         public static final String FIND_BY_INTERACTION_ID = LOGICAL_TYPE_NAME 
+ ".findByInteractionId";
-        public static final String FIND_BY_PARENT = LOGICAL_TYPE_NAME + 
".findByParent";
+        public static final String FIND_BY_PARENT_INTERACTION_ID = 
LOGICAL_TYPE_NAME + ".findByParentInteractionId";
         public static final String FIND_CURRENT = LOGICAL_TYPE_NAME + 
".findCurrent";
         public static final String FIND_COMPLETED = LOGICAL_TYPE_NAME + 
".findCompleted";
         public static final String FIND_RECENT_BY_TARGET = LOGICAL_TYPE_NAME + 
".findRecentByTarget";
@@ -131,10 +119,21 @@ implements Comparable<CommandLogEntry>, 
DomainChangeRecord, HasCommandDto {
         public static final String FIND_RECENT_BY_USERNAME = LOGICAL_TYPE_NAME 
+ ".findRecentByUsername";
         public static final String FIND_FIRST = LOGICAL_TYPE_NAME + 
".findFirst";
         public static final String FIND_SINCE = LOGICAL_TYPE_NAME + 
".findSince";
+        /**
+         * The most recent (replayed) command previously replicated from 
primary to secondary.
+         *
+         * <p>
+         *     This should always exist except for the very first times (after 
restored the prod DB to secondary).
+         * </p>
+         */
         public static final String FIND_MOST_RECENT_REPLAYED = 
LOGICAL_TYPE_NAME + ".findMostRecentReplayed";
+        /**
+         * The most recent completed command, as queried on the secondary, 
corresponding to the last command run on
+         * primary before the production database was restored to the 
secondary.
+         */
         public static final String FIND_MOST_RECENT_COMPLETED = 
LOGICAL_TYPE_NAME + ".findMostRecentCompleted";
         public static final String FIND_BY_REPLAY_STATE = LOGICAL_TYPE_NAME + 
".findNotYetReplayed";
-        public static final String FIND_NOT_YET_STARTED = "findNotYetStarted";
+        public static final String FIND_BACKGROUND_AND_NOT_YET_STARTED = 
"findBackgroundAndNotYetStarted";
     }
 
 
@@ -179,8 +178,8 @@ implements Comparable<CommandLogEntry>, DomainChangeRecord, 
HasCommandDto {
         
setTarget(Bookmark.forOidDto(commandDto.getTargets().getOid().get(targetIndex)));
         
setLogicalMemberIdentifier(commandDto.getMember().getLogicalMemberIdentifier());
 
-        // the hierarchy of commands calling other commands is only available 
on the primary system, and is
-        setParent(null);
+        // the hierarchy of commands calling other commands is only available 
on the primary system.
+        setParentInteractionId(null);
 
         
setStartedAt(JavaSqlXMLGregorianCalendarMarshalling.toTimestamp(commandDto.getTimings().getStartedAt()));
         
setCompletedAt(JavaSqlXMLGregorianCalendarMarshalling.toTimestamp(commandDto.getTimings().getCompletedAt()));
@@ -299,6 +298,46 @@ implements Comparable<CommandLogEntry>, 
DomainChangeRecord, HasCommandDto {
 
 
 
+    @Property(
+            domainEvent = ExecuteIn.DomainEvent.class
+    )
+    @java.lang.annotation.Target({ ElementType.METHOD, ElementType.FIELD, 
ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface ExecuteIn {
+        class DomainEvent extends 
PropertyDomainEvent<org.apache.causeway.extensions.commandlog.applib.dom.ExecuteIn>
 {}
+        int MAX_LENGTH = 10;
+        boolean NULLABLE = true;
+        String ALLOWS_NULL = "true";
+    }
+    /**
+     * Whether the command was executed immediately in the current thread of 
execution, or scheduled to be
+     * executed at some time later in a &quot;background&quot; thread of 
execution.
+     */
+    @ExecuteIn
+    public abstract 
org.apache.causeway.extensions.commandlog.applib.dom.ExecuteIn getExecuteIn();
+    public abstract void 
setExecuteIn(org.apache.causeway.extensions.commandlog.applib.dom.ExecuteIn 
replayState);
+
+
+    /**
+     * The interactionId of the parent command, if any.
+     *
+     * <p>
+     *     We store only the id rather than a reference to the parent, because 
the
+     *     {@link 
org.apache.causeway.extensions.commandlog.applib.subscriber.CommandSubscriberForCommandLog}'s
+     *     callback is only called at the end of the transaction, meaning that 
the {@link CommandLogEntry} of the
+     *     &quot;parent&quot; will be persisted only after any of its child 
background {@link CommandLogEntry}s are
+     *     to be persisted (within the body of the underlying action).
+     * </p>
+     *
+     * @see #getParent()
+     */
+    @Domain.Exclude
+    @InteractionId
+    public abstract UUID getParentInteractionId();
+    public abstract void setParentInteractionId(UUID parentInteractionId);
+
+
+
     @Property(
             domainEvent = Parent.DomainEvent.class,
             optionality = Optionality.OPTIONAL
@@ -318,10 +357,16 @@ implements Comparable<CommandLogEntry>, 
DomainChangeRecord, HasCommandDto {
         String ALLOWS_NULL = "true";
     }
     @Parent
-    public abstract <C extends CommandLogEntry> C getParent();
-    public abstract void setParent(CommandLogEntry parent);
-
+    public <C extends CommandLogEntry> C getParent() {
+        if (getParentInteractionId() == null) {
+            return null;
+        }
+        val parentCommandLogEntryIfAny = 
commandLogEntryRepository.findByInteractionId(getParentInteractionId());
+        val commandLogEntry = parentCommandLogEntryIfAny.orElse(null);
+        return _Casts.uncheckedCast(commandLogEntry);
+    }
 
+    @Inject CommandLogEntryRepository<? extends CommandLogEntry> 
commandLogEntryRepository;
 
 
     @Property(
diff --git 
a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntryRepository.java
 
b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntryRepository.java
index bdb8915b67..8cb4f9f9e0 100644
--- 
a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntryRepository.java
+++ 
b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/CommandLogEntryRepository.java
@@ -78,10 +78,12 @@ public abstract class CommandLogEntryRepository<C extends 
CommandLogEntry> {
         return commandLogEntryClass;
     }
 
-    public C createEntryAndPersist(final Command command, CommandLogEntry 
parentEntryIfAny) {
+    public C createEntryAndPersist(
+            final Command command, final UUID parentInteractionIdIfAny, final 
ExecuteIn executeIn) {
         C c = factoryService.detachedEntity(commandLogEntryClass);
         c.init(command);
-        c.setParent(parentEntryIfAny);
+        c.setParentInteractionId(parentInteractionIdIfAny);
+        c.setExecuteIn(executeIn);
         persist(c);
         return c;
     }
@@ -93,9 +95,13 @@ public abstract class CommandLogEntryRepository<C extends 
CommandLogEntry> {
     }
 
     public List<C> findByParent(final CommandLogEntry parent) {
+        return findByParentInteractionId(parent.getInteractionId());
+    }
+
+    public List<C> findByParentInteractionId(final UUID parentInteractionId) {
         return repositoryService().allMatches(
-                Query.named(commandLogEntryClass, 
CommandLogEntry.Nq.FIND_BY_PARENT)
-                        .withParameter("parent", parent));
+                Query.named(commandLogEntryClass, 
CommandLogEntry.Nq.FIND_BY_PARENT_INTERACTION_ID)
+                        .withParameter("parentInteractionId", 
parentInteractionId));
     }
 
     public List<C> findByFromAndTo(
@@ -256,7 +262,7 @@ public abstract class CommandLogEntryRepository<C extends 
CommandLogEntry> {
     }
 
     /**
-     * Returns any parented commands that have not yet started.
+     * Returns any persisted commands that have not yet started.
      *
      * <p>
      * This is to support the notion of background commands (the same as their 
implementation in v1) whereby a
@@ -265,9 +271,9 @@ public abstract class CommandLogEntryRepository<C extends 
CommandLogEntry> {
      * quartz or similar background job could execute the {@link Command} at 
some point later.
      * </p>
      */
-    public Optional<C> findParentedCommandsNotYetStarted() {
-        return repositoryService().firstMatch(
-                Query.named(commandLogEntryClass, 
CommandLogEntry.Nq.FIND_NOT_YET_STARTED));
+    public List<C> findBackgroundAndNotYetStarted() {
+        return repositoryService().allMatches(
+                Query.named(commandLogEntryClass, 
CommandLogEntry.Nq.FIND_BACKGROUND_AND_NOT_YET_STARTED));
     }
 
     /**
diff --git 
a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/ExecuteIn.java
 
b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/ExecuteIn.java
new file mode 100644
index 0000000000..06253fcfa3
--- /dev/null
+++ 
b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/ExecuteIn.java
@@ -0,0 +1,37 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.extensions.commandlog.applib.dom;
+
+/**
+ * Whether the command is executed explicitly by the end-user, or is scheduled 
(for example, using the
+ * {@link BackgroundService}) to be executed asynchronously at some later time.
+ *
+ * @since 2.0 {@index}
+ */
+public enum ExecuteIn {
+    /**
+     * Command executed in immediately, in the current thread of execution.
+     */
+    FOREGROUND,
+
+    /**
+     * Command scheduled to be executed at some later time, in a 
&quot;background&quot; thread of execution.
+     */
+    BACKGROUND
+}
diff --git 
a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/job/RunBackgroundCommandsJob.java
 
b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/job/RunBackgroundCommandsJob.java
new file mode 100644
index 0000000000..0906fe3dec
--- /dev/null
+++ 
b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/job/RunBackgroundCommandsJob.java
@@ -0,0 +1,95 @@
+package org.apache.causeway.extensions.commandlog.applib.job;
+
+import lombok.RequiredArgsConstructor;
+import lombok.val;
+
+import java.sql.Timestamp;
+import java.util.List;
+
+import javax.inject.Inject;
+
+import org.apache.causeway.applib.jaxb.JavaSqlJaxbAdapters;
+import org.apache.causeway.applib.services.bookmark.Bookmark;
+import org.apache.causeway.applib.services.command.CommandExecutorService;
+import org.apache.causeway.applib.services.command.CommandOutcomeHandler;
+import org.apache.causeway.applib.services.iactnlayer.InteractionContext;
+import org.apache.causeway.applib.services.iactnlayer.InteractionService;
+import org.apache.causeway.applib.services.user.UserMemento;
+import org.apache.causeway.applib.services.xactn.TransactionService;
+import org.apache.causeway.commons.functional.ThrowingRunnable;
+import org.apache.causeway.commons.functional.Try;
+import org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntry;
+import 
org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntryRepository;
+import org.quartz.DisallowConcurrentExecution;
+import org.quartz.Job;
+import org.quartz.JobExecutionContext;
+import org.quartz.PersistJobDataAfterExecution;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Propagation;
+
+/**
+ * An implementation of a Quartz {@link Job} that queries for {@link 
CommandLogEntry}s that have been persisted by
+ * the {@link 
org.apache.causeway.extensions.commandlog.applib.dom.BackgroundService} but not 
yet started; and then
+ * executes them.
+ *
+ * @since 2.0 {@index}
+ */
+@Component
+@DisallowConcurrentExecution
+@PersistJobDataAfterExecution
+public class RunBackgroundCommandsJob implements Job {
+
+    @Inject InteractionService interactionService;
+    @Inject TransactionService transactionService;
+    @Inject CommandLogEntryRepository<? extends CommandLogEntry> 
commandLogEntryRepository;
+    @Inject CommandExecutorService commandExecutorService;
+
+    private final static 
JavaSqlJaxbAdapters.TimestampToXMLGregorianCalendarAdapter 
gregorianCalendarAdapter  = new 
JavaSqlJaxbAdapters.TimestampToXMLGregorianCalendarAdapter();;
+
+    public void execute(final JobExecutionContext quartzContext) {
+        val user = UserMemento.ofNameAndRoleNames("scheduler_user", 
"admin_role");
+        val interactionContext = 
InteractionContext.builder().user(user).build();
+        interactionService.run(interactionContext, new 
ExecuteNotYetStartedCommands());
+    }
+
+    private class ExecuteNotYetStartedCommands implements ThrowingRunnable {
+
+        @Override
+        public void run() {
+            transactionService.runTransactional(Propagation.REQUIRES_NEW, () 
-> {
+                val notYetStartedEntries = 
commandLogEntryRepository.findBackgroundAndNotYetStarted();
+                for (val commandLogEntry : notYetStartedEntries) {
+                    val commandDto = commandLogEntry.getCommandDto();
+                    
commandExecutorService.executeCommand(CommandExecutorService.InteractionContextPolicy.NO_SWITCH,
 commandDto, new OutcomeHandler(commandLogEntry));
+                }
+            }).ifFailureFail();
+
+        }
+    }
+
+    @RequiredArgsConstructor
+    private class OutcomeHandler implements CommandOutcomeHandler {
+
+        private final CommandLogEntry commandLogEntry;
+
+        @Override
+        public Timestamp getStartedAt() {
+            return commandLogEntry.getStartedAt();
+        }
+
+        @Override
+        public void setStartedAt(Timestamp startedAt) {
+            commandLogEntry.setStartedAt(startedAt);
+        }
+
+        @Override
+        public void setCompletedAt(Timestamp completedAt) {
+            commandLogEntry.setCompletedAt(completedAt);
+        }
+
+        @Override
+        public void setResult(Try<Bookmark> resultBookmark) {
+            resultBookmark.ifSuccess(bookmarkIfAny -> 
bookmarkIfAny.ifPresent(commandLogEntry::setResult));
+        }
+    }
+}
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 f0491b88dc..8cf2cb39e9 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
@@ -21,6 +21,7 @@ package 
org.apache.causeway.extensions.commandlog.applib.subscriber;
 import javax.inject.Inject;
 import javax.inject.Named;
 
+import org.apache.causeway.extensions.commandlog.applib.dom.ExecuteIn;
 import org.springframework.stereotype.Service;
 
 import org.apache.causeway.applib.annotation.PriorityPrecedence;
@@ -58,30 +59,33 @@ public class CommandSubscriberForCommandLog implements 
CommandSubscriber {
             return;
         }
 
-        val existingCommandJdoIfAny =
+        val existingCommandLogEntryIfAny =
                 
commandLogEntryRepository.findByInteractionId(command.getInteractionId());
-        if(existingCommandJdoIfAny.isPresent()) {
-            if(log.isDebugEnabled()) {
-                // this isn't expected to happen ... we just log the fact if 
it does
-                val existingCommandDto = 
existingCommandJdoIfAny.get().getCommandDto();
+        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 = JaxbUtil.toXml(existingCommandDto)
-                        .getValue().orElse("Dto to Xml failure");
-                val commandDtoXml = JaxbUtil.toXml(command.getCommandDto())
-                        .getValue().orElse("Dto to Xml failure");
+                        val existingCommandDtoXml = 
JaxbUtil.toXml(existingCommandDto)
+                                .getValue().orElse("Dto to Xml failure");
+                        val commandDtoXml = 
JaxbUtil.toXml(command.getCommandDto())
+                                .getValue().orElse("Dto to Xml failure");
 
-                log.debug("existing: \n{}", existingCommandDtoXml);
-                log.debug("proposed: \n{}", commandDtoXml);
+                        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();
-            val parentEntryIfAny =
-                    parentInteractionId != null
-                    ? commandLogEntryRepository
-                        .findByInteractionId(parentInteractionId)
-                        .orElse(null)
-                    : null;
-            commandLogEntryRepository.createEntryAndPersist(command, 
parentEntryIfAny);
+            commandLogEntryRepository.createEntryAndPersist(command, 
parentInteractionId, ExecuteIn.FOREGROUND);
         }
     }
 
diff --git 
a/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/BackgroundService_IntegTestAbstract.java
 
b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/BackgroundService_IntegTestAbstract.java
new file mode 100644
index 0000000000..3fe4e08a8b
--- /dev/null
+++ 
b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/BackgroundService_IntegTestAbstract.java
@@ -0,0 +1,224 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.extensions.commandlog.applib.integtest;
+
+import lombok.SneakyThrows;
+import lombok.val;
+
+import java.util.List;
+
+import javax.inject.Inject;
+
+import org.apache.causeway.applib.services.command.Command;
+import org.apache.causeway.core.config.environment.CausewaySystemEnvironment;
+import org.apache.causeway.extensions.commandlog.applib.dom.*;
+import 
org.apache.causeway.extensions.commandlog.applib.integtest.model.Counter;
+import 
org.apache.causeway.extensions.commandlog.applib.integtest.model.CounterRepository;
+import 
org.apache.causeway.extensions.commandlog.applib.integtest.model.Counter_bumpUsingMixin;
+import 
org.apache.causeway.extensions.commandlog.applib.job.RunBackgroundCommandsJob;
+import 
org.apache.causeway.testing.integtestsupport.applib.CausewayIntegrationTestAbstract;
+import org.apache.causeway.applib.services.bookmark.Bookmark;
+import org.apache.causeway.applib.services.bookmark.BookmarkService;
+import org.apache.causeway.applib.services.wrapper.WrapperFactory;
+import org.apache.causeway.applib.services.wrapper.control.AsyncControl;
+import org.apache.causeway.applib.services.xactn.TransactionService;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.quartz.JobExecutionContext;
+import org.springframework.transaction.annotation.Propagation;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@ExtendWith(MockitoExtension.class)
+public abstract class BackgroundService_IntegTestAbstract extends 
CausewayIntegrationTestAbstract {
+
+    @Mock JobExecutionContext mockQuartzJobExecutionContext;
+
+    Bookmark bookmark;
+
+
+    protected abstract Counter newCounter(String name);
+
+    private static boolean prototypingOrig;
+
+    @BeforeAll
+    static void setup_environment() {
+        prototypingOrig = new CausewaySystemEnvironment().isPrototyping();
+        new CausewaySystemEnvironment().setPrototyping(true);
+    }
+
+    @AfterAll
+    static void reset_environment() {
+        new CausewaySystemEnvironment().setPrototyping(prototypingOrig);
+    }
+
+    @BeforeEach
+    void setup_counter() {
+
+        transactionService.runTransactional(Propagation.REQUIRES_NEW, () -> {
+            counterRepository.removeAll();
+
+            counterRepository.persist(newCounter("fred"));
+            List<Counter> counters = counterRepository.find();
+            assertThat(counters).hasSize(1);
+
+            bookmark = bookmarkService.bookmarkForElseFail(counters.get(0));
+        }).ifFailureFail();
+
+        // given
+        assertThat(bookmark).isNotNull();
+
+        val counter = bookmarkService.lookup(bookmark, 
Counter.class).orElseThrow();
+        assertThat(counter.getNum()).isNull();
+    }
+
+    @Test
+    void async_using_default_executor_service() {
+
+        // when
+        transactionService.runTransactional(Propagation.REQUIRES_NEW, () -> {
+            val counter = bookmarkService.lookup(bookmark, 
Counter.class).orElseThrow();
+
+            wrapperFactory.asyncWrap(counter, 
AsyncControl.returning(Counter.class)).bumpUsingDeclaredAction();
+
+            Thread.sleep(1_000);// horrid, but let's just wait 1 sec to allow 
executor to complete before continuing
+        }).ifFailureFail();
+
+        // then
+        transactionService.runTransactional(Propagation.REQUIRES_NEW, () -> {
+            val counter = bookmarkService.lookup(bookmark, 
Counter.class).orElseThrow();
+            assertThat(counter.getNum()).isEqualTo(1L);
+        }).ifFailureFail();
+
+        // when
+        transactionService.runTransactional(Propagation.REQUIRES_NEW, () -> {
+            val counter = bookmarkService.lookup(bookmark, 
Counter.class).orElseThrow();
+            assertThat(counter.getNum()).isEqualTo(1L);
+
+            // when
+            wrapperFactory.asyncWrapMixin(Counter_bumpUsingMixin.class, 
counter, AsyncControl.returning(Counter.class)).act();
+
+            Thread.sleep(1_000);// horrid, but let's just wait 1 sec to allow 
executor to complete before continuing
+        }).ifFailureFail();
+
+        // then
+        transactionService.runTransactional(Propagation.REQUIRES_NEW, () -> {
+            val counter = bookmarkService.lookup(bookmark, 
Counter.class).orElseThrow();
+            assertThat(counter.getNum()).isEqualTo(2L);
+        }).ifFailureFail();
+
+    }
+
+
+    @SneakyThrows
+    @Test
+    void using_background_service() {
+
+        // given
+        removeAllCommandLogEntriesAndCounters();
+
+        // when
+        transactionService.runTransactional(Propagation.REQUIRES_NEW, () -> {
+            val counter = bookmarkService.lookup(bookmark, 
Counter.class).orElseThrow();
+            assertThat(counter.getNum()).isNull();
+
+            // when
+            backgroundService.execute(counter).bumpUsingDeclaredAction();
+
+            Thread.sleep(1_000);// horrid, but let's just wait 1 sec before 
testing
+        }).ifFailureFail();
+
+        // then no change to the counter
+        transactionService.runTransactional(Propagation.REQUIRES_NEW, () -> {
+            val counter = bookmarkService.lookup(bookmark, 
Counter.class).orElseThrow();
+            assertThat(counter.getNum()).isNull();   // still null
+        }).ifFailureFail();
+
+        // but then instead a background command is persisted
+        transactionService.runTransactional(Propagation.REQUIRES_NEW, () -> {
+            val all = commandLogEntryRepository.findAll();
+            assertThat(all).hasSize(1);
+            CommandLogEntry commandLogEntry = all.get(0);
+
+            assertThat(commandLogEntry)
+                    .satisfies(x -> 
assertThat(x.getTarget()).isEqualTo(bookmark))
+                    .satisfies(x -> 
assertThat(x.getLogicalMemberIdentifier()).isEqualTo("commandlog.test.Counter#bumpUsingDeclaredAction"))
+                    .satisfies(x -> assertThat(x.getTimestamp()).isNotNull())
+                    .satisfies(x -> 
assertThat(x.getExecuteIn()).isEqualTo(ExecuteIn.BACKGROUND))
+                    .satisfies(x -> 
assertThat(x.getParentInteractionId()).isNotNull())
+                    .satisfies(x -> assertThat(x.getCommandDto()).isNotNull())
+                    .satisfies(x -> assertThat(x.getStartedAt()).isNull())
+                    .satisfies(x -> assertThat(x.getCompletedAt()).isNull())
+                    .satisfies(x -> assertThat(x.getResult()).isNull())
+                    .satisfies(x -> 
assertThat(x.getException()).isNullOrEmpty())
+                    .satisfies(x -> 
assertThat(x.getResultSummary()).isNullOrEmpty())
+                    .satisfies(x -> 
assertThat(x.getReplayState()).isEqualTo(ReplayState.UNDEFINED))
+                    .satisfies(x -> 
assertThat(x.getReplayStateFailureReason()).isNull());
+        }).ifFailureFail();
+
+
+
+        // when (simulate quartz running in the background)
+        runBackgroundCommandsJob.execute(mockQuartzJobExecutionContext);
+
+        // then bumped
+        transactionService.runTransactional(Propagation.REQUIRES_NEW, () -> {
+            val counter = bookmarkService.lookup(bookmark, 
Counter.class).orElseThrow();
+            assertThat(counter.getNum()).isEqualTo(1L);
+        }).ifFailureFail();
+
+        // and marked as started and completed
+        transactionService.runTransactional(Propagation.REQUIRES_NEW, () -> {
+            val after = commandLogEntryRepository.findAll();
+            assertThat(after).hasSize(1);
+            CommandLogEntry commandLogEntryAfter = after.get(0);
+
+            assertThat(commandLogEntryAfter)
+                    .satisfies(x -> assertThat(x.getStartedAt()).isNotNull()) 
// changed
+                    .satisfies(x -> 
assertThat(x.getCompletedAt()).isNotNull()) // changed
+                    .satisfies(x -> assertThat(x.getResult()).isNotNull()) // 
changed
+                    .satisfies(x -> 
assertThat(x.getResultSummary()).isNotNull()) // changed
+                    ;
+        }).ifFailureFail();
+
+
+    }
+
+    private void removeAllCommandLogEntriesAndCounters() {
+        transactionService.runTransactional(Propagation.REQUIRES_NEW, () -> {
+            commandLogEntryRepository.removeAll();
+            assertThat(commandLogEntryRepository.findAll()).isEmpty();
+        }).ifFailureFail();
+    }
+
+    @Inject BackgroundService backgroundService;
+    @Inject BackgroundService.PersistCommandExecutorService 
persistCommandExecutorService;
+    @Inject WrapperFactory wrapperFactory;
+    @Inject CommandLogEntryRepository<? extends CommandLogEntry> 
commandLogEntryRepository;
+    @Inject TransactionService transactionService;
+    @Inject RunBackgroundCommandsJob runBackgroundCommandsJob;
+    @Inject BookmarkService bookmarkService;
+    @Inject CounterRepository counterRepository;
+
+}
diff --git 
a/extensions/core/commandlog/persistence-jdo/src/main/java/org/apache/causeway/extensions/commandlog/jdo/dom/CommandLogEntry.java
 
b/extensions/core/commandlog/persistence-jdo/src/main/java/org/apache/causeway/extensions/commandlog/jdo/dom/CommandLogEntry.java
index 0b4acc2502..27c1224560 100644
--- 
a/extensions/core/commandlog/persistence-jdo/src/main/java/org/apache/causeway/extensions/commandlog/jdo/dom/CommandLogEntry.java
+++ 
b/extensions/core/commandlog/persistence-jdo/src/main/java/org/apache/causeway/extensions/commandlog/jdo/dom/CommandLogEntry.java
@@ -139,10 +139,10 @@ import lombok.Setter;
                   + " ORDER BY timestamp DESC "
                   + " RANGE 0,30"),
     @Query(
-            name  = Nq.FIND_BY_PARENT,
+            name  = Nq.FIND_BY_PARENT_INTERACTION_ID,
             value = "SELECT "
                     + "  FROM " + CommandLogEntry.FQCN + " "
-                    + " WHERE parent == :parent "),
+                    + " WHERE parentInteractionId == :parentInteractionId "),
     @Query(
             name  = Nq.FIND_CURRENT,
             value = "SELECT "
@@ -173,14 +173,12 @@ import lombok.Setter;
                   + "   && completedAt != null "
                   + "ORDER BY timestamp ASC"),
     @Query(
-            name  = Nq.FIND_NOT_YET_STARTED,
+            name  = Nq.FIND_BACKGROUND_AND_NOT_YET_STARTED,
             value = "SELECT "
-                  + "FROM " + CommandLogEntry.FQCN + " "
-                  + "WHERE startedAt == null "
-                  + "ORDER BY timestamp ASC "),
-    // most recent (replayed) command previously replicated from primary to
-    // secondary.  This should always exist except for the very first times
-    // (after restored the prod DB to secondary).
+                  + "  FROM " + CommandLogEntry.FQCN + " "
+                  + " WHERE executeIn == 'BACKGROUND' "
+                  + "    && startedAt == null "
+                  + " ORDER BY timestamp ASC "),
     @Query(
             name  = Nq.FIND_MOST_RECENT_REPLAYED,
             value = "SELECT "
@@ -190,9 +188,6 @@ import lombok.Setter;
                   + " RANGE 0,2"), // this should be RANGE 0,1 but results in 
DataNucleus submitting "FETCH NEXT ROW ONLY"
                                    // which SQL Server doesn't understand.  
However, as workaround, SQL Server *does* understand FETCH NEXT 2 ROWS ONLY
 
-    // the most recent completed command, as queried on the
-    // secondary, corresponding to the last command run on primary before the
-    // production database was restored to the secondary
     @Query(
             name  = Nq.FIND_MOST_RECENT_COMPLETED,
             value = "SELECT "
@@ -263,14 +258,16 @@ extends 
org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntry {
     private Bookmark target;
 
 
-    @Column(name = Parent.NAME, allowsNull = Parent.ALLOWS_NULL)
-    @Parent
-    @Getter
-    private CommandLogEntry parent;
-    @Override
-    public void setParent(final 
org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntry parent) {
-        this.parent = (CommandLogEntry)parent;
-    }
+    @Column(allowsNull = ExecuteIn.ALLOWS_NULL, length = ExecuteIn.MAX_LENGTH)
+    @ExecuteIn
+    @Getter @Setter
+    private org.apache.causeway.extensions.commandlog.applib.dom.ExecuteIn 
executeIn;
+
+
+    @Column(allowsNull = Parent.ALLOWS_NULL, length = InteractionId.MAX_LENGTH)
+    @InteractionId
+    @Getter @Setter
+    private UUID parentInteractionId;
 
 
     @Column(allowsNull = LogicalMemberIdentifier.ALLOWS_NULL, length = 
LogicalMemberIdentifier.MAX_LENGTH)
@@ -283,7 +280,7 @@ extends 
org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntry {
     @Column(allowsNull = CommandDtoAnnot.ALLOWS_NULL, jdbcType = "CLOB")
     @CommandDtoAnnot
     @Getter @Setter
-    private org.apache.causeway.schema.cmd.v2.CommandDto commandDto;
+    private CommandDto commandDto;
 
 
     @Column(allowsNull = StartedAt.ALLOWS_NULL)
diff --git 
a/extensions/core/commandlog/persistence-jdo/src/test/java/org/apache/causeway/extensions/commandlog/jdo/integtests/CommandLog_IntegTest.java
 
b/extensions/core/commandlog/persistence-jdo/src/test/java/org/apache/causeway/extensions/commandlog/jdo/integtests/AppManifest.java
similarity index 60%
copy from 
extensions/core/commandlog/persistence-jdo/src/test/java/org/apache/causeway/extensions/commandlog/jdo/integtests/CommandLog_IntegTest.java
copy to 
extensions/core/commandlog/persistence-jdo/src/test/java/org/apache/causeway/extensions/commandlog/jdo/integtests/AppManifest.java
index 2a648b5566..3b3fa77faf 100644
--- 
a/extensions/core/commandlog/persistence-jdo/src/test/java/org/apache/causeway/extensions/commandlog/jdo/integtests/CommandLog_IntegTest.java
+++ 
b/extensions/core/commandlog/persistence-jdo/src/test/java/org/apache/causeway/extensions/commandlog/jdo/integtests/AppManifest.java
@@ -18,48 +18,29 @@
  */
 package org.apache.causeway.extensions.commandlog.jdo.integtests;
 
-import org.springframework.boot.SpringBootConfiguration;
-import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.context.annotation.ComponentScan;
-import org.springframework.context.annotation.Import;
-import org.springframework.context.annotation.PropertySource;
-import org.springframework.context.annotation.PropertySources;
-import org.springframework.test.context.ActiveProfiles;
-
 import org.apache.causeway.core.config.presets.CausewayPresets;
 import 
org.apache.causeway.core.runtimeservices.CausewayModuleCoreRuntimeServices;
-import 
org.apache.causeway.extensions.commandlog.applib.integtest.CommandLog_IntegTestAbstract;
 import 
org.apache.causeway.extensions.commandlog.applib.integtest.model.CommandLogTestDomainModel;
 import 
org.apache.causeway.extensions.commandlog.jdo.CausewayModuleExtCommandLogPersistenceJdo;
-import org.apache.causeway.extensions.commandlog.jdo.integtests.model.Counter;
 import 
org.apache.causeway.extensions.commandlog.jdo.integtests.model.CounterRepository;
 import org.apache.causeway.security.bypass.CausewayModuleSecurityBypass;
+import org.springframework.boot.SpringBootConfiguration;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Import;
+import org.springframework.context.annotation.PropertySource;
+import org.springframework.context.annotation.PropertySources;
 
-@SpringBootTest(
-        classes = CommandLog_IntegTest.AppManifest.class
-)
-@ActiveProfiles("test")
-public class CommandLog_IntegTest extends CommandLog_IntegTestAbstract {
-
-
-    @SpringBootConfiguration
-    @EnableAutoConfiguration
-    @Import({
-            CausewayModuleCoreRuntimeServices.class,
-            CausewayModuleSecurityBypass.class,
-            CausewayModuleExtCommandLogPersistenceJdo.class,
-    })
-    @PropertySources({
-            @PropertySource(CausewayPresets.UseLog4j2Test)
-    })
-    @ComponentScan(basePackageClasses = {AppManifest.class, 
CommandLogTestDomainModel.class, CounterRepository.class})
-    public static class AppManifest {
-    }
-
-
-    protected 
org.apache.causeway.extensions.commandlog.applib.integtest.model.Counter 
newCounter(String name) {
-        return Counter.builder().name(name).build();
-    }
-
+@SpringBootConfiguration
+@EnableAutoConfiguration
+@Import({
+        CausewayModuleCoreRuntimeServices.class,
+        CausewayModuleSecurityBypass.class,
+        CausewayModuleExtCommandLogPersistenceJdo.class,
+})
+@PropertySources({
+        @PropertySource(CausewayPresets.UseLog4j2Test)
+})
+@ComponentScan(basePackageClasses = {AppManifest.class, 
CommandLogTestDomainModel.class, CounterRepository.class})
+public class AppManifest {
 }
diff --git 
a/extensions/core/commandlog/persistence-jdo/src/test/java/org/apache/causeway/extensions/commandlog/jdo/integtests/CommandLog_IntegTest.java
 
b/extensions/core/commandlog/persistence-jdo/src/test/java/org/apache/causeway/extensions/commandlog/jdo/integtests/BackgroundService_IntegTest.java
similarity index 78%
copy from 
extensions/core/commandlog/persistence-jdo/src/test/java/org/apache/causeway/extensions/commandlog/jdo/integtests/CommandLog_IntegTest.java
copy to 
extensions/core/commandlog/persistence-jdo/src/test/java/org/apache/causeway/extensions/commandlog/jdo/integtests/BackgroundService_IntegTest.java
index 2a648b5566..136df64701 100644
--- 
a/extensions/core/commandlog/persistence-jdo/src/test/java/org/apache/causeway/extensions/commandlog/jdo/integtests/CommandLog_IntegTest.java
+++ 
b/extensions/core/commandlog/persistence-jdo/src/test/java/org/apache/causeway/extensions/commandlog/jdo/integtests/BackgroundService_IntegTest.java
@@ -18,44 +18,29 @@
  */
 package org.apache.causeway.extensions.commandlog.jdo.integtests;
 
-import org.springframework.boot.SpringBootConfiguration;
-import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.context.annotation.ComponentScan;
-import org.springframework.context.annotation.Import;
-import org.springframework.context.annotation.PropertySource;
-import org.springframework.context.annotation.PropertySources;
-import org.springframework.test.context.ActiveProfiles;
-
 import org.apache.causeway.core.config.presets.CausewayPresets;
 import 
org.apache.causeway.core.runtimeservices.CausewayModuleCoreRuntimeServices;
+import 
org.apache.causeway.extensions.commandlog.applib.integtest.BackgroundService_IntegTestAbstract;
 import 
org.apache.causeway.extensions.commandlog.applib.integtest.CommandLog_IntegTestAbstract;
 import 
org.apache.causeway.extensions.commandlog.applib.integtest.model.CommandLogTestDomainModel;
 import 
org.apache.causeway.extensions.commandlog.jdo.CausewayModuleExtCommandLogPersistenceJdo;
 import org.apache.causeway.extensions.commandlog.jdo.integtests.model.Counter;
 import 
org.apache.causeway.extensions.commandlog.jdo.integtests.model.CounterRepository;
 import org.apache.causeway.security.bypass.CausewayModuleSecurityBypass;
+import org.springframework.boot.SpringBootConfiguration;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Import;
+import org.springframework.context.annotation.PropertySource;
+import org.springframework.context.annotation.PropertySources;
+import org.springframework.test.context.ActiveProfiles;
 
 @SpringBootTest(
-        classes = CommandLog_IntegTest.AppManifest.class
+        classes = AppManifest.class
 )
 @ActiveProfiles("test")
-public class CommandLog_IntegTest extends CommandLog_IntegTestAbstract {
-
-
-    @SpringBootConfiguration
-    @EnableAutoConfiguration
-    @Import({
-            CausewayModuleCoreRuntimeServices.class,
-            CausewayModuleSecurityBypass.class,
-            CausewayModuleExtCommandLogPersistenceJdo.class,
-    })
-    @PropertySources({
-            @PropertySource(CausewayPresets.UseLog4j2Test)
-    })
-    @ComponentScan(basePackageClasses = {AppManifest.class, 
CommandLogTestDomainModel.class, CounterRepository.class})
-    public static class AppManifest {
-    }
+public class BackgroundService_IntegTest extends 
BackgroundService_IntegTestAbstract {
 
 
     protected 
org.apache.causeway.extensions.commandlog.applib.integtest.model.Counter 
newCounter(String name) {
diff --git 
a/extensions/core/commandlog/persistence-jdo/src/test/java/org/apache/causeway/extensions/commandlog/jdo/integtests/CommandLog_IntegTest.java
 
b/extensions/core/commandlog/persistence-jdo/src/test/java/org/apache/causeway/extensions/commandlog/jdo/integtests/CommandLog_IntegTest.java
index 2a648b5566..2ca2907791 100644
--- 
a/extensions/core/commandlog/persistence-jdo/src/test/java/org/apache/causeway/extensions/commandlog/jdo/integtests/CommandLog_IntegTest.java
+++ 
b/extensions/core/commandlog/persistence-jdo/src/test/java/org/apache/causeway/extensions/commandlog/jdo/integtests/CommandLog_IntegTest.java
@@ -18,46 +18,19 @@
  */
 package org.apache.causeway.extensions.commandlog.jdo.integtests;
 
-import org.springframework.boot.SpringBootConfiguration;
-import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
 import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.context.annotation.ComponentScan;
-import org.springframework.context.annotation.Import;
-import org.springframework.context.annotation.PropertySource;
-import org.springframework.context.annotation.PropertySources;
 import org.springframework.test.context.ActiveProfiles;
 
-import org.apache.causeway.core.config.presets.CausewayPresets;
-import 
org.apache.causeway.core.runtimeservices.CausewayModuleCoreRuntimeServices;
 import 
org.apache.causeway.extensions.commandlog.applib.integtest.CommandLog_IntegTestAbstract;
-import 
org.apache.causeway.extensions.commandlog.applib.integtest.model.CommandLogTestDomainModel;
-import 
org.apache.causeway.extensions.commandlog.jdo.CausewayModuleExtCommandLogPersistenceJdo;
 import org.apache.causeway.extensions.commandlog.jdo.integtests.model.Counter;
-import 
org.apache.causeway.extensions.commandlog.jdo.integtests.model.CounterRepository;
-import org.apache.causeway.security.bypass.CausewayModuleSecurityBypass;
 
 @SpringBootTest(
-        classes = CommandLog_IntegTest.AppManifest.class
+        classes = AppManifest.class
 )
 @ActiveProfiles("test")
 public class CommandLog_IntegTest extends CommandLog_IntegTestAbstract {
 
 
-    @SpringBootConfiguration
-    @EnableAutoConfiguration
-    @Import({
-            CausewayModuleCoreRuntimeServices.class,
-            CausewayModuleSecurityBypass.class,
-            CausewayModuleExtCommandLogPersistenceJdo.class,
-    })
-    @PropertySources({
-            @PropertySource(CausewayPresets.UseLog4j2Test)
-    })
-    @ComponentScan(basePackageClasses = {AppManifest.class, 
CommandLogTestDomainModel.class, CounterRepository.class})
-    public static class AppManifest {
-    }
-
-
     protected 
org.apache.causeway.extensions.commandlog.applib.integtest.model.Counter 
newCounter(String name) {
         return Counter.builder().name(name).build();
     }
diff --git 
a/extensions/core/commandlog/persistence-jpa/src/main/java/org/apache/causeway/extensions/commandlog/jpa/dom/CommandLogEntry.java
 
b/extensions/core/commandlog/persistence-jpa/src/main/java/org/apache/causeway/extensions/commandlog/jpa/dom/CommandLogEntry.java
index 9ffd8aea3a..c4220a3978 100644
--- 
a/extensions/core/commandlog/persistence-jpa/src/main/java/org/apache/causeway/extensions/commandlog/jpa/dom/CommandLogEntry.java
+++ 
b/extensions/core/commandlog/persistence-jpa/src/main/java/org/apache/causeway/extensions/commandlog/jpa/dom/CommandLogEntry.java
@@ -18,27 +18,15 @@
  */
 package org.apache.causeway.extensions.commandlog.jpa.dom;
 
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
 import java.util.UUID;
 
 import javax.inject.Named;
-import javax.persistence.Basic;
-import javax.persistence.Column;
-import javax.persistence.Convert;
-import javax.persistence.EmbeddedId;
-import javax.persistence.Entity;
-import javax.persistence.EntityListeners;
-import javax.persistence.EnumType;
-import javax.persistence.Enumerated;
-import javax.persistence.FetchType;
-import javax.persistence.Index;
-import javax.persistence.JoinColumn;
-import javax.persistence.JoinColumns;
-import javax.persistence.Lob;
-import javax.persistence.ManyToOne;
-import javax.persistence.NamedQueries;
-import javax.persistence.NamedQuery;
-import javax.persistence.Table;
-import javax.persistence.Transient;
+import javax.persistence.*;
 import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
 
 import org.apache.causeway.applib.annotation.DomainObject;
@@ -49,13 +37,10 @@ import 
org.apache.causeway.applib.services.bookmark.Bookmark;
 import org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntry.Nq;
 import 
org.apache.causeway.persistence.jpa.applib.integration.CausewayEntityListener;
 import 
org.apache.causeway.persistence.jpa.integration.typeconverters.applib.CausewayBookmarkConverter;
+import 
org.apache.causeway.persistence.jpa.integration.typeconverters.java.util.JavaUtilUuidConverter;
 import 
org.apache.causeway.persistence.jpa.integration.typeconverters.schema.v2.CausewayCommandDtoConverter;
 import org.apache.causeway.schema.cmd.v2.CommandDto;
 
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-import lombok.Setter;
-
 @Entity
 @Table(
         schema = CommandLogEntry.SCHEMA,
@@ -150,10 +135,10 @@ import lombok.Setter;
                   + " WHERE cl.username = :username "
                   + " ORDER BY cl.timestamp DESC"), // programmatic LIMIT 30
         @NamedQuery(
-                name  = Nq.FIND_BY_PARENT,
+                name  = Nq.FIND_BY_PARENT_INTERACTION_ID,
                 query = "SELECT cl "
                         + "  FROM CommandLogEntry cl "
-                        + " WHERE cl.parent = :parent "),
+                        + " WHERE cl.parentInteractionId = 
:parentInteractionId "),
         @NamedQuery(
                 name  = Nq.FIND_CURRENT,
                 query = "SELECT cl "
@@ -182,14 +167,12 @@ import lombok.Setter;
                   + "   AND cl.completedAt is not null "
                   + " ORDER BY cl.timestamp ASC"),
     @NamedQuery(
-            name  = Nq.FIND_NOT_YET_STARTED,
+            name  = Nq.FIND_BACKGROUND_AND_NOT_YET_STARTED,
             query = "SELECT cl "
                   + "  FROM CommandLogEntry cl "
-                  + " WHERE cl.startedAt is null "
+                  + " WHERE cl.executeIn = 
org.apache.causeway.extensions.commandlog.applib.dom.ExecuteIn.BACKGROUND "
+                  + "   AND cl.startedAt is null "
                   + " ORDER BY cl.timestamp ASC"),
-    // most recent (replayed) command previously replicated from primary to
-    // secondary.  This should always exist except for the very first times
-    // (after restored the prod DB to secondary).
     @NamedQuery(
             name  = Nq.FIND_MOST_RECENT_REPLAYED,
             query = "SELECT cl "
@@ -271,17 +254,18 @@ public class CommandLogEntry extends 
org.apache.causeway.extensions.commandlog.a
     private Bookmark target;
 
 
-    @ManyToOne
-    @JoinColumns({
-        @JoinColumn(name = Parent.NAME, nullable = Parent.NULLABLE, 
referencedColumnName = InteractionId.NAME)
-    })
-    @Parent
-    @Getter
-    private CommandLogEntry parent;
-    @Override
-    public void setParent(final 
org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntry parent) {
-        this.parent = (CommandLogEntry)parent;
-    }
+    @Column(nullable = ExecuteIn.NULLABLE, length = ExecuteIn.MAX_LENGTH)
+    @Enumerated(EnumType.STRING)
+    @ExecuteIn
+    @Getter @Setter
+    private org.apache.causeway.extensions.commandlog.applib.dom.ExecuteIn 
executeIn;
+
+
+    @Convert(converter = JavaUtilUuidConverter.class)
+    @Column(nullable = Parent.NULLABLE, length = InteractionId.MAX_LENGTH)
+    @Getter @Setter
+    private UUID parentInteractionId;
+
 
 
     @Column(nullable = LogicalMemberIdentifier.NULLABLE, length = 
LogicalMemberIdentifier.MAX_LENGTH)
diff --git 
a/extensions/core/commandlog/persistence-jpa/src/test/java/org/apache/causeway/extensions/commandlog/jpa/integtests/CommandLog_IntegTest.java
 
b/extensions/core/commandlog/persistence-jpa/src/test/java/org/apache/causeway/extensions/commandlog/jpa/integtests/AppManifest.java
similarity index 62%
copy from 
extensions/core/commandlog/persistence-jpa/src/test/java/org/apache/causeway/extensions/commandlog/jpa/integtests/CommandLog_IntegTest.java
copy to 
extensions/core/commandlog/persistence-jpa/src/test/java/org/apache/causeway/extensions/commandlog/jpa/integtests/AppManifest.java
index a7d006b694..ea05142180 100644
--- 
a/extensions/core/commandlog/persistence-jpa/src/test/java/org/apache/causeway/extensions/commandlog/jpa/integtests/CommandLog_IntegTest.java
+++ 
b/extensions/core/commandlog/persistence-jpa/src/test/java/org/apache/causeway/extensions/commandlog/jpa/integtests/AppManifest.java
@@ -18,49 +18,31 @@
  */
 package org.apache.causeway.extensions.commandlog.jpa.integtests;
 
+import org.apache.causeway.core.config.presets.CausewayPresets;
+import 
org.apache.causeway.core.runtimeservices.CausewayModuleCoreRuntimeServices;
+import 
org.apache.causeway.extensions.commandlog.applib.integtest.model.CommandLogTestDomainModel;
+import 
org.apache.causeway.extensions.commandlog.jpa.CausewayModuleExtCommandLogPersistenceJpa;
+import org.apache.causeway.extensions.commandlog.jpa.integtests.model.Counter;
+import org.apache.causeway.security.bypass.CausewayModuleSecurityBypass;
 import org.springframework.boot.SpringBootConfiguration;
 import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
 import org.springframework.boot.autoconfigure.domain.EntityScan;
-import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.context.annotation.ComponentScan;
 import org.springframework.context.annotation.Import;
 import org.springframework.context.annotation.PropertySource;
 import org.springframework.context.annotation.PropertySources;
-import org.springframework.test.context.ActiveProfiles;
-
-import org.apache.causeway.core.config.presets.CausewayPresets;
-import 
org.apache.causeway.core.runtimeservices.CausewayModuleCoreRuntimeServices;
-import 
org.apache.causeway.extensions.commandlog.applib.integtest.CommandLog_IntegTestAbstract;
-import 
org.apache.causeway.extensions.commandlog.applib.integtest.model.CommandLogTestDomainModel;
-import 
org.apache.causeway.extensions.commandlog.jpa.CausewayModuleExtCommandLogPersistenceJpa;
-import org.apache.causeway.extensions.commandlog.jpa.integtests.model.Counter;
-import org.apache.causeway.security.bypass.CausewayModuleSecurityBypass;
-
-@SpringBootTest(
-        classes = CommandLog_IntegTest.AppManifest.class
-)
-@ActiveProfiles("test")
-public class CommandLog_IntegTest extends CommandLog_IntegTestAbstract {
-
-
-    @SpringBootConfiguration
-    @EnableAutoConfiguration
-    @Import({
-            CausewayModuleCoreRuntimeServices.class,
-            CausewayModuleSecurityBypass.class,
-            CausewayModuleExtCommandLogPersistenceJpa.class,
-    })
-    @PropertySources({
-            @PropertySource(CausewayPresets.UseLog4j2Test)
-    })
-    @EntityScan(basePackageClasses = {Counter.class})
-    @ComponentScan(basePackageClasses = {AppManifest.class, 
CommandLogTestDomainModel.class})
-    public static class AppManifest {
-    }
-
-
-    protected 
org.apache.causeway.extensions.commandlog.applib.integtest.model.Counter 
newCounter(String name) {
-        return Counter.builder().name(name).build();
-    }
 
+@SpringBootConfiguration
+@EnableAutoConfiguration
+@Import({
+        CausewayModuleCoreRuntimeServices.class,
+        CausewayModuleSecurityBypass.class,
+        CausewayModuleExtCommandLogPersistenceJpa.class,
+})
+@PropertySources({
+        @PropertySource(CausewayPresets.UseLog4j2Test)
+})
+@EntityScan(basePackageClasses = {Counter.class})
+@ComponentScan(basePackageClasses = {AppManifest.class, 
CommandLogTestDomainModel.class})
+public class AppManifest {
 }
diff --git 
a/extensions/core/commandlog/persistence-jpa/src/test/java/org/apache/causeway/extensions/commandlog/jpa/integtests/CommandLog_IntegTest.java
 
b/extensions/core/commandlog/persistence-jpa/src/test/java/org/apache/causeway/extensions/commandlog/jpa/integtests/BackgroundService_IntegTest.java
similarity index 50%
copy from 
extensions/core/commandlog/persistence-jpa/src/test/java/org/apache/causeway/extensions/commandlog/jpa/integtests/CommandLog_IntegTest.java
copy to 
extensions/core/commandlog/persistence-jpa/src/test/java/org/apache/causeway/extensions/commandlog/jpa/integtests/BackgroundService_IntegTest.java
index a7d006b694..613480fb7f 100644
--- 
a/extensions/core/commandlog/persistence-jpa/src/test/java/org/apache/causeway/extensions/commandlog/jpa/integtests/CommandLog_IntegTest.java
+++ 
b/extensions/core/commandlog/persistence-jpa/src/test/java/org/apache/causeway/extensions/commandlog/jpa/integtests/BackgroundService_IntegTest.java
@@ -18,45 +18,17 @@
  */
 package org.apache.causeway.extensions.commandlog.jpa.integtests;
 
-import org.springframework.boot.SpringBootConfiguration;
-import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
-import org.springframework.boot.autoconfigure.domain.EntityScan;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.context.annotation.ComponentScan;
-import org.springframework.context.annotation.Import;
-import org.springframework.context.annotation.PropertySource;
-import org.springframework.context.annotation.PropertySources;
-import org.springframework.test.context.ActiveProfiles;
-
-import org.apache.causeway.core.config.presets.CausewayPresets;
-import 
org.apache.causeway.core.runtimeservices.CausewayModuleCoreRuntimeServices;
+import 
org.apache.causeway.extensions.commandlog.applib.integtest.BackgroundService_IntegTestAbstract;
 import 
org.apache.causeway.extensions.commandlog.applib.integtest.CommandLog_IntegTestAbstract;
-import 
org.apache.causeway.extensions.commandlog.applib.integtest.model.CommandLogTestDomainModel;
-import 
org.apache.causeway.extensions.commandlog.jpa.CausewayModuleExtCommandLogPersistenceJpa;
 import org.apache.causeway.extensions.commandlog.jpa.integtests.model.Counter;
-import org.apache.causeway.security.bypass.CausewayModuleSecurityBypass;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
 
 @SpringBootTest(
-        classes = CommandLog_IntegTest.AppManifest.class
+        classes = AppManifest.class
 )
 @ActiveProfiles("test")
-public class CommandLog_IntegTest extends CommandLog_IntegTestAbstract {
-
-
-    @SpringBootConfiguration
-    @EnableAutoConfiguration
-    @Import({
-            CausewayModuleCoreRuntimeServices.class,
-            CausewayModuleSecurityBypass.class,
-            CausewayModuleExtCommandLogPersistenceJpa.class,
-    })
-    @PropertySources({
-            @PropertySource(CausewayPresets.UseLog4j2Test)
-    })
-    @EntityScan(basePackageClasses = {Counter.class})
-    @ComponentScan(basePackageClasses = {AppManifest.class, 
CommandLogTestDomainModel.class})
-    public static class AppManifest {
-    }
+public class BackgroundService_IntegTest extends 
BackgroundService_IntegTestAbstract {
 
 
     protected 
org.apache.causeway.extensions.commandlog.applib.integtest.model.Counter 
newCounter(String name) {
diff --git 
a/extensions/core/commandlog/persistence-jpa/src/test/java/org/apache/causeway/extensions/commandlog/jpa/integtests/CommandLog_IntegTest.java
 
b/extensions/core/commandlog/persistence-jpa/src/test/java/org/apache/causeway/extensions/commandlog/jpa/integtests/CommandLog_IntegTest.java
index a7d006b694..8a686d86e1 100644
--- 
a/extensions/core/commandlog/persistence-jpa/src/test/java/org/apache/causeway/extensions/commandlog/jpa/integtests/CommandLog_IntegTest.java
+++ 
b/extensions/core/commandlog/persistence-jpa/src/test/java/org/apache/causeway/extensions/commandlog/jpa/integtests/CommandLog_IntegTest.java
@@ -18,47 +18,19 @@
  */
 package org.apache.causeway.extensions.commandlog.jpa.integtests;
 
-import org.springframework.boot.SpringBootConfiguration;
-import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
-import org.springframework.boot.autoconfigure.domain.EntityScan;
 import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.context.annotation.ComponentScan;
-import org.springframework.context.annotation.Import;
-import org.springframework.context.annotation.PropertySource;
-import org.springframework.context.annotation.PropertySources;
 import org.springframework.test.context.ActiveProfiles;
 
-import org.apache.causeway.core.config.presets.CausewayPresets;
-import 
org.apache.causeway.core.runtimeservices.CausewayModuleCoreRuntimeServices;
 import 
org.apache.causeway.extensions.commandlog.applib.integtest.CommandLog_IntegTestAbstract;
-import 
org.apache.causeway.extensions.commandlog.applib.integtest.model.CommandLogTestDomainModel;
-import 
org.apache.causeway.extensions.commandlog.jpa.CausewayModuleExtCommandLogPersistenceJpa;
 import org.apache.causeway.extensions.commandlog.jpa.integtests.model.Counter;
-import org.apache.causeway.security.bypass.CausewayModuleSecurityBypass;
 
 @SpringBootTest(
-        classes = CommandLog_IntegTest.AppManifest.class
+        classes = AppManifest.class
 )
 @ActiveProfiles("test")
 public class CommandLog_IntegTest extends CommandLog_IntegTestAbstract {
 
 
-    @SpringBootConfiguration
-    @EnableAutoConfiguration
-    @Import({
-            CausewayModuleCoreRuntimeServices.class,
-            CausewayModuleSecurityBypass.class,
-            CausewayModuleExtCommandLogPersistenceJpa.class,
-    })
-    @PropertySources({
-            @PropertySource(CausewayPresets.UseLog4j2Test)
-    })
-    @EntityScan(basePackageClasses = {Counter.class})
-    @ComponentScan(basePackageClasses = {AppManifest.class, 
CommandLogTestDomainModel.class})
-    public static class AppManifest {
-    }
-
-
     protected 
org.apache.causeway.extensions.commandlog.applib.integtest.model.Counter 
newCounter(String name) {
         return Counter.builder().name(name).build();
     }
diff --git a/extensions/core/commandlog/pom.xml 
b/extensions/core/commandlog/pom.xml
index 0609eac341..43c7289937 100644
--- a/extensions/core/commandlog/pom.xml
+++ b/extensions/core/commandlog/pom.xml
@@ -29,6 +29,36 @@
 
        <dependencyManagement>
                <dependencies>
+
+                       <dependency>
+                               
<groupId>org.apache.causeway.extensions</groupId>
+                               
<artifactId>causeway-extensions-commandlog</artifactId>
+                               <version>2.0.0-SNAPSHOT</version>
+                               <type>pom</type>
+                       </dependency>
+                       <dependency>
+                               
<groupId>org.apache.causeway.extensions</groupId>
+                               
<artifactId>causeway-extensions-commandlog-applib</artifactId>
+                               <version>2.0.0-SNAPSHOT</version>
+                       </dependency>
+                       <dependency>
+                               
<groupId>org.apache.causeway.extensions</groupId>
+                               
<artifactId>causeway-extensions-commandlog-applib</artifactId>
+                               <version>2.0.0-SNAPSHOT</version>
+                               <scope>test</scope>
+                               <type>test-jar</type>
+                       </dependency>
+                       <dependency>
+                               
<groupId>org.apache.causeway.extensions</groupId>
+                               
<artifactId>causeway-extensions-commandlog-persistence-jdo</artifactId>
+                               <version>2.0.0-SNAPSHOT</version>
+                       </dependency>
+                       <dependency>
+                               
<groupId>org.apache.causeway.extensions</groupId>
+                               
<artifactId>causeway-extensions-commandlog-persistence-jpa</artifactId>
+                               <version>2.0.0-SNAPSHOT</version>
+                       </dependency>
+
                        <dependency>
                                
<groupId>org.apache.causeway.extensions</groupId>
                                <artifactId>causeway-extensions</artifactId>
diff --git 
a/testing/integtestsupport/applib/src/main/java/org/apache/causeway/testing/integtestsupport/applib/CausewayInteractionHandler.java
 
b/testing/integtestsupport/applib/src/main/java/org/apache/causeway/testing/integtestsupport/applib/CausewayInteractionHandler.java
index d0aefbc1b4..b02223a010 100644
--- 
a/testing/integtestsupport/applib/src/main/java/org/apache/causeway/testing/integtestsupport/applib/CausewayInteractionHandler.java
+++ 
b/testing/integtestsupport/applib/src/main/java/org/apache/causeway/testing/integtestsupport/applib/CausewayInteractionHandler.java
@@ -18,6 +18,7 @@
  */
 package org.apache.causeway.testing.integtestsupport.applib;
 
+import org.apache.causeway.core.config.environment.CausewaySystemEnvironment;
 import org.junit.jupiter.api.extension.AfterEachCallback;
 import org.junit.jupiter.api.extension.BeforeEachCallback;
 import org.junit.jupiter.api.extension.ExtensionContext;


Reply via email to