You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by da...@apache.org on 2020/09/13 10:26:57 UTC

[isis] branch ISIS-2222 updated: ISIS-2222: reworking command, lots of stuff here...

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

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


The following commit(s) were added to refs/heads/ISIS-2222 by this push:
     new 718ec97   ISIS-2222: reworking command, lots of stuff here...
718ec97 is described below

commit 718ec97f369d47851bb3f3cc8f5ba54a941d774d
Author: danhaywood <da...@haywood-associates.co.uk>
AuthorDate: Sun Sep 13 11:26:00 2020 +0100

     ISIS-2222: reworking command, lots of stuff here...
---
 .../isis/applib/services/DomainChangeRecord.java   |   2 +-
 .../services/conmap/command/UserDataKeys.java      |   4 +-
 .../apache/isis/core/config/IsisConfiguration.java |  60 ++++++--
 .../core/runtime/iactn/IsisInteractionFactory.java |  16 +-
 .../template/AbstractIsisInteractionTemplate.java  |  76 ---------
 .../session/IsisInteractionFactoryDefault.java     |  26 ++++
 .../wrapper/WrapperFactoryDefault.java             |   2 +-
 .../_commands/ExposePersistedCommands.java         |  30 ++++
 .../config/application-primary.properties          |   1 +
 .../config/application-secondary.properties        |   5 +
 examples/demo/web/pom.xml                          |   2 +-
 .../src/main/java/demoapp/web/DemoAppManifest.java |   8 +-
 .../wicket/src/main/resources/log4j2-spring.xml    |   4 +-
 .../extensions/commandlog/impl/jdo/CommandJdo.java |  46 ++++--
 .../impl/jdo/CommandJdo.layout.fallback.xml        |  47 +++---
 .../commandlog/impl/jdo/CommandJdoRepository.java  |  12 +-
 .../impl/mixins/HasUniqueId_command.java           |   5 +-
 extensions/core/command-replay/impl/pom.xml        |   4 +-
 .../impl/IsisModuleExtCommandReplayImpl.java       |  91 ++++++++++-
 .../{SlaveStatus.java => SecondaryStatus.java}     |   2 +-
 .../commandreplay/impl/StatusException.java        |  10 +-
 .../impl/analysis/CommandReplayAnalyser.java       |   8 +-
 .../analysis/CommandReplayAnalyserAbstract.java    |  52 -------
 .../analysis/CommandReplayAnalyserException.java   |  65 ++++++++
 .../CommandReplayAnalyserExceptionStr.java         |  56 -------
 ...ltStr.java => CommandReplayAnalyserResult.java} |  32 ++--
 .../impl/clock/TickingClockService.java            |  50 +++---
 .../executor/CommandExecutorServiceWithTime.java   |   5 +-
 .../commandreplay/impl/fetch/CommandFetcher.java   |  30 ++--
 .../impl/fetch/MasterConfiguration.java            |  38 -----
 .../commandreplay/impl/fetch/PrimaryConfig.java    |  38 +++++
 .../commandreplay/impl/fetch/SecondaryConfig.java  |  32 ++++
 .../impl/job/ReplicateAndReplayJob.java            |  93 +++++++++++
 .../impl/job/SecondaryStatusData.java              |  36 +++++
 .../job/callables/IsTickingClockInitialized.java   |  22 +++
 .../callables/ReplicateAndRunCommands.java}        | 105 ++++++-------
 ...OnMaster.java => CommandJdo_openOnPrimary.java} |  10 +-
 .../impl/mixins/CommandJdo_replayNext.java         |  16 +-
 .../impl/mixins/CommandJdo_replayQueue.java        |   6 +-
 .../impl/quartz/QuartzConfigKeys.java              |  13 --
 ...kgroundCommandsWithReplicationAndReplayJob.java | 171 ---------------------
 .../impl/spi/ReplayCommandExecutionController.java |  11 +-
 .../impl/ui/CommandReplayOnPrimaryService.java     |  14 +-
 .../impl/ui/CommandReplayOnSecondaryService.java   |   9 +-
 .../extensions/commandreplay/impl/util/Holder.java |   6 -
 .../impl/fetch/ReplicateAndReplayJob_Test.java}    |   2 +-
 .../examples/DemoIsisInteractionTemplate.java      |  28 ++++
 .../modules/quartz/examples}/DemoJob.java          |  15 +-
 .../examples/DemoJobQuartzConfigurerModule.java    |   7 +-
 .../quartz/adoc/modules/quartz/pages/about.adoc    |  30 +++-
 .../quartz/context/JobExecutionData.java           |  36 +++++
 .../spring}/AutowiringSpringBeanJobFactory.java    |   4 +-
 52 files changed, 837 insertions(+), 656 deletions(-)

diff --git a/api/applib/src/main/java/org/apache/isis/applib/services/DomainChangeRecord.java b/api/applib/src/main/java/org/apache/isis/applib/services/DomainChangeRecord.java
index ea81d3f..7cfc48d 100644
--- a/api/applib/src/main/java/org/apache/isis/applib/services/DomainChangeRecord.java
+++ b/api/applib/src/main/java/org/apache/isis/applib/services/DomainChangeRecord.java
@@ -96,7 +96,7 @@ public interface DomainChangeRecord extends HasUniqueId, HasUsername {
      * The object type of the domain object being changed.
      */
     @Property
-    @PropertyLayout(named="Class")
+    @PropertyLayout(named="Object Type")
     @MemberOrder(name="Target", sequence = "10")
     default String getTargetObjectType() {
         return getTarget().getObjectType();
diff --git a/api/applib/src/main/java/org/apache/isis/applib/services/conmap/command/UserDataKeys.java b/api/applib/src/main/java/org/apache/isis/applib/services/conmap/command/UserDataKeys.java
index 5559f40..bc1bb2f 100644
--- a/api/applib/src/main/java/org/apache/isis/applib/services/conmap/command/UserDataKeys.java
+++ b/api/applib/src/main/java/org/apache/isis/applib/services/conmap/command/UserDataKeys.java
@@ -28,7 +28,7 @@ import lombok.experimental.UtilityClass;
 @UtilityClass
 public class UserDataKeys {
 
-    public static String RESULT = "org.apache.isis.extensions.commandlog.impl.api.UserDataKeys#RESULT";
-    public static String EXCEPTION = "org.apache.isis.extensions.commandlog.impl.api.UserDataKeys#EXCEPTION";
+    public static String RESULT = UserDataKeys.class.getName() + "#" + "RESULT";
+    public static String EXCEPTION = UserDataKeys.class.getName() + "#" + "EXCEPTION";
 
 }
diff --git a/core/config/src/main/java/org/apache/isis/core/config/IsisConfiguration.java b/core/config/src/main/java/org/apache/isis/core/config/IsisConfiguration.java
index 22c4f48..ceaf342 100644
--- a/core/config/src/main/java/org/apache/isis/core/config/IsisConfiguration.java
+++ b/core/config/src/main/java/org/apache/isis/core/config/IsisConfiguration.java
@@ -3049,28 +3049,70 @@ public class IsisConfiguration {
         private final Quartz quartz = new Quartz();
         @Data
         public static class Quartz {
-            private final RunBackgroundCommands runBackgroundCommands = new RunBackgroundCommands();
-            @Data
-            public static class RunBackgroundCommands {
-                private String user = "isisModuleExtQuartzRunBackgroundCommandsUser";
-                private List<String> roles = listOf("isisModuleExtQuartzRunBackgroundCommandsRole");
-            }
         }
 
         private final CommandReplay commandReplay = new CommandReplay();
         @Data
         public static class CommandReplay {
 
-            private final Master master = new Master();
+            private final Primary primary = new Primary();
             @Data
-            public static class Master {
+            public static class Primary {
                 private Optional<String> baseUrl;               
-                private Optional<String> password;               
                 private Optional<String> user;               
+                private Optional<String> password;               
                 private Optional<String> baseUrlEndUser;               
                 private Integer batchSize = 10;               
             }
+            
+            private final Secondary secondary = new Secondary();
+            @Data
+            public static class Secondary {
+                
+                private final QuartzSession quartzSession = new QuartzSession();
+                @Data
+                public static class QuartzSession {
+                    /**
+                     * The user that runs the replay session secondary.
+                     */
+                    private String user = "isisModuleExtCommandReplaySecondaryUser";
+                    private List<String> roles = listOf("isisModuleExtCommandReplaySecondaryRole");
+                }
+                
+                private final QuartzReplicateAndReplayJob quartzReplicateAndReplayJob = new QuartzReplicateAndReplayJob();
+                @Data
+                public static class QuartzReplicateAndReplayJob {
+                    /**
+                     * Number of milliseconds before starting the job.
+                     */
+                    private long startDelay = 15000;
+                    /**
+                     * Number of milliseconds before running again.
+                     */
+                    private long repeatInterval = 10000;
+                }
+
+                private final Analyser analyser = new Analyser();
+                @Data
+                public static class Analyser {
+                    private final Result result = new Result();
+                    @Data
+                    public static class Result {
+                        private boolean enabled = true;
+                    }
+                    private final Exception exception = new Exception();
+                    @Data
+                    public static class Exception {
+                        private boolean enabled = true;
+                    }
+
+                }
+
+            }
+            
+            
         }
+        
 
     }
 
diff --git a/core/runtime/src/main/java/org/apache/isis/core/runtime/iactn/IsisInteractionFactory.java b/core/runtime/src/main/java/org/apache/isis/core/runtime/iactn/IsisInteractionFactory.java
index f949982..f7561ba 100644
--- a/core/runtime/src/main/java/org/apache/isis/core/runtime/iactn/IsisInteractionFactory.java
+++ b/core/runtime/src/main/java/org/apache/isis/core/runtime/iactn/IsisInteractionFactory.java
@@ -20,6 +20,7 @@
 package org.apache.isis.core.runtime.iactn;
 
 import java.util.concurrent.Callable;
+import java.util.function.Supplier;
 
 import javax.inject.Inject;
 
@@ -90,25 +91,14 @@ public interface IsisInteractionFactory {
      * @param callable
      */
     @SneakyThrows
-    public default <R> R callAnonymous(Callable<R> callable) {
-        if(isInInteraction()) {
-            return callable.call(); // reuse existing session
-        }
-        return callAuthenticated(new InitialisationSession(), callable);
-    }
+    public <R> R callAnonymous(Callable<R> callable);
     
     /**
      * Variant of {@link #callAnonymous(Callable)} that takes a runnable.
      * @param runnable
      */
     @SneakyThrows
-    public default void runAnonymous(ThrowingRunnable runnable) {
-        if(isInInteraction()) {
-            runnable.run(); // reuse existing session
-            return;
-        }
-        runAuthenticated(new InitialisationSession(), runnable);
-    }
+    public void runAnonymous(ThrowingRunnable runnable);
 
     /**
      * closes all open IsisInteractions as stacked on the current thread
diff --git a/core/runtime/src/main/java/org/apache/isis/core/runtime/iactn/template/AbstractIsisInteractionTemplate.java b/core/runtime/src/main/java/org/apache/isis/core/runtime/iactn/template/AbstractIsisInteractionTemplate.java
deleted file mode 100644
index 4d79391..0000000
--- a/core/runtime/src/main/java/org/apache/isis/core/runtime/iactn/template/AbstractIsisInteractionTemplate.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- *  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.isis.core.runtime.iactn.template;
-
-import javax.inject.Inject;
-
-import org.apache.isis.applib.services.inject.ServiceInjector;
-import org.apache.isis.applib.services.xactn.TransactionService;
-import org.apache.isis.core.runtime.iactn.IsisInteraction;
-import org.apache.isis.core.runtime.iactn.IsisInteractionFactory;
-import org.apache.isis.core.security.authentication.AuthenticationSession;
-
-public abstract class AbstractIsisInteractionTemplate {
-    
-    @Inject protected TransactionService transactionService;
-    @Inject protected IsisInteractionFactory isisInteractionFactory;
-    @Inject protected ServiceInjector serviceInjector;
-
-    /**
-     * Sets up an {@link IsisInteraction} then passes along any calling framework's context.
-     */
-    public void execute(final AuthenticationSession authSession, final Object context) {
-        
-        isisInteractionFactory.runAuthenticated(authSession, ()->{
-            serviceInjector.injectServicesInto(this);
-            doExecute(context);
-        });
-        
-    }
-
-    // //////////////////////////////////////
-
-    /**
-     * Either override {@link #doExecute(Object)} (this method) or alternatively override
-     * {@link #doExecuteWithTransaction(Object)}.
-     *
-     * <p>
-     * This method is called within a current {@link IsisInteraction interaction} request,
-     * but with no current transaction.  The default implementation sets up a transaction
-     * and then calls {@link #doExecuteWithTransaction(Object)}.  Override if you require more sophisticated
-     * transaction handling.
-     */
-    protected void doExecute(final Object context) {
-        transactionService.executeWithinTransaction(()->{
-            doExecuteWithTransaction(context);
-        });
-    }
-
-    /**
-     * Either override {@link #doExecuteWithTransaction(Object)} (this method) or alternatively override
-     * {@link #doExecuteWithTransaction(Object)}.
-     *
-     * <p>
-     * This method is called within a current transaction, by the default
-     * implementation of {@link #doExecute(Object)}.
-     */
-    protected void doExecuteWithTransaction(final Object context) {}
-
-
-}
diff --git a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/session/IsisInteractionFactoryDefault.java b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/session/IsisInteractionFactoryDefault.java
index 94741c9..5ef1394 100644
--- a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/session/IsisInteractionFactoryDefault.java
+++ b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/session/IsisInteractionFactoryDefault.java
@@ -24,6 +24,7 @@ import java.util.Optional;
 import java.util.Stack;
 import java.util.UUID;
 import java.util.concurrent.Callable;
+import java.util.function.Supplier;
 
 import javax.annotation.PostConstruct;
 import javax.inject.Inject;
@@ -54,6 +55,7 @@ import org.apache.isis.core.runtime.iactn.IsisInteractionFactory;
 import org.apache.isis.core.runtime.iactn.IsisInteractionTracker;
 import org.apache.isis.core.runtime.iactn.scope.IsisInteractionScopeBeanFactoryPostProcessor;
 import org.apache.isis.core.runtime.iactn.scope.IsisInteractionScopeCloseListener;
+import org.apache.isis.core.runtime.session.init.InitialisationSession;
 import org.apache.isis.core.runtime.session.init.IsisLocaleInitializer;
 import org.apache.isis.core.runtime.session.init.IsisTimeZoneInitializer;
 import org.apache.isis.core.runtimeservices.user.UserServiceDefault;
@@ -214,6 +216,7 @@ public class IsisInteractionFactoryDefault implements IsisInteractionFactory, Is
         openSession(authenticationSession);
         
         try {
+            serviceInjector.injectServicesInto(callable);
             return callable.call();
         } finally {
             closeSessionStackDownToStackSize(stackSizeWhenEntering);
@@ -221,6 +224,29 @@ public class IsisInteractionFactoryDefault implements IsisInteractionFactory, Is
 
     }
 
+    @SneakyThrows
+    public <R> R callAnonymous(Callable<R> callable) {
+        if(isInInteraction()) {
+            serviceInjector.injectServicesInto(callable);
+            return callable.call(); // reuse existing session
+        }
+        return callAuthenticated(new InitialisationSession(), callable);
+    }
+
+    /**
+     * Variant of {@link #callAnonymous(Callable)} that takes a runnable.
+     * @param runnable
+     */
+    @SneakyThrows
+    public void runAnonymous(ThrowingRunnable runnable) {
+        if(isInInteraction()) {
+            serviceInjector.injectServicesInto(runnable);
+            runnable.run(); // reuse existing session
+            return;
+        }
+        runAuthenticated(new InitialisationSession(), runnable);
+    }
+
     private final ThreadLocal<UUID> conversationId = ThreadLocal.withInitial(()->null);
     
     @Override
diff --git a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/wrapper/WrapperFactoryDefault.java b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/wrapper/WrapperFactoryDefault.java
index be198a2..9634467 100644
--- a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/wrapper/WrapperFactoryDefault.java
+++ b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/wrapper/WrapperFactoryDefault.java
@@ -297,7 +297,6 @@ public class WrapperFactoryDefault implements WrapperFactory {
             final Object[] args,
             final AsyncControl<R> asyncControl) {
 
-        val executorService = asyncControl.getExecutorService();
         val isisInteraction = currentIsisInteraction();
         val asyncAuthSession = authSessionFrom(asyncControl, isisInteraction.getAuthenticationSession());
 
@@ -325,6 +324,7 @@ public class WrapperFactoryDefault implements WrapperFactory {
 
         asyncControlService.init(asyncControl, method, Bookmark.from(oidDto));
 
+        val executorService = asyncControl.getExecutorService();
         Future future = executorService.submit(() ->
                 isisInteractionFactory.callAuthenticated(asyncAuthSession, () ->
                     transactionService.executeWithinTransaction(() -> {
diff --git a/examples/demo/domain/src/main/java/demoapp/dom/annotDomain/_commands/ExposePersistedCommands.java b/examples/demo/domain/src/main/java/demoapp/dom/annotDomain/_commands/ExposePersistedCommands.java
index 4c70430..253abcc 100644
--- a/examples/demo/domain/src/main/java/demoapp/dom/annotDomain/_commands/ExposePersistedCommands.java
+++ b/examples/demo/domain/src/main/java/demoapp/dom/annotDomain/_commands/ExposePersistedCommands.java
@@ -1,12 +1,18 @@
 package demoapp.dom.annotDomain._commands;
 
+import java.util.Arrays;
 import java.util.LinkedList;
 import java.util.List;
 
 import javax.inject.Inject;
 
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Service;
+
 import org.apache.isis.applib.annotation.Collection;
 import org.apache.isis.applib.annotation.CollectionLayout;
+import org.apache.isis.applib.annotation.OrderPrecedence;
+import org.apache.isis.applib.services.tablecol.TableColumnOrderForCollectionTypeAbstract;
 import org.apache.isis.extensions.commandlog.impl.jdo.CommandJdo;
 import org.apache.isis.extensions.commandlog.impl.jdo.CommandJdoRepository;
 
@@ -20,5 +26,29 @@ import demoapp.dom.annotDomain.Action.command.ActionCommandJdo;
  */
 //tag::class[]
 public interface ExposePersistedCommands {
+
+    @Service
+    @Order(OrderPrecedence.EARLY)
+    public static class TableColumnOrderDefault extends TableColumnOrderForCollectionTypeAbstract<CommandJdo> {
+
+        public TableColumnOrderDefault() { super(CommandJdo.class); }
+
+        @Override
+        protected List<String> orderParented(Object parent, String collectionId, List<String> propertyIds) {
+            return ordered(propertyIds);
+        }
+
+        @Override
+        protected List<String> orderStandalone(List<String> propertyIds) {
+            return ordered(propertyIds);
+        }
+
+        private List<String> ordered(List<String> propertyIds) {
+            return Arrays.asList(
+                    "timestamp", "commandDto", "username", "complete", "resultSummary"
+            );
+        }
+    }
+
 }
 //end::class[]
diff --git a/examples/demo/domain/src/main/resources/config/application-primary.properties b/examples/demo/domain/src/main/resources/config/application-primary.properties
new file mode 100644
index 0000000..a3ac65c
--- /dev/null
+++ b/examples/demo/domain/src/main/resources/config/application-primary.properties
@@ -0,0 +1 @@
+server.port=8080
\ No newline at end of file
diff --git a/examples/demo/domain/src/main/resources/config/application-secondary.properties b/examples/demo/domain/src/main/resources/config/application-secondary.properties
new file mode 100644
index 0000000..0b1d756
--- /dev/null
+++ b/examples/demo/domain/src/main/resources/config/application-secondary.properties
@@ -0,0 +1,5 @@
+isis.extensions.command-replay.primary.base-url=http://localhost:8080
+isis.extensions.command-replay.primary.user=sven
+isis.extensions.command-replay.primary.password=pass
+
+server.port=9090
\ No newline at end of file
diff --git a/examples/demo/web/pom.xml b/examples/demo/web/pom.xml
index c7f5253..6108727 100644
--- a/examples/demo/web/pom.xml
+++ b/examples/demo/web/pom.xml
@@ -60,7 +60,7 @@
 		<!-- Extensions -->
 		<dependency>
 			<groupId>org.apache.isis.extensions</groupId>
-			<artifactId>isis-extensions-quartz-impl</artifactId>
+			<artifactId>isis-extensions-command-replay-impl</artifactId>
 		</dependency>
 
 	</dependencies>
diff --git a/examples/demo/web/src/main/java/demoapp/web/DemoAppManifest.java b/examples/demo/web/src/main/java/demoapp/web/DemoAppManifest.java
index 2029608..d79a04e 100644
--- a/examples/demo/web/src/main/java/demoapp/web/DemoAppManifest.java
+++ b/examples/demo/web/src/main/java/demoapp/web/DemoAppManifest.java
@@ -27,6 +27,7 @@ import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Import;
 
 import org.apache.isis.extensions.commandlog.impl.IsisModuleExtCommandLogImpl;
+import org.apache.isis.extensions.commandreplay.impl.IsisModuleExtCommandReplayImpl;
 import org.apache.isis.extensions.cors.impl.IsisModuleExtCorsImpl;
 import org.apache.isis.extensions.quartz.IsisModuleExtQuartzImpl;
 import org.apache.isis.extensions.secman.encryption.jbcrypt.IsisModuleExtSecmanEncryptionJbcrypt;
@@ -44,7 +45,6 @@ import lombok.extern.log4j.Log4j2;
 
 import demoapp.dom.DemoModule;
 import demoapp.dom._infra.fixtures.DemoFixtureScript;
-import demoapp.web.quartz.BackgroundCommandsQuartzJobConfigurerModule;
 
 /**
  * Makes the integral parts of the 'demo' web application.
@@ -54,10 +54,8 @@ import demoapp.web.quartz.BackgroundCommandsQuartzJobConfigurerModule;
     // @Configuration's
     DemoModule.class, // shared demo core module
 
-    // background commands
-    BackgroundCommandsQuartzJobConfigurerModule.class,
-    IsisModuleExtCommandLogImpl.class,
-    IsisModuleExtQuartzImpl.class,
+    // commands
+    IsisModuleExtCommandReplayImpl.class,
 
     // SECURITY
     IsisModuleSecurityShiro.class,
diff --git a/examples/demo/wicket/src/main/resources/log4j2-spring.xml b/examples/demo/wicket/src/main/resources/log4j2-spring.xml
index 2e788de..5d9f666 100644
--- a/examples/demo/wicket/src/main/resources/log4j2-spring.xml
+++ b/examples/demo/wicket/src/main/resources/log4j2-spring.xml
@@ -53,8 +53,10 @@ under the License.
 		<logger name="DataNucleus.Datastore.Schema" level="info"/>
 		<logger name="DataNucleus.Datastore.Native" level="info"/>
 
+
 		<!-- debugging -->
-		<Logger name="org.apache.isis.applib.services.command.CommandServiceDefault" level="debug" />
+		<Logger name="org.apache.isis.applib.services.command.CommandServiceDefault" level="warn" />
+		<Logger name="org.apache.isis.extensions.commandreplay.impl.job.callables.ReplicateAndRunCommands" level="debug" />
 
 		<Root level="info">
 			<AppenderRef ref="Console" />
diff --git a/extensions/core/command-log/impl/src/main/java/org/apache/isis/extensions/commandlog/impl/jdo/CommandJdo.java b/extensions/core/command-log/impl/src/main/java/org/apache/isis/extensions/commandlog/impl/jdo/CommandJdo.java
index 0b3e6e2..1ba4381 100644
--- a/extensions/core/command-log/impl/src/main/java/org/apache/isis/extensions/commandlog/impl/jdo/CommandJdo.java
+++ b/extensions/core/command-log/impl/src/main/java/org/apache/isis/extensions/commandlog/impl/jdo/CommandJdo.java
@@ -29,6 +29,7 @@ import java.util.function.Consumer;
 
 import javax.jdo.annotations.IdentityType;
 
+import org.springframework.context.event.EventListener;
 import org.springframework.core.annotation.Order;
 import org.springframework.stereotype.Service;
 
@@ -185,14 +186,14 @@ import lombok.extern.log4j.Log4j2;
                     + "   && startedAt != null "
                     + "   && completedAt != null "
                     + "ORDER BY this.timestamp ASC"),
-    // most recent replayable command previously replicated from primary to
+    // 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).
     @javax.jdo.annotations.Query(
             name="findReplayedHwm",
             value="SELECT "
                     + "FROM org.apache.isis.extensions.commandlog.impl.jdo.CommandJdo "
-                    + "WHERE (replayState == 'OK' || replayState == 'FAILED')"
+                    + "WHERE (replayState == 'OK' || replayState == 'FAILED') "
                     + "ORDER BY this.timestamp DESC "
                     + "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
@@ -326,15 +327,27 @@ public class CommandJdo
                 .ifPresent(consume);
     }
 
-    public String title() {
-        // nb: not thread-safe
-        // formats defined in https://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html
-        val format = new SimpleDateFormat("yyyy-MM-dd HH:mm");
+    @Service
+    public static class TitleProvider {
+
+        @EventListener(TitleUiEvent.class)
+        public void on(TitleUiEvent ev) {
+            if(!Objects.equals(ev.getTitle(), "Command Jdo") || ev.getTranslatableTitle() != null) {
+                return;
+            }
+            ev.setTitle(title(ev.getSource()));
+        }
+
+        private static String title(CommandJdo source) {
+            // nb: not thread-safe
+            // formats defined in https://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html
+            val format = new SimpleDateFormat("yyyy-MM-dd HH:mm");
 
-        val buf = new TitleBuffer();
-        buf.append(format.format(getTimestamp()));
-        buf.append(" ").append(getLogicalMemberIdentifier());
-        return buf.toString();
+            val buf = new TitleBuffer();
+            buf.append(format.format(source.getTimestamp()));
+            buf.append(" ").append(source.getLogicalMemberIdentifier());
+            return buf.toString();
+        }
     }
 
 
@@ -414,7 +427,7 @@ public class CommandJdo
     @javax.jdo.annotations.Persistent
     @javax.jdo.annotations.Column(allowsNull="true", length = 2000, name="target")
     @Property(domainEvent = TargetDomainEvent.class)
-    @PropertyLayout(hidden = Where.REFERENCES_PARENT, named = "Object")
+    @PropertyLayout(named = "Object")
     @Getter @Setter
     private Bookmark target;
 
@@ -423,6 +436,13 @@ public class CommandJdo
         return getCommandDto().getMember().getLogicalMemberIdentifier();
     }
 
+    @Property(domainEvent = TargetDomainEvent.class)
+    @PropertyLayout(named = "Member")
+    public String getLocalMember() {
+        val targetMember = getTargetMember();
+        return targetMember.substring(targetMember.indexOf("#") + 1);
+    }
+
     public static class LogicalMemberIdentifierDomainEvent extends PropertyDomainEvent<String> { }
     @Property(domainEvent = LogicalMemberIdentifierDomainEvent.class)
     @PropertyLayout(hidden = Where.ALL_TABLES)
@@ -595,11 +615,9 @@ public class CommandJdo
 
         private List<String> ordered(List<String> propertyIds) {
             return Arrays.asList(
-                "timestamp", "target", "commandDto", "targetMember", "username", "complete", "resultSummary", "uniqueIdStr"
+                "timestamp", "target", "targetMember", "username", "complete", "resultSummary", "uniqueIdStr"
             );
         }
     }
-
-
 }
 
diff --git a/extensions/core/command-log/impl/src/main/java/org/apache/isis/extensions/commandlog/impl/jdo/CommandJdo.layout.fallback.xml b/extensions/core/command-log/impl/src/main/java/org/apache/isis/extensions/commandlog/impl/jdo/CommandJdo.layout.fallback.xml
index fd7c2b2..d69472e 100644
--- a/extensions/core/command-log/impl/src/main/java/org/apache/isis/extensions/commandlog/impl/jdo/CommandJdo.layout.fallback.xml
+++ b/extensions/core/command-log/impl/src/main/java/org/apache/isis/extensions/commandlog/impl/jdo/CommandJdo.layout.fallback.xml
@@ -9,27 +9,36 @@
     <bs3:row>
         <bs3:col span="12" unreferencedActions="true">
             <cpt:domainObject/>
-            <cpt:action id="links"/>
+            <cpt:action id="clearHints" position="PANEL_DROPDOWN"/>
         </bs3:col>
     </bs3:row>
     <bs3:row>
         <bs3:col span="4">
             <bs3:row>
                 <bs3:col span="12">
-                    <cpt:fieldSet name="Identifiers" id="identifiers" unreferencedProperties="true">
-                        <cpt:action id="recentAuditEntries" position="PANEL_DROPDOWN"/>
-                        <cpt:action id="findChangesByDate" position="PANEL_DROPDOWN"/>
-                        <cpt:action id="recentChanges" position="PANEL_DROPDOWN"/>
-                        <cpt:action id="clearHints" position="PANEL_DROPDOWN"/>
+                    <cpt:fieldSet name="Identifiers" id="identifiers">
                         <cpt:action id="downloadLayoutXml" position="PANEL_DROPDOWN"/>
-                        <cpt:action id="downloadJdoMetadata" position="PANEL_DROPDOWN"/>
                         <cpt:action id="rebuildMetamodel" position="PANEL_DROPDOWN"/>
+                        <cpt:action id="downloadMetamodelXml" position="PANEL_DROPDOWN"/>
+                        <cpt:action id="inspectMetamodel" position="PANEL_DROPDOWN"/>
+                        <cpt:action id="downloadJdoMetadata" position="PANEL_DROPDOWN"/>
+                        <cpt:action id="openRestApi" position="PANEL_DROPDOWN"/>
                         <cpt:property id="type"/>
-                        <cpt:property id="transactionId"/>
-                        <cpt:property id="memberIdentifier"/>
-                        <cpt:property id="user"/>
+                        <cpt:property id="uniqueIdStr"/>
+                        <cpt:property id="logicalMemberIdentifier"/>
+                    </cpt:fieldSet>
+                    <cpt:fieldSet name="Who and When">
+                        <cpt:property id="username"/>
                         <cpt:property id="timestamp"/>
                     </cpt:fieldSet>
+                    <cpt:fieldSet name="Other" id="other" unreferencedProperties="true">
+                    </cpt:fieldSet>
+                </bs3:col>
+                <bs3:col span="0">
+                    <cpt:fieldSet name="Hidden">
+                        <cpt:property id="preValue" hidden="EVERYWHERE"/>
+                        <cpt:property id="postValue" hidden="EVERYWHERE"/>
+                    </cpt:fieldSet>
                 </bs3:col>
             </bs3:row>
             <bs3:row>
@@ -39,10 +48,11 @@
                             <bs3:row>
                                 <bs3:col span="12">
                                     <cpt:fieldSet name="Target" id="target">
-                                        <cpt:property id="targetClass"/>
-                                        <cpt:property id="targetAction"/>
-                                        <cpt:property id="propertyId"/>
-                                        <cpt:property id="targetStr" hidden="ALL_TABLES"/>
+                                        <cpt:property id="targetObjectType"/>
+                                        <cpt:property id="localMember" hidden="OBJECT_FORMS"/>
+                                        <cpt:property id="target">
+                                            <cpt:action id="open"/>
+                                        </cpt:property>
                                     </cpt:fieldSet>
                                     <cpt:fieldSet name="Notes" id="notes"/>
                                 </bs3:col>
@@ -65,11 +75,8 @@
         <bs3:col span="8">
             <bs3:row>
                 <bs3:col span="6">
-                    <cpt:fieldSet name="Arguments" id="arguments">
-                        <cpt:property id="arguments" labelPosition="TOP"/>
-                        <cpt:property id="preValue"  hidden="ALL_TABLES"/>
-                        <cpt:property id="postValue" hidden="ALL_TABLES"/>
-                        <cpt:property id="memento" labelPosition="TOP" multiLine="20"/>
+                    <cpt:fieldSet name="DTO" id="arguments">
+                        <cpt:property id="commandDto" labelPosition="NONE" multiLine="20"/>
                     </cpt:fieldSet>
                 </bs3:col>
                 <bs3:col span="6">
@@ -90,7 +97,7 @@
                     </cpt:fieldSet>
                     <cpt:fieldSet name="Results" id="results">
                         <cpt:property id="resultSummary"/>
-                        <cpt:property id="resultStr"/>
+                        <cpt:property id="result"/>
                         <cpt:property id="exception" labelPosition="TOP"/>
                     </cpt:fieldSet>
                 </bs3:col>
diff --git a/extensions/core/command-log/impl/src/main/java/org/apache/isis/extensions/commandlog/impl/jdo/CommandJdoRepository.java b/extensions/core/command-log/impl/src/main/java/org/apache/isis/extensions/commandlog/impl/jdo/CommandJdoRepository.java
index 6bd6a4f..1f632a7 100644
--- a/extensions/core/command-log/impl/src/main/java/org/apache/isis/extensions/commandlog/impl/jdo/CommandJdoRepository.java
+++ b/extensions/core/command-log/impl/src/main/java/org/apache/isis/extensions/commandlog/impl/jdo/CommandJdoRepository.java
@@ -30,7 +30,6 @@ import javax.annotation.Nullable;
 import javax.inject.Inject;
 import javax.inject.Named;
 import javax.inject.Provider;
-import javax.jdo.JDOQLTypedQuery;
 
 import org.joda.time.LocalDate;
 import org.springframework.beans.factory.annotation.Qualifier;
@@ -54,7 +53,6 @@ import org.apache.isis.schema.cmd.v2.MapDto;
 import org.apache.isis.schema.common.v2.OidDto;
 
 import lombok.val;
-import lombok.var;
 import lombok.extern.log4j.Log4j2;
 
 /**
@@ -222,7 +220,7 @@ public class CommandJdoRepository {
      * typically we expect that the secondary will be set up to run against a
      * copy of the primary instance's DB (restored from a backup), in which
      * case there will already be a {@link CommandJdo command} representing the
-     * current high water mark on the slave.
+     * current high water mark on the secondary system.
      *
      * If the unique id is not null but the corresponding
      * {@link CommandJdo command} is not found, then <tt>null</tt> is returned.
@@ -232,7 +230,7 @@ public class CommandJdoRepository {
      * the primary.
      *
      * @param uniqueId - the identifier of the {@link CommandJdo command} being
-     *                   the replay hwm (using {@link #findReplayHwm()} on the
+     *                   the replay hwm (using {@link #findReplayedHwm()} on the
      *                   secondary), or null if no HWM was found there.
      * @param batchSize - to restrict the number returned (so that replay
      *                   commands can be batched).
@@ -289,7 +287,7 @@ public class CommandJdoRepository {
 
 
     /**
-     * The most recent replayable command previously replicated from primary to
+     * The most recent replayed command previously replicated from primary to
      * secondary.
      *
      * <p>
@@ -303,12 +301,12 @@ public class CommandJdoRepository {
      * production database was restored to the secondary
      * </p>
      */
-    public CommandJdo findReplayHwm() {
+    public CommandJdo findReplayedHwm() {
 
         // most recent replayable command, replicated from primary to secondary
         return repositoryService.firstMatch(
                 new QueryDefault<>(
-                        CommandJdo.class, "findReplayableHwm"))
+                        CommandJdo.class, "findReplayedHwm"))
                 .orElseGet(() -> {
             // otherwise, the most recent completed command, run on the secondary,
             // this corresponds to a command restored from a copy of
diff --git a/extensions/core/command-log/impl/src/main/java/org/apache/isis/extensions/commandlog/impl/mixins/HasUniqueId_command.java b/extensions/core/command-log/impl/src/main/java/org/apache/isis/extensions/commandlog/impl/mixins/HasUniqueId_command.java
index 3766317..f5da020 100644
--- a/extensions/core/command-log/impl/src/main/java/org/apache/isis/extensions/commandlog/impl/mixins/HasUniqueId_command.java
+++ b/extensions/core/command-log/impl/src/main/java/org/apache/isis/extensions/commandlog/impl/mixins/HasUniqueId_command.java
@@ -44,7 +44,7 @@ public class HasUniqueId_command {
      * {@link Command#getParent() parent} property.
      */
     public boolean hideAct() {
-        return (hasUniqueId instanceof Command);
+        return (hasUniqueId instanceof CommandJdo);
     }
     public String disableAct() {
         return findCommand() == null ? "No command found for unique Id": null;
@@ -57,6 +57,5 @@ public class HasUniqueId_command {
                 .orElse(null);
     }
 
-    @Inject
-    CommandJdoRepository commandServiceRepository;
+    @Inject CommandJdoRepository commandServiceRepository;
 }
diff --git a/extensions/core/command-replay/impl/pom.xml b/extensions/core/command-replay/impl/pom.xml
index e58de94..195e92b 100644
--- a/extensions/core/command-replay/impl/pom.xml
+++ b/extensions/core/command-replay/impl/pom.xml
@@ -25,8 +25,8 @@
     <packaging>jar</packaging>
 
     <description>
-        A module providing a Quartz Job to run on a slave app, 
-		for obtaining commands from a master and saving them so that they are replayed.
+        A module providing a Quartz Job to run on a secondary system,
+		for obtaining commands from a primary and saving them so that they are replayed.
     </description>
 
     <properties>
diff --git a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/IsisModuleExtCommandReplayImpl.java b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/IsisModuleExtCommandReplayImpl.java
index e04d51a..a7d27ff 100644
--- a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/IsisModuleExtCommandReplayImpl.java
+++ b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/IsisModuleExtCommandReplayImpl.java
@@ -1,33 +1,64 @@
 package org.apache.isis.extensions.commandreplay.impl;
 
 
+import javax.inject.Inject;
+
+import org.quartz.JobDetail;
+import org.quartz.Scheduler;
+import org.quartz.SchedulerException;
+import org.quartz.SimpleTrigger;
+import org.quartz.Trigger;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Import;
+import org.springframework.context.annotation.Profile;
+import org.springframework.scheduling.quartz.JobDetailFactoryBean;
+import org.springframework.scheduling.quartz.SchedulerFactoryBean;
+import org.springframework.scheduling.quartz.SimpleTriggerFactoryBean;
+import org.springframework.scheduling.quartz.SpringBeanJobFactory;
 
+import org.apache.isis.core.config.IsisConfiguration;
 import org.apache.isis.extensions.commandlog.impl.IsisModuleExtCommandLogImpl;
+import org.apache.isis.extensions.commandreplay.impl.analysis.CommandReplayAnalyserException;
+import org.apache.isis.extensions.commandreplay.impl.analysis.CommandReplayAnalyserResult;
 import org.apache.isis.extensions.commandreplay.impl.executor.CommandExecutorServiceWithTime;
-import org.apache.isis.extensions.commandreplay.impl.fetch.MasterConfiguration;
+import org.apache.isis.extensions.commandreplay.impl.fetch.CommandFetcher;
+import org.apache.isis.extensions.commandreplay.impl.fetch.PrimaryConfig;
 import org.apache.isis.extensions.commandreplay.impl.clock.TickingClockService;
 import org.apache.isis.extensions.commandreplay.impl.analysis.CommandReplayAnalysisService;
+import org.apache.isis.extensions.commandreplay.impl.fetch.SecondaryConfig;
+import org.apache.isis.extensions.commandreplay.impl.job.ReplicateAndReplayJob;
 import org.apache.isis.extensions.commandreplay.impl.ui.CommandReplayOnPrimaryService;
 import org.apache.isis.extensions.commandreplay.impl.ui.CommandReplayOnSecondaryService;
+import org.apache.isis.extensions.quartz.IsisModuleExtQuartzImpl;
+import org.apache.isis.extensions.quartz.spring.AutowiringSpringBeanJobFactory;
+
+import lombok.val;
 
 @Configuration
 @Import({
         // @Configuration's
         IsisModuleExtCommandLogImpl.class,
+        IsisModuleExtQuartzImpl.class,
 
         // @DomainService's
         CommandExecutorServiceWithTime.class,
+        CommandFetcher.class,
+        CommandReplayAnalyserResult.class,
+        CommandReplayAnalyserException.class,
         CommandReplayAnalysisService.class,
         CommandReplayOnPrimaryService.class,
         CommandReplayOnSecondaryService.class,
         TickingClockService.class,
 
         // @Service's
-        MasterConfiguration.class,
+        PrimaryConfig.class,
+        SecondaryConfig.class,
 
 })
+@Profile("secondary")
 public class IsisModuleExtCommandReplayImpl {
 
     public abstract static class ActionDomainEvent<S>
@@ -39,4 +70,60 @@ public class IsisModuleExtCommandReplayImpl {
     public abstract static class PropertyDomainEvent<S,T>
             extends org.apache.isis.applib.events.domain.PropertyDomainEvent<S,T> { }
 
+
+    @Inject ApplicationContext applicationContext;
+    @Inject IsisConfiguration isisConfiguration;
+
+    @Bean(name = "ReplicateAndReplayJob")
+    public JobDetailFactoryBean replicateAndReplayJobDetailFactory() {
+        val jobDetailFactory = new JobDetailFactoryBean();
+        jobDetailFactory.setJobClass(ReplicateAndReplayJob.class);
+        jobDetailFactory.setDescription("Replicate commands from primary and replay on secondary");
+        jobDetailFactory.setDurability(true);
+        return jobDetailFactory;
+    }
+
+    @Bean(name = "ReplicateAndReplayTrigger" )
+    public SimpleTriggerFactoryBean replicateAndReplayTriggerFactory(@Qualifier("ReplicateAndReplayJob") JobDetail job) {
+        val triggerFactory = new SimpleTriggerFactoryBean();
+        triggerFactory.setJobDetail(job);
+        val config = isisConfiguration.getExtensions().getCommandReplay().getSecondary().getQuartzReplicateAndReplayJob();
+        triggerFactory.setRepeatInterval(config.getRepeatInterval());
+        triggerFactory.setStartDelay(config.getStartDelay());
+        triggerFactory.setRepeatCount(SimpleTrigger.REPEAT_INDEFINITELY);
+        return triggerFactory;
+    }
+
+    @Bean(name = "ReplicateAndReplaySbjf")
+    public SpringBeanJobFactory springBeanJobFactory() {
+        val jobFactory = new AutowiringSpringBeanJobFactory();
+        jobFactory.setApplicationContext(applicationContext);
+        return jobFactory;
+    }
+
+    @Bean(name = "ReplicateAndReplaySfb")
+    public SchedulerFactoryBean scheduler(
+            @Qualifier("ReplicateAndReplayTrigger") final Trigger trigger,
+            @Qualifier("ReplicateAndReplayJob") final JobDetail jobDetail,
+            @Qualifier("ReplicateAndReplaySbjf") final SpringBeanJobFactory sbjf) {
+        val schedulerFactory = new SchedulerFactoryBean();
+
+        schedulerFactory.setJobFactory(sbjf);
+        schedulerFactory.setJobDetails(jobDetail);
+        schedulerFactory.setTriggers(trigger);
+
+        return schedulerFactory;
+    }
+
+    @Bean(name = "ReplicateAndReplayScheduler")
+    public Scheduler scheduler(
+            @Qualifier("ReplicateAndReplayTrigger") final Trigger trigger,
+            @Qualifier("ReplicateAndReplayJob") final JobDetail job,
+            @Qualifier("ReplicateAndReplaySfb") final SchedulerFactoryBean factory)
+            throws SchedulerException {
+        val scheduler = factory.getScheduler();
+        scheduler.start();
+        return scheduler;
+    }
+
 }
diff --git a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/SlaveStatus.java b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/SecondaryStatus.java
similarity index 87%
rename from extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/SlaveStatus.java
rename to extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/SecondaryStatus.java
index 9c75e7e..1cc6c96 100644
--- a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/SlaveStatus.java
+++ b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/SecondaryStatus.java
@@ -1,6 +1,6 @@
 package org.apache.isis.extensions.commandreplay.impl;
 
-public enum SlaveStatus {
+public enum SecondaryStatus {
     TICKING_CLOCK_STATUS_UNKNOWN,
     TICKING_CLOCK_NOT_YET_INITIALIZED,
     OK,
diff --git a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/StatusException.java b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/StatusException.java
index ff01054..6ba46e6 100644
--- a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/StatusException.java
+++ b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/StatusException.java
@@ -2,13 +2,13 @@ package org.apache.isis.extensions.commandreplay.impl;
 
 
 public class StatusException extends Exception {
-    public final SlaveStatus slaveStatus;
+    public final SecondaryStatus secondaryStatus;
 
-    public StatusException(SlaveStatus slaveStatus) {
-        this(slaveStatus, null);
+    public StatusException(SecondaryStatus secondaryStatus) {
+        this(secondaryStatus, null);
     }
-    public StatusException(SlaveStatus slaveStatus, final Exception ex) {
+    public StatusException(SecondaryStatus secondaryStatus, final Exception ex) {
         super(ex);
-        this.slaveStatus = slaveStatus;
+        this.secondaryStatus = secondaryStatus;
     }
 }
diff --git a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/analysis/CommandReplayAnalyser.java b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/analysis/CommandReplayAnalyser.java
index 5931dde..e4d3e50 100644
--- a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/analysis/CommandReplayAnalyser.java
+++ b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/analysis/CommandReplayAnalyser.java
@@ -4,7 +4,11 @@ import org.apache.isis.extensions.commandlog.impl.jdo.CommandJdo;
 
 public interface CommandReplayAnalyser {
 
-    String analyzeReplay(
-            final CommandJdo commandJdo);
+    /**
+     *
+     * @param commandJdo
+     * @return - if not <code>null</code>, indicates the reason that there was an issue replaying the command.
+     */
+    String analyzeReplay(final CommandJdo commandJdo);
 
 }
diff --git a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/analysis/CommandReplayAnalyserAbstract.java b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/analysis/CommandReplayAnalyserAbstract.java
deleted file mode 100644
index 1454d8b..0000000
--- a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/analysis/CommandReplayAnalyserAbstract.java
+++ /dev/null
@@ -1,52 +0,0 @@
-package org.apache.isis.extensions.commandreplay.impl.analysis;
-
-import java.util.Map;
-
-import javax.annotation.PostConstruct;
-
-import org.apache.isis.applib.annotation.Programmatic;
-import org.apache.isis.core.config.metamodel.facets.Util;
-import org.apache.isis.extensions.commandlog.impl.jdo.CommandJdo;
-
-public abstract class CommandReplayAnalyserAbstract implements CommandReplayAnalyser {
-
-    private final String key;
-    private final String defaultValue;
-
-    public CommandReplayAnalyserAbstract(final String key) {
-        this(key, "enabled");
-    }
-
-    public CommandReplayAnalyserAbstract(final String key, final String defaultValue) {
-        this.key = key;
-        this.defaultValue = defaultValue;
-    }
-
-    private boolean enabled;
-
-    @PostConstruct
-    public void init(final Map<String,String> properties) {
-        final String anslysisStr = getPropertyElse(properties, key, defaultValue);
-        enabled = Util.parseYes(anslysisStr);
-    }
-
-
-    @Programmatic
-    public final String analyzeReplay(final CommandJdo commandJdo) {
-
-        if(!enabled) {
-            return null;
-        }
-        return doAnalyzeReplay(commandJdo);
-
-    }
-
-    protected abstract String doAnalyzeReplay(final CommandJdo command);
-
-    private static String getPropertyElse(final Map<String, String> properties, final String key, final String dflt) {
-        final String str = properties.get(key);
-        return str != null ? str : dflt;
-    }
-
-
-}
diff --git a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/analysis/CommandReplayAnalyserException.java b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/analysis/CommandReplayAnalyserException.java
new file mode 100644
index 0000000..5039a11
--- /dev/null
+++ b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/analysis/CommandReplayAnalyserException.java
@@ -0,0 +1,65 @@
+package org.apache.isis.extensions.commandreplay.impl.analysis;
+
+import javax.annotation.PostConstruct;
+
+import com.google.common.base.Objects;
+
+import org.apache.isis.applib.annotation.DomainService;
+import org.apache.isis.applib.services.conmap.command.UserDataKeys;
+import org.apache.isis.applib.util.schema.CommandDtoUtils;
+import org.apache.isis.core.config.IsisConfiguration;
+import org.apache.isis.extensions.commandlog.impl.jdo.CommandJdo;
+import org.apache.isis.schema.cmd.v2.CommandDto;
+
+import lombok.RequiredArgsConstructor;
+import lombok.val;
+
+@DomainService()
+@RequiredArgsConstructor
+public class CommandReplayAnalyserException implements CommandReplayAnalyser {
+
+    private final IsisConfiguration isisConfiguration;
+    private boolean enabled;
+
+    @PostConstruct
+    public void init() {
+        enabled = isisConfiguration.getExtensions().getCommandReplay().getSecondary().getAnalyser().getResult().isEnabled();
+    }
+
+    @Override
+    public String analyzeReplay(final CommandJdo commandJdo) {
+        if(!enabled) {
+            return null;
+        }
+
+        val dto = commandJdo.getCommandDto();
+
+        val primaryException = CommandDtoUtils.getUserData(dto, UserDataKeys.EXCEPTION);
+        if (primaryException == null) {
+            return null;
+        }
+
+        val replayedException = commandJdo.getException();
+
+        val primaryExceptionTrimmed = trimmed(primaryException);
+        val replayedExceptionTrimmed = trimmed(replayedException);
+        return Objects.equal(primaryExceptionTrimmed, replayedExceptionTrimmed)
+                ? null
+                : String.format("Exceptions differ.  On primary system was '%s'", primaryException);
+    }
+
+    private String trimmed(final String str) {
+        return withoutWhitespace(initialPartOfStackTrace(str));
+    }
+
+    // we only look at beginning of the stack trace because the latter part will differ when replayed
+    private String initialPartOfStackTrace(final String str) {
+        final int toInspectOfStackTrace = 500;
+        return str.length() > toInspectOfStackTrace ? str.substring(0, toInspectOfStackTrace) : str;
+    }
+
+    private String withoutWhitespace(final String s) {
+        return s.replaceAll("\\s", "");
+    }
+
+}
diff --git a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/analysis/CommandReplayAnalyserExceptionStr.java b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/analysis/CommandReplayAnalyserExceptionStr.java
deleted file mode 100644
index 7437631..0000000
--- a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/analysis/CommandReplayAnalyserExceptionStr.java
+++ /dev/null
@@ -1,56 +0,0 @@
-package org.apache.isis.extensions.commandreplay.impl.analysis;
-
-import com.google.common.base.Objects;
-
-import org.apache.isis.applib.annotation.DomainService;
-import org.apache.isis.applib.services.conmap.command.UserDataKeys;
-import org.apache.isis.applib.util.schema.CommandDtoUtils;
-import org.apache.isis.core.commons.internal.exceptions._Exceptions;
-import org.apache.isis.extensions.commandlog.impl.jdo.CommandJdo;
-import org.apache.isis.schema.cmd.v2.CommandDto;
-
-@DomainService()
-public class CommandReplayAnalyserExceptionStr extends CommandReplayAnalyserAbstract {
-
-    public static final String ANALYSIS_KEY = "isis.services."
-            + CommandReplayAnalyserExceptionStr.class.getSimpleName()
-            + ".analysis";
-
-    public CommandReplayAnalyserExceptionStr() {
-        super(ANALYSIS_KEY);
-    }
-
-    protected String doAnalyzeReplay(final CommandJdo commandJdo) {
-
-        final CommandDto dto = commandJdo.getCommandDto();
-
-        final String primaryException =
-                CommandDtoUtils.getUserData(dto, UserDataKeys.EXCEPTION);
-        if (primaryException == null) {
-            return null;
-        }
-
-        final String replayedException = commandJdo.getException();
-
-        final String masterExceptionTrimmed = trimmed(primaryException);
-        final String replayedExceptionTrimmed = trimmed(replayedException);
-        return Objects.equal(masterExceptionTrimmed, replayedExceptionTrimmed)
-                ? null
-                : String.format("Exceptions differ.  Master was '%s'", primaryException);
-    }
-
-    private String trimmed(final String str) {
-        return withoutWhitespace(initialPartOfStackTrace(str));
-    }
-
-    // we only look at beginning of the stack trace because the latter part will differ when replayed
-    private String initialPartOfStackTrace(final String str) {
-        final int toInspectOfStackTrace = 500;
-        return str.length() > toInspectOfStackTrace ? str.substring(0, toInspectOfStackTrace) : str;
-    }
-
-    private String withoutWhitespace(final String s) {
-        return s.replaceAll("\\s", "");
-    }
-
-}
diff --git a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/analysis/CommandReplayAnalyserResultStr.java b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/analysis/CommandReplayAnalyserResult.java
similarity index 55%
rename from extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/analysis/CommandReplayAnalyserResultStr.java
rename to extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/analysis/CommandReplayAnalyserResult.java
index fd5b4dd..3db2db2 100644
--- a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/analysis/CommandReplayAnalyserResultStr.java
+++ b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/analysis/CommandReplayAnalyserResult.java
@@ -1,6 +1,6 @@
 package org.apache.isis.extensions.commandreplay.impl.analysis;
 
-import java.util.Optional;
+import javax.annotation.PostConstruct;
 
 import com.google.common.base.Objects;
 
@@ -9,31 +9,39 @@ import org.apache.isis.applib.services.bookmark.Bookmark;
 import org.apache.isis.applib.services.conmap.command.UserDataKeys;
 import org.apache.isis.applib.util.schema.CommandDtoUtils;
 
+import org.apache.isis.core.config.IsisConfiguration;
 import org.apache.isis.extensions.commandlog.impl.jdo.CommandJdo;
 import org.apache.isis.schema.cmd.v2.CommandDto;
 
+import lombok.RequiredArgsConstructor;
+import lombok.val;
+
 @DomainService()
-public class CommandReplayAnalyserResultStr extends CommandReplayAnalyserAbstract {
+@RequiredArgsConstructor
+public class CommandReplayAnalyserResult implements CommandReplayAnalyser {
 
-    public static final String ANALYSIS_KEY = "isis.services."
-            + CommandReplayAnalyserResultStr.class.getSimpleName() +
-            ".analysis";
+    private final IsisConfiguration isisConfiguration;
+    private boolean enabled;
 
-    public CommandReplayAnalyserResultStr() {
-        super(ANALYSIS_KEY);
+    @PostConstruct
+    public void init() {
+        enabled = isisConfiguration.getExtensions().getCommandReplay().getSecondary().getAnalyser().getException().isEnabled();
     }
 
-    protected String doAnalyzeReplay(final CommandJdo commandJdo) {
+    @Override
+    public String analyzeReplay(final CommandJdo commandJdo) {
+        if(!enabled) {
+            return null;
+        }
 
         final CommandDto dto = commandJdo.getCommandDto();
 
         // see if the outcome was the same...
         // ... either the same result when replayed
-        final String primaryResultStr =
-                CommandDtoUtils.getUserData(dto, UserDataKeys.RESULT);
+        val primaryResultStr = CommandDtoUtils.getUserData(dto, UserDataKeys.RESULT);
 
-        final Bookmark secondaryResult = commandJdo.getResult();
-        final String secondaryResultStr =
+        val secondaryResult = commandJdo.getResult();
+        val secondaryResultStr =
                 secondaryResult != null ? secondaryResult.toString() : null;
         return Objects.equal(primaryResultStr, secondaryResultStr)
                 ? null
diff --git a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/clock/TickingClockService.java b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/clock/TickingClockService.java
index 5f60ab0..0f7a798 100644
--- a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/clock/TickingClockService.java
+++ b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/clock/TickingClockService.java
@@ -7,6 +7,8 @@ import java.util.function.Supplier;
 import javax.annotation.PostConstruct;
 import javax.inject.Inject;
 
+import org.springframework.context.annotation.Profile;
+
 import org.apache.isis.applib.annotation.DomainService;
 import org.apache.isis.applib.annotation.Programmatic;
 import org.apache.isis.applib.clock.Clock;
@@ -17,17 +19,20 @@ import lombok.val;
 import lombok.extern.log4j.Log4j2;
 
 /**
- * If configured as the slave, then sets up to use {@link TickingFixtureClock} so that time can be changed dynamically
- * when running.
+ * Only enabled for the <tt>secondary</tt> profile, where it sets up the
+ * framework to use {@link TickingFixtureClock} so that time can be changed
+ * dynamically when running.
  *
  * <p>
- *     If the configuration keys for a replay slave are not provided, then the service will not initialize.
+ *     As an additional safeguard, if the configuration keys to access the
+ *     primary are not provided, then the service will not initialize.
  * </p>
  *
  * <p>
- *     IMPORTANT: the methods provided by this service are not thread-safe, because the clock is a globally-scoped
- *     singleton rather than a thread-local.  This method should therefore only be used in single-user systems,
- *     eg a replay slave.
+ *     IMPORTANT: the methods provided by this service are not thread-safe,
+ *     because the clock is a globally-scoped singleton rather than a
+ *     thread-local.  These methods should therefore only be used in single-user
+ *     systems, eg a replay secondary.
  * </p>
  */
 @DomainService()
@@ -38,14 +43,14 @@ public class TickingClockService {
 
     @PostConstruct
     public void init() {
-        Optional<String> baseUrl = isisConfiguration.getExtensions().getCommandReplay().getMaster().getBaseUrl();
-        Optional<String> user = isisConfiguration.getExtensions().getCommandReplay().getMaster().getUser();
-        Optional<String> password = isisConfiguration.getExtensions().getCommandReplay().getMaster().getPassword();
+        val baseUrl = isisConfiguration.getExtensions().getCommandReplay().getPrimary().getBaseUrl();
+        val user = isisConfiguration.getExtensions().getCommandReplay().getPrimary().getUser();
+        val password = isisConfiguration.getExtensions().getCommandReplay().getPrimary().getPassword();
 
         if( !baseUrl.isPresent()||
             !user.isPresent() ||
             !password.isPresent()) {
-            log.info("init() - skipping, one or more 'isis.extensions.command-replay.master' configuration constants missing");
+            log.info("init() - skipping, one or more 'isis.extensions.command-replay.primary' configuration constants missing");
             return;
         }
 
@@ -53,22 +58,22 @@ public class TickingClockService {
         TickingFixtureClock.replaceExisting();
     }
 
-    @Programmatic
     public boolean isInitialized() {
         return Clock.getInstance() instanceof TickingFixtureClock;
     }
 
 
     /**
-     * Executes the runnable, setting the clock to be the specified time beforehand (and reinstating it to its original
-     * time afterwards).
+     * Executes the runnable, setting the clock to be the specified time
+     * beforehand (and reinstating it to its original time afterwards).
      *
      * <p>
-     *     IMPORTANT: this method is not thread-safe, because the clock is a globally-scoped singleton rather than a
-     *     thread-local.  This method should therefore only be used in single-user systems, eg a replay slave.
+     *     IMPORTANT: this method is not thread-safe, because the clock is a
+     *     globally-scoped singleton rather than a thread-local.  This method
+     *     should therefore only be used in single-user systems, eg a replay
+     *     secondary.
      * </p>
      */
-    @Programmatic
     public void at(Timestamp timestamp, Runnable runnable) {
         ensureInitialized();
 
@@ -85,15 +90,16 @@ public class TickingClockService {
     }
 
     /**
-     * Executes the callable, setting the clock to be the specified time beforehand (and reinstating it to its original
-     * time afterwards).
+     * Executes the callable, setting the clock to be the specified time
+     * beforehand (and reinstating it to its original time afterwards).
      *
      * <p>
-     *     IMPORTANT: this method is not thread-safe, because the clock is a globally-scoped singleton rather than a
-     *     thread-local.  This method should therefore only be used in single-user systems, eg a replay slave.
+     *     IMPORTANT: this method is not thread-safe, because the clock is a
+     *     globally-scoped singleton rather than a thread-local.  This method
+     *     should therefore only be used in single-user systems, eg a replay
+     *     secondary.
      * </p>
      */
-    @Programmatic
     public <T> T at(Timestamp timestamp, Supplier<T> supplier) {
         ensureInitialized();
 
@@ -114,7 +120,7 @@ public class TickingClockService {
     private void ensureInitialized() {
         if(!isInitialized()) {
             throw new IllegalStateException(
-                    "Not initialized.  Make sure that the application is configured to run as a replay slave");
+                    "Not initialized.  Make sure that the application is configured to run as a replay secondary");
         }
     }
 
diff --git a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/executor/CommandExecutorServiceWithTime.java b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/executor/CommandExecutorServiceWithTime.java
index 2a35c40..6169627 100644
--- a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/executor/CommandExecutorServiceWithTime.java
+++ b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/executor/CommandExecutorServiceWithTime.java
@@ -22,6 +22,8 @@ import java.util.function.Supplier;
 
 import javax.inject.Named;
 
+import org.springframework.beans.factory.annotation.Qualifier;
+
 import org.apache.isis.applib.annotation.DomainService;
 import org.apache.isis.applib.jaxb.JavaSqlXMLGregorianCalendarMarshalling;
 import org.apache.isis.applib.services.bookmark.Bookmark;
@@ -49,7 +51,8 @@ public class CommandExecutorServiceWithTime implements CommandExecutorService {
     final TickingClockService tickingClockService;
 
     public CommandExecutorServiceWithTime(
-            @Named("Default")
+            //@Named("isisRuntimeServices.CommandExecutorServiceDefault")
+            @Qualifier("Default")
             CommandExecutorService delegate
             , TickingClockService tickingClockService) {
         this.delegate = delegate;
diff --git a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/fetch/CommandFetcher.java b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/fetch/CommandFetcher.java
index 0f034e6..27ec9a7 100644
--- a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/fetch/CommandFetcher.java
+++ b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/fetch/CommandFetcher.java
@@ -9,11 +9,9 @@ import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriBuilder;
 
 import org.apache.isis.applib.annotation.DomainService;
-import org.apache.isis.applib.annotation.Programmatic;
-import org.apache.isis.applib.services.command.Command;
 import org.apache.isis.applib.services.jaxb.JaxbService;
 import org.apache.isis.extensions.commandlog.impl.jdo.CommandJdo;
-import org.apache.isis.extensions.commandreplay.impl.SlaveStatus;
+import org.apache.isis.extensions.commandreplay.impl.SecondaryStatus;
 import org.apache.isis.extensions.commandreplay.impl.StatusException;
 import org.apache.isis.extensions.jaxrsclient.applib.client.JaxRsClient;
 import org.apache.isis.extensions.jaxrsclient.applib.client.JaxRsResponse;
@@ -29,7 +27,7 @@ import lombok.extern.log4j.Log4j2;
 public class CommandFetcher {
 
     static final String URL_SUFFIX =
-            "services/isisextcommandlog.CommandReplayOnMasterService/actions/findCommandsOnMasterSince/invoke";
+            "services/isisExtensionsCommandReplay.CommandReplayOnPrimaryService/actions/findCommandsOnPrimarySince/invoke";
 
 
     /**
@@ -43,7 +41,7 @@ public class CommandFetcher {
             final CommandJdo previousHwm)
             throws StatusException {
 
-        log.debug("finding command on master ...");
+        log.debug("finding command on primary ...");
 
         final CommandsDto commandsDto = fetchCommands(previousHwm);
 
@@ -65,11 +63,11 @@ public class CommandFetcher {
     private CommandsDto fetchCommands(final CommandJdo previousHwm) throws StatusException {
         final UUID transactionId = previousHwm != null ? previousHwm.getUniqueId() : null;
 
-        log.debug("finding commands on master ...");
+        log.debug("finding commands on primary ...");
 
         final URI uri = buildUri(transactionId);
 
-        final JaxRsResponse response = callMaster(uri);
+        final JaxRsResponse response = callPrimary(uri);
 
         final CommandsDto commandsDto = unmarshal(response, uri);
 
@@ -86,22 +84,22 @@ public class CommandFetcher {
                 transactionId != null
                         ? String.format(
                         "%s%s?transactionId=%s&batchSize=%d",
-                        masterConfiguration.getMasterBaseUrl(), URL_SUFFIX, transactionId, masterConfiguration.getMasterBatchSize())
+                        primaryConfig.getBaseUrl(), URL_SUFFIX, transactionId, primaryConfig.getBatchSize())
                         : String.format(
                         "%s%s?batchSize=%d",
-                        masterConfiguration.getMasterBaseUrl(), URL_SUFFIX, masterConfiguration.getMasterBatchSize())
+                        primaryConfig.getBaseUrl(), URL_SUFFIX, primaryConfig.getBatchSize())
         );
         final URI uri = uriBuilder.build();
         log.info("uri = {}", uri);
         return uri;
     }
 
-    private JaxRsResponse callMaster(final URI uri) throws StatusException {
+    private JaxRsResponse callPrimary(final URI uri) throws StatusException {
         final JaxRsResponse response;
         final JaxRsClient jaxRsClient = new JaxRsClientDefault();
         try {
-            final String user = masterConfiguration.getMasterUser();
-            final String password = masterConfiguration.getMasterPassword();
+            final String user = primaryConfig.getUser();
+            final String password = primaryConfig.getPassword();
             response = jaxRsClient.get(uri, CommandsDto.class, JaxRsClient.ReprType.ACTION_RESULT, user, password);
             int status = response.getStatus();
             if(status != Response.Status.OK.getStatusCode()) {
@@ -111,11 +109,11 @@ public class CommandFetcher {
                 } else {
                     log.warn("status: {}, unable to read entity from response", status);
                 }
-                throw new StatusException(SlaveStatus.REST_CALL_FAILING);
+                throw new StatusException(SecondaryStatus.REST_CALL_FAILING);
             }
         } catch(Exception ex) {
             log.warn("rest call failed", ex);
-            throw new StatusException(SlaveStatus.REST_CALL_FAILING, ex);
+            throw new StatusException(SecondaryStatus.REST_CALL_FAILING, ex);
         }
         return response;
     }
@@ -130,7 +128,7 @@ public class CommandFetcher {
             log.debug("commands:\n{}", entity);
         } catch(Exception ex) {
             log.warn("unable to unmarshal entity from {} to CommandsDto.class; was:\n{}", uri, entity);
-            throw new StatusException(SlaveStatus.FAILED_TO_UNMARSHALL_RESPONSE, ex);
+            throw new StatusException(SecondaryStatus.FAILED_TO_UNMARSHALL_RESPONSE, ex);
         }
         return commandsDto;
     }
@@ -144,6 +142,6 @@ public class CommandFetcher {
     }
 
     @Inject
-    MasterConfiguration masterConfiguration;
+    PrimaryConfig primaryConfig;
 
 }
\ No newline at end of file
diff --git a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/fetch/MasterConfiguration.java b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/fetch/MasterConfiguration.java
deleted file mode 100644
index 727f7d5..0000000
--- a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/fetch/MasterConfiguration.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package org.apache.isis.extensions.commandreplay.impl.fetch;
-
-import javax.validation.constraints.NotNull;
-
-import org.springframework.stereotype.Service;
-
-import org.apache.isis.core.config.IsisConfiguration;
-
-import lombok.Getter;
-import lombok.val;
-import lombok.extern.log4j.Log4j2;
-
-@Service
-@Log4j2
-public class MasterConfiguration {
-
-    @Getter final String masterUser;
-    @Getter final String masterPassword;
-    @Getter final String masterBaseUrl;
-    @Getter final int masterBatchSize;
-
-    public MasterConfiguration(@NotNull IsisConfiguration isisConfiguration) {
-        val masterConfig = isisConfiguration.getExtensions().getCommandReplay().getMaster();
-        masterUser = masterConfig.getUser().orElse(null);
-        masterPassword = masterConfig.getPassword().orElse(null);
-        masterBaseUrl = masterConfig.getBaseUrl()
-                            .map(x -> !x.endsWith("/") ? x + "/" : x)
-                            .orElse(null);
-        masterBatchSize = masterConfig.getBatchSize();
-    }
-
-
-    public boolean isConfigured() {
-        return masterUser != null &&
-               masterPassword != null &&
-               masterBaseUrl != null;
-    }
-}
diff --git a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/fetch/PrimaryConfig.java b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/fetch/PrimaryConfig.java
new file mode 100644
index 0000000..81fb315
--- /dev/null
+++ b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/fetch/PrimaryConfig.java
@@ -0,0 +1,38 @@
+package org.apache.isis.extensions.commandreplay.impl.fetch;
+
+import javax.validation.constraints.NotNull;
+
+import org.springframework.stereotype.Service;
+
+import org.apache.isis.core.config.IsisConfiguration;
+
+import lombok.Getter;
+import lombok.val;
+import lombok.extern.log4j.Log4j2;
+
+@Service
+@Log4j2
+public class PrimaryConfig {
+
+    @Getter final String user;
+    @Getter final String password;
+    @Getter final String baseUrl;
+    @Getter final int batchSize;
+
+    public PrimaryConfig(@NotNull final IsisConfiguration isisConfiguration) {
+        val config = isisConfiguration.getExtensions().getCommandReplay().getPrimary();
+        user = config.getUser().orElse(null);
+        password = config.getPassword().orElse(null);
+        baseUrl = config.getBaseUrl()
+                            .map(x -> !x.endsWith("/") ? x + "/" : x)
+                            .orElse(null);
+        batchSize = config.getBatchSize();
+    }
+
+
+    public boolean isConfigured() {
+        return user != null &&
+               password != null &&
+               baseUrl != null;
+    }
+}
diff --git a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/fetch/SecondaryConfig.java b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/fetch/SecondaryConfig.java
new file mode 100644
index 0000000..4680303
--- /dev/null
+++ b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/fetch/SecondaryConfig.java
@@ -0,0 +1,32 @@
+package org.apache.isis.extensions.commandreplay.impl.fetch;
+
+import java.util.List;
+
+import javax.validation.constraints.NotNull;
+
+import org.springframework.stereotype.Service;
+
+import org.apache.isis.core.config.IsisConfiguration;
+
+import lombok.Getter;
+import lombok.val;
+import lombok.extern.log4j.Log4j2;
+
+@Service
+@Log4j2
+public class SecondaryConfig {
+
+    @Getter final String user;
+    @Getter final List<String> roles;
+
+    public SecondaryConfig(@NotNull final IsisConfiguration isisConfiguration) {
+        val config = isisConfiguration.getExtensions().getCommandReplay().getSecondary();
+        user = config.getQuartzSession().getUser();
+        roles = config.getQuartzSession().getRoles();
+    }
+
+    public boolean isConfigured() {
+        return user != null &&
+               roles != null;
+    }
+}
diff --git a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/job/ReplicateAndReplayJob.java b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/job/ReplicateAndReplayJob.java
new file mode 100644
index 0000000..cde212b
--- /dev/null
+++ b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/job/ReplicateAndReplayJob.java
@@ -0,0 +1,93 @@
+package org.apache.isis.extensions.commandreplay.impl.job;
+
+import javax.inject.Inject;
+
+import org.quartz.DisallowConcurrentExecution;
+import org.quartz.Job;
+import org.quartz.JobExecutionContext;
+import org.quartz.PersistJobDataAfterExecution;
+
+import org.apache.isis.applib.services.xactn.TransactionService;
+import org.apache.isis.core.runtime.iactn.IsisInteractionFactory;
+import org.apache.isis.core.security.authentication.AuthenticationSession;
+import org.apache.isis.core.security.authentication.standard.SimpleSession;
+
+import org.apache.isis.extensions.commandreplay.impl.fetch.SecondaryConfig;
+import org.apache.isis.extensions.commandreplay.impl.job.callables.IsTickingClockInitialized;
+import org.apache.isis.extensions.commandreplay.impl.job.callables.ReplicateAndRunCommands;
+import org.apache.isis.extensions.commandreplay.impl.fetch.PrimaryConfig;
+import org.apache.isis.extensions.commandreplay.impl.SecondaryStatus;
+
+import lombok.val;
+import lombok.extern.log4j.Log4j2;
+
+@DisallowConcurrentExecution
+@PersistJobDataAfterExecution
+@Log4j2
+public class ReplicateAndReplayJob implements Job {
+
+    @Inject PrimaryConfig primaryConfig;
+    @Inject SecondaryConfig secondaryConfig;
+
+    AuthenticationSession authSession;
+
+    public void execute(final JobExecutionContext quartzContext) {
+
+        // figure out if this instance is configured to run as primary or secondary
+        new SecondaryStatusData(quartzContext);
+
+        if(primaryConfig.isConfigured() && secondaryConfig.isConfigured()) {
+            authSession = new SimpleSession(secondaryConfig.getUser(), secondaryConfig.getRoles());
+            exec(quartzContext);
+        }
+    }
+
+    @Inject protected TransactionService transactionService;
+    @Inject protected IsisInteractionFactory isisInteractionFactory;
+
+    private void exec(final JobExecutionContext quartzContext) {
+        val ssh = new SecondaryStatusData(quartzContext);
+        val secondaryStatus = ssh.getSecondaryStatus(SecondaryStatus.TICKING_CLOCK_STATUS_UNKNOWN);
+
+        switch (secondaryStatus) {
+
+            case TICKING_CLOCK_STATUS_UNKNOWN:
+            case TICKING_CLOCK_NOT_YET_INITIALIZED:
+                ssh.setSecondaryStatus(
+                        isTickingClockInitialized(authSession)
+                            ? SecondaryStatus.OK
+                            : SecondaryStatus.TICKING_CLOCK_NOT_YET_INITIALIZED);
+                if(ssh.getSecondaryStatus() == SecondaryStatus.OK) {
+                    log.info("Ticking clock now initialised");
+                } else {
+                    log.info("Still waiting for ticking clock to be initialised: {}" , secondaryStatus);
+                }
+                return;
+
+            case OK:
+                val newStatus =
+                        isisInteractionFactory.callAuthenticated(authSession, new ReplicateAndRunCommands());
+
+                if(newStatus != null) {
+                    ssh.setSecondaryStatus(newStatus);
+                }
+                return;
+
+            case REST_CALL_FAILING:
+            case FAILED_TO_UNMARSHALL_RESPONSE:
+            case UNKNOWN_STATE:
+                log.warn("skipped - configured as secondary, however: {}" , secondaryStatus);
+                return;
+            default:
+                throw new IllegalStateException("Unrecognised status: " + secondaryStatus);
+        }
+    }
+
+    private boolean isTickingClockInitialized(final AuthenticationSession authSession) {
+
+        return isisInteractionFactory.callAuthenticated(authSession, new IsTickingClockInitialized());
+    }
+
+
+}
+
diff --git a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/job/SecondaryStatusData.java b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/job/SecondaryStatusData.java
new file mode 100644
index 0000000..d9f3d73
--- /dev/null
+++ b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/job/SecondaryStatusData.java
@@ -0,0 +1,36 @@
+package org.apache.isis.extensions.commandreplay.impl.job;
+
+import org.quartz.JobExecutionContext;
+
+import org.apache.isis.extensions.commandreplay.impl.SecondaryStatus;
+import org.apache.isis.extensions.quartz.context.JobExecutionData;
+
+import lombok.val;
+import lombok.extern.log4j.Log4j2;
+
+@Log4j2
+class SecondaryStatusData {
+
+    private static final String KEY_SECONDARY_STATUS = SecondaryStatusData.class.getCanonicalName();
+
+    private final JobExecutionData jobExecutionData;
+
+    SecondaryStatusData(final JobExecutionContext jobExecutionContext) {
+        this.jobExecutionData = new JobExecutionData((jobExecutionContext));
+    }
+
+    SecondaryStatus getSecondaryStatus() {
+        return getSecondaryStatus(SecondaryStatus.UNKNOWN_STATE);
+    }
+
+    SecondaryStatus getSecondaryStatus(final SecondaryStatus defaultStatus) {
+        val mode = jobExecutionData.getString( KEY_SECONDARY_STATUS, defaultStatus.name());
+        return SecondaryStatus.valueOf(mode);
+    }
+
+    void setSecondaryStatus(final SecondaryStatus mode) {
+        jobExecutionData.setString(KEY_SECONDARY_STATUS, mode.name());
+    }
+
+}
+
diff --git a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/job/callables/IsTickingClockInitialized.java b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/job/callables/IsTickingClockInitialized.java
new file mode 100644
index 0000000..9a8b6c9
--- /dev/null
+++ b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/job/callables/IsTickingClockInitialized.java
@@ -0,0 +1,22 @@
+package org.apache.isis.extensions.commandreplay.impl.job.callables;
+
+import java.util.concurrent.Callable;
+
+import javax.inject.Inject;
+
+import org.apache.isis.applib.services.xactn.TransactionService;
+import org.apache.isis.extensions.commandreplay.impl.clock.TickingClockService;
+
+public class IsTickingClockInitialized implements Callable<Boolean> {
+
+    @Inject
+    TransactionService transactionService;
+    @Inject
+    TickingClockService tickingClockService;
+
+    @Override
+    public Boolean call() {
+        return transactionService.executeWithinTransaction(
+                () -> tickingClockService.isInitialized());
+    }
+}
diff --git a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/executor/ReplayableCommandExecution.java b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/job/callables/ReplicateAndRunCommands.java
similarity index 54%
rename from extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/executor/ReplayableCommandExecution.java
rename to extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/job/callables/ReplicateAndRunCommands.java
index 24602e0..2b805c7 100644
--- a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/executor/ReplayableCommandExecution.java
+++ b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/job/callables/ReplicateAndRunCommands.java
@@ -1,55 +1,50 @@
-package org.apache.isis.extensions.commandreplay.impl.executor;
+package org.apache.isis.extensions.commandreplay.impl.job.callables;
 
-import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.Callable;
 
 import javax.inject.Inject;
 
-import org.apache.isis.applib.services.bookmark.Bookmark;
 import org.apache.isis.applib.services.command.CommandExecutorService;
 import org.apache.isis.applib.services.xactn.TransactionService;
-import org.apache.isis.core.runtime.iactn.template.AbstractIsisInteractionTemplate;
+import org.apache.isis.extensions.commandlog.impl.jdo.CommandJdo;
+import org.apache.isis.extensions.commandlog.impl.jdo.CommandJdoRepository;
 import org.apache.isis.extensions.commandlog.impl.jdo.ReplayState;
-import org.apache.isis.extensions.commandreplay.impl.SlaveStatus;
+import org.apache.isis.extensions.commandreplay.impl.SecondaryStatus;
 import org.apache.isis.extensions.commandreplay.impl.StatusException;
 import org.apache.isis.extensions.commandreplay.impl.analysis.CommandReplayAnalysisService;
-import org.apache.isis.extensions.commandreplay.impl.fetch.MasterConfiguration;
 import org.apache.isis.extensions.commandreplay.impl.fetch.CommandFetcher;
 import org.apache.isis.extensions.commandreplay.impl.spi.ReplayCommandExecutionController;
-import org.apache.isis.extensions.commandreplay.impl.util.Holder;
-import org.apache.isis.schema.cmd.v2.CommandDto;
-
-import org.apache.isis.extensions.commandlog.impl.jdo.CommandJdo;
-import org.apache.isis.extensions.commandlog.impl.jdo.CommandJdoRepository;
 
+import lombok.val;
 import lombok.extern.log4j.Log4j2;
 
 @Log4j2
-public class ReplayableCommandExecution extends AbstractIsisInteractionTemplate {
-
-    private final MasterConfiguration masterConfiguration;
+public class ReplicateAndRunCommands implements Callable<SecondaryStatus> {
 
-    public ReplayableCommandExecution(final MasterConfiguration masterConfiguration) {
-        this.masterConfiguration = masterConfiguration;
-    }
+    @Inject CommandExecutorService commandExecutorService;
+    @Inject TransactionService transactionService;
+    @Inject CommandFetcher commandFetcher;
+    @Inject CommandJdoRepository commandJdoRepository;
+    @Inject CommandReplayAnalysisService analysisService;
+    @Inject Optional<ReplayCommandExecutionController> controller;
 
-    @Inject
-    CommandExecutorService commandExecutorService;
 
     @Override
-    protected void doExecute(final Object context) {
-        Holder<SlaveStatus> holder = (Holder<SlaveStatus>) context;
+    public SecondaryStatus call() throws Exception {
         try {
-            replicateAndRunCommands();
+            doCall();
+            return SecondaryStatus.OK;
         } catch (StatusException e) {
-            holder.setObject(e.slaveStatus);
+            return e.secondaryStatus;
         }
     }
 
-    private void replicateAndRunCommands() throws  StatusException  {
+    private void doCall() throws  StatusException  {
 
         CommandJdo hwmCommand = null;
         if(!isRunning()) {
-            log.debug("ReplayableCommandExecution is paused");
+            log.debug("ReplicateAndRunCommands is paused");
             return;
         }
 
@@ -57,18 +52,18 @@ public class ReplayableCommandExecution extends AbstractIsisInteractionTemplate
 
             if(hwmCommand == null) {
                 // first time through the loop we need to find the HWM command
-                // (subsequent iterations we use the command from before as the HWM)
-                log.debug("searching for hwm on slave ...");
-                hwmCommand = commandJdoRepository.findReplayHwm();
+                // on the secondary.  On subsequent iterations we use the
+                // command from before as the HWM
+                log.debug("searching for hwm on secondary ...");
+                hwmCommand = commandJdoRepository.findReplayedHwm();
             }
 
             if(hwmCommand == null) {
-                log.debug("could not find HWM on slave, breaking");
+                log.debug("could not find HWM on secondary, breaking");
                 return;
-
             }
 
-            log.debug("current hwm transactionId = {} {} {}",
+            log.debug("current hwm {} @ {} : {}",
                     hwmCommand.getUniqueId(), hwmCommand.getTimestamp(),
                     hwmCommand.getLogicalMemberIdentifier());
 
@@ -94,9 +89,9 @@ public class ReplayableCommandExecution extends AbstractIsisInteractionTemplate
 
             if(fetchNext) {
                 //
-                // replicate next command from master (if any)
+                // replicate next command from primary (if any)
                 //
-                final CommandDto commandDto = commandFetcher.fetchCommand(hwmCommand);
+                val commandDto = commandFetcher.fetchCommand(hwmCommand);
                 if (commandDto == null) {
                     log.info("No more commands found, breaking out");
                     return;
@@ -117,53 +112,41 @@ public class ReplayableCommandExecution extends AbstractIsisInteractionTemplate
             //
             // find child commands, and run them
             //
-            final CommandJdo parent = hwmCommand;
-            final List<CommandJdo> childCommands =
+            val parent = hwmCommand;
+            val childCommands =
                     transactionService.executeWithinTransaction(
                         () -> commandJdoRepository.findByParent(parent));
-            for (final CommandJdo childCommand : childCommands) {
+            for (val childCommand : childCommands) {
                 executeCommandInTran(childCommand);
             }
 
-
             //
             // if hit an issue, then mark this as in error.
             // this will effectively block the running of any further commands until the adminstrator fixes
             //
-            transactionService.executeWithinTransaction(() -> analysisService.analyse(parent));
+            transactionService.executeWithinTransaction(() -> {
+                analysisService.analyse(parent);
+            });
         }
     }
 
     private void executeCommandInTran(final CommandJdo command) {
+        val commandDto = command.getCommandDto();
         transactionService.executeWithinTransaction(
-                () -> commandExecutorService.executeCommand(CommandExecutorService.SudoPolicy.SWITCH, command.getCommandDto()));
+                () -> {
+                    commandExecutorService.executeCommand(
+                            CommandExecutorService.SudoPolicy.SWITCH, commandDto);
+                });
     }
 
     private boolean isRunning() {
+        return controller
+                .map( control -> transactionService.executeWithinTransaction(control::getState))
+                .map(state -> state == ReplayCommandExecutionController.State.RUNNING)
+            // if no controller implementation provided, then just continue
+            .orElse(true);
 
-        // if no controller implementation provided, then just continue
-        if (controller == null) {
-            return true;
-        }
-
-        final ReplayCommandExecutionController.State state =
-                transactionService.executeWithinTransaction(() -> controller.getState());
-
-        // if null, then not yet initialized, so fail back to not running
-        if(state == null) {
-            return false;
-        }
-
-        return state == ReplayCommandExecutionController.State.RUNNING;
     }
 
 
-    @Inject TransactionService transactionService;
-    @Inject
-    CommandFetcher commandFetcher;
-    @Inject
-    CommandJdoRepository commandJdoRepository;
-    @Inject
-    CommandReplayAnalysisService analysisService;
-    @Inject ReplayCommandExecutionController controller;
 }
\ No newline at end of file
diff --git a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/mixins/CommandJdo_openOnMaster.java b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/mixins/CommandJdo_openOnPrimary.java
similarity index 87%
rename from extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/mixins/CommandJdo_openOnMaster.java
rename to extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/mixins/CommandJdo_openOnPrimary.java
index 6734cc4..c5af120 100644
--- a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/mixins/CommandJdo_openOnMaster.java
+++ b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/mixins/CommandJdo_openOnPrimary.java
@@ -16,15 +16,15 @@ import org.apache.isis.extensions.commandreplay.impl.IsisModuleExtCommandReplayI
 
 @Action(
         semantics = SemanticsOf.SAFE,
-        domainEvent = CommandJdo_openOnMaster.ActionDomainEvent.class
+        domainEvent = CommandJdo_openOnPrimary.ActionDomainEvent.class
 )
-public class CommandJdo_openOnMaster<T> {
+public class CommandJdo_openOnPrimary<T> {
 
     public static class ActionDomainEvent
-            extends IsisModuleExtCommandReplayImpl.ActionDomainEvent<CommandJdo_openOnMaster> { }
+            extends IsisModuleExtCommandReplayImpl.ActionDomainEvent<CommandJdo_openOnPrimary> { }
 
     private final CommandJdo commandJdo;
-    public CommandJdo_openOnMaster(CommandJdo commandJdo) {
+    public CommandJdo_openOnPrimary(CommandJdo commandJdo) {
         this.commandJdo = commandJdo;
     }
 
@@ -45,7 +45,7 @@ public class CommandJdo_openOnMaster<T> {
     }
 
     private String lookupBaseUrlPrefix() {
-        return isisConfiguration.getExtensions().getCommandReplay().getMaster().getBaseUrlEndUser()
+        return isisConfiguration.getExtensions().getCommandReplay().getPrimary().getBaseUrlEndUser()
                 .map(x -> !x.endsWith("/") ? x + "/" : x)
                 .map(x -> x + "wicket/entity/")
                 .orElse(null);
diff --git a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/mixins/CommandJdo_replayNext.java b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/mixins/CommandJdo_replayNext.java
index 5c85d9f..040595a 100644
--- a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/mixins/CommandJdo_replayNext.java
+++ b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/mixins/CommandJdo_replayNext.java
@@ -1,7 +1,5 @@
 package org.apache.isis.extensions.commandreplay.impl.mixins;
 
-import java.util.List;
-
 import javax.inject.Inject;
 
 import org.apache.isis.applib.annotation.Action;
@@ -12,7 +10,7 @@ import org.apache.isis.applib.services.message.MessageService;
 import org.apache.isis.extensions.commandlog.impl.jdo.CommandJdo;
 import org.apache.isis.extensions.commandlog.impl.jdo.CommandJdoRepository;
 import org.apache.isis.extensions.commandreplay.impl.IsisModuleExtCommandReplayImpl;
-import org.apache.isis.extensions.commandreplay.impl.fetch.MasterConfiguration;
+import org.apache.isis.extensions.commandreplay.impl.fetch.PrimaryConfig;
 import org.apache.isis.extensions.commandreplay.impl.StatusException;
 import org.apache.isis.schema.cmd.v2.CommandDto;
 
@@ -38,7 +36,7 @@ public class CommandJdo_replayNext {
     public CommandJdo act() throws StatusException {
 
         // double check this is still the HWM
-        final CommandJdo replayHwm = commandJdoRepository.findReplayHwm();
+        final CommandJdo replayHwm = commandJdoRepository.findReplayedHwm();
         if(commandJdo != replayHwm) {
             messageService.informUser("HWM has changed");
             return replayHwm;
@@ -46,7 +44,7 @@ public class CommandJdo_replayNext {
 
         final CommandJdo nextHwm = fetchNext();
         if(nextHwm == null) {
-            messageService.informUser("No more commands on master");
+            messageService.informUser("No more commands on primary system");
             return commandJdo;
         }
 
@@ -77,10 +75,10 @@ public class CommandJdo_replayNext {
     }
 
     public String disableAct() {
-        final CommandJdo replayHwm = commandJdoRepository.findReplayHwm();
+        final CommandJdo replayHwm = commandJdoRepository.findReplayedHwm();
 
         if(commandJdo != replayHwm) {
-            return "This action can only be performed against the 'HWM' command on the slave";
+            return "This action can only be performed against the 'HWM' command on the secondary";
         }
         if(commandJdo.getReplayState() != null && commandJdo.getReplayState().isFailed()) {
             return "Replayable command is in error.  Exclude the command to continue.";
@@ -93,7 +91,7 @@ public class CommandJdo_replayNext {
     }
 
     public boolean hideAct() {
-        return !masterConfiguration.isConfigured();
+        return !primaryConfig.isConfigured();
     }
 
 
@@ -102,7 +100,7 @@ public class CommandJdo_replayNext {
     @Inject CommandFetcher commandFetcher;
     @Inject CommandExecutorService commandExecutorService;
     @Inject
-    MasterConfiguration masterConfiguration;
+    PrimaryConfig primaryConfig;
     @Inject MessageService messageService;
     @Inject CommandReplayAnalysisService analysisService;
 }
diff --git a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/mixins/CommandJdo_replayQueue.java b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/mixins/CommandJdo_replayQueue.java
index 2bc91e6..f4b20b3 100644
--- a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/mixins/CommandJdo_replayQueue.java
+++ b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/mixins/CommandJdo_replayQueue.java
@@ -11,7 +11,7 @@ import org.apache.isis.applib.annotation.Mixin;
 import org.apache.isis.extensions.commandlog.impl.jdo.CommandJdo;
 import org.apache.isis.extensions.commandlog.impl.jdo.CommandJdoRepository;
 import org.apache.isis.extensions.commandreplay.impl.IsisModuleExtCommandReplayImpl;
-import org.apache.isis.extensions.commandreplay.impl.fetch.MasterConfiguration;
+import org.apache.isis.extensions.commandreplay.impl.fetch.PrimaryConfig;
 
 @Collection(
         domainEvent = CommandJdo_replayQueue.CollectionDomainEvent.class
@@ -36,11 +36,11 @@ public class CommandJdo_replayQueue {
     }
 
     public boolean hideColl() {
-        return !masterConfiguration.isConfigured();
+        return !primaryConfig.isConfigured();
     }
 
     @Inject
-    MasterConfiguration masterConfiguration;
+    PrimaryConfig primaryConfig;
     @Inject
     CommandJdoRepository commandJdoRepository;
 
diff --git a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/quartz/QuartzConfigKeys.java b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/quartz/QuartzConfigKeys.java
deleted file mode 100644
index 43357bb..0000000
--- a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/quartz/QuartzConfigKeys.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package org.apache.isis.extensions.commandreplay.impl.quartz;
-
-import lombok.experimental.UtilityClass;
-
-@UtilityClass
-public class QuartzConfigKeys {
-
-    static final String SLAVE_USER_QUARTZ_KEY  = "user";
-    static final String SLAVE_USER_DEFAULT     = "replay_user";
-    static final String SLAVE_ROLES_QUARTZ_KEY = "roles";
-    static final String SLAVE_ROLES_DEFAULT    = "replay_role";
-
-}
diff --git a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/quartz/RunBackgroundCommandsWithReplicationAndReplayJob.java b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/quartz/RunBackgroundCommandsWithReplicationAndReplayJob.java
deleted file mode 100644
index 7a955b3..0000000
--- a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/quartz/RunBackgroundCommandsWithReplicationAndReplayJob.java
+++ /dev/null
@@ -1,171 +0,0 @@
-package org.apache.isis.extensions.commandreplay.impl.quartz;
-
-import javax.inject.Inject;
-
-import com.google.common.base.Splitter;
-
-import org.quartz.DisallowConcurrentExecution;
-import org.quartz.Job;
-import org.quartz.JobExecutionContext;
-import org.quartz.PersistJobDataAfterExecution;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import org.apache.isis.core.config.IsisConfiguration;
-import org.apache.isis.core.runtime.iactn.template.AbstractIsisInteractionTemplate;
-import org.apache.isis.core.security.authentication.AuthenticationSession;
-import org.apache.isis.core.security.authentication.standard.SimpleSession;
-
-import org.apache.isis.extensions.commandreplay.impl.clock.TickingClockService;
-import org.apache.isis.extensions.commandreplay.impl.util.Holder;
-import org.apache.isis.extensions.commandreplay.impl.executor.ReplayableCommandExecution;
-import org.apache.isis.extensions.commandreplay.impl.fetch.MasterConfiguration;
-import org.apache.isis.extensions.commandreplay.impl.SlaveStatus;
-
-import static org.apache.isis.extensions.commandreplay.impl.quartz.QuartzConfigKeys.SLAVE_ROLES_DEFAULT;
-import static org.apache.isis.extensions.commandreplay.impl.quartz.QuartzConfigKeys.SLAVE_ROLES_QUARTZ_KEY;
-import static org.apache.isis.extensions.commandreplay.impl.quartz.QuartzConfigKeys.SLAVE_USER_DEFAULT;
-import static org.apache.isis.extensions.commandreplay.impl.quartz.QuartzConfigKeys.SLAVE_USER_QUARTZ_KEY;
-
-import lombok.val;
-import lombok.extern.log4j.Log4j2;
-
-@DisallowConcurrentExecution
-@PersistJobDataAfterExecution
-@Log4j2
-public class RunBackgroundCommandsWithReplicationAndReplayJob implements Job {
-
-    AuthenticationSession authSession;
-    MasterConfiguration masterConfiguration;
-
-    public void execute(final JobExecutionContext quartzContext) {
-
-        // figure out if this instance is configured to run as master or slave
-        authSession = new SimpleSessionFromQuartz(quartzContext);
-        final IsisConfiguration isisConfiguration = lookupIsisConfiguration(authSession);
-        masterConfiguration = new MasterConfiguration(isisConfiguration);
-
-        if(masterConfiguration.isConfigured()) {
-            runBackgroundCommandsOnSlave(quartzContext);
-        }
-    }
-
-    private void runBackgroundCommandsOnSlave(final JobExecutionContext quartzContext) {
-        final SlaveStatus slaveStatus =
-                getSlaveStatus(quartzContext, SlaveStatus.TICKING_CLOCK_STATUS_UNKNOWN);
-
-        switch (slaveStatus) {
-
-            case TICKING_CLOCK_STATUS_UNKNOWN:
-            case TICKING_CLOCK_NOT_YET_INITIALIZED:
-                setSlaveStatus(quartzContext,
-                        lookupTickingClockServiceStatus(authSession)
-                            ? SlaveStatus.OK
-                            : SlaveStatus.TICKING_CLOCK_NOT_YET_INITIALIZED);
-                return;
-
-            case OK:
-                Holder<SlaveStatus> holder = new Holder<>();
-                new ReplayableCommandExecution(masterConfiguration).execute(authSession, holder);
-                final SlaveStatus newStatus = holder.getObject();
-                if(newStatus != null) {
-                    setSlaveStatus(quartzContext, newStatus);
-                }
-                return;
-
-
-            case REST_CALL_FAILING:
-            case FAILED_TO_UNMARSHALL_RESPONSE:
-            case UNKNOWN_STATE:
-                log.warn("skipped - configured as slave, however: {}" ,slaveStatus);
-                return;
-            default:
-                throw new IllegalStateException("Unrecognised status: " + slaveStatus);
-        }
-
-    }
-
-    private IsisConfiguration lookupIsisConfiguration(final AuthenticationSession authSession) {
-
-        final Holder<IsisConfiguration> holder = new Holder<>();
-        new AbstractIsisInteractionTemplate() {
-            @Override
-            protected void doExecuteWithTransaction(final Object unused) {
-                holder.setObject(isisConfiguration);
-            }
-
-            @Inject
-            IsisConfiguration isisConfiguration;
-        }.execute(authSession, null);
-
-        return holder.getObject();
-    }
-
-    private boolean lookupTickingClockServiceStatus(final AuthenticationSession authSession) {
-
-        final Holder<Boolean> holder = new Holder<>();
-        new AbstractIsisInteractionTemplate() {
-            @Override
-            protected void doExecuteWithTransaction(final Object unused) {
-                holder.setObject(tickingClockService.isInitialized());
-            }
-
-            @Inject
-            TickingClockService tickingClockService;
-        }.execute(authSession, null);
-
-        return holder.getObject();
-    }
-
-    static class SimpleSessionFromQuartz extends SimpleSession {
-
-        private static String getUser(final JobExecutionContext quartzContext) {
-            return getString(quartzContext, SLAVE_USER_QUARTZ_KEY, SLAVE_USER_DEFAULT);
-        }
-
-        private static Iterable<String> getRoles(final JobExecutionContext quartzContext) {
-            val slaveRoles = getString(quartzContext, SLAVE_ROLES_QUARTZ_KEY, SLAVE_ROLES_DEFAULT);
-            return Splitter.on(",").split(
-                    slaveRoles);
-        }
-
-        SimpleSessionFromQuartz(final JobExecutionContext quartzContext) {
-            super(getUser(quartzContext), getRoles(quartzContext));
-        }
-    }
-
-
-    private static final String KEY_SLAVE_STATUS = "slaveStatus";
-
-    /**
-     * Lookup from quartz configuration for this job.
-     */
-    private static String getString(JobExecutionContext context, String key, final String defaultValue) {
-        try {
-            String v = context.getJobDetail().getJobDataMap().getString(key);
-            return v != null ? v : defaultValue;
-        } catch (Exception e) {
-            return defaultValue;
-        }
-    }
-
-
-    /**
-     * Save into quartz configuration for this job, for next invocation.
-     */
-    private static void setString(JobExecutionContext context, String key, String value) {
-        context.getJobDetail().getJobDataMap().put(key, value);
-    }
-    private static SlaveStatus getSlaveStatus(
-            final JobExecutionContext context,
-            final SlaveStatus defaultStatus) {
-        String mode = getString(context, KEY_SLAVE_STATUS, defaultStatus.name());
-        return SlaveStatus.valueOf(mode);
-    }
-
-    private static void setSlaveStatus(final JobExecutionContext context, final SlaveStatus mode) {
-        setString(context, KEY_SLAVE_STATUS, mode.name());
-    }
-
-}
-
diff --git a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/spi/ReplayCommandExecutionController.java b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/spi/ReplayCommandExecutionController.java
index 261d3ec..c2fec8d 100644
--- a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/spi/ReplayCommandExecutionController.java
+++ b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/spi/ReplayCommandExecutionController.java
@@ -1,7 +1,14 @@
 package org.apache.isis.extensions.commandreplay.impl.spi;
 
-import org.apache.isis.applib.annotation.Programmatic;
-
+/**
+ * Optional SPI that allows the replicate and replay job to be paused if
+ * required.
+ *
+ * <p>
+ * If no implementation is configured, then replication/replay will continue
+ * without interruption.
+ * </p>
+ */
 public interface ReplayCommandExecutionController {
 
     enum State {
diff --git a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/ui/CommandReplayOnPrimaryService.java b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/ui/CommandReplayOnPrimaryService.java
index 7087893..f0570e0 100644
--- a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/ui/CommandReplayOnPrimaryService.java
+++ b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/ui/CommandReplayOnPrimaryService.java
@@ -67,7 +67,7 @@ public class CommandReplayOnPrimaryService {
     @Action(domainEvent = FindCommandsOnPrimarySinceDomainEvent.class, semantics = SemanticsOf.SAFE)
     @ActionLayout(cssClassFa = "fa-files-o")
     @MemberOrder(sequence="40")
-    public List<CommandJdo> findCommandsOnMasterSince(
+    public List<CommandJdo> findCommandsOnPrimarySince(
             @Nullable
             @ParameterLayout(named="Transaction Id")
             final UUID transactionId,
@@ -81,13 +81,13 @@ public class CommandReplayOnPrimaryService {
         }
         return commands;
     }
-    public Integer default1FindCommandsOnMasterSince() {
+    public Integer default1FindCommandsOnPrimarySince() {
         return 25;
     }
 
 
 
-    public static class DownloadCommandsOnMasterSinceDomainEvent extends ActionDomainEvent { }
+    public static class DownloadCommandsOnPrimarySinceDomainEvent extends ActionDomainEvent { }
     /**
      * These actions should be called with HTTP Accept Header set to:
      * <code>application/xml;profile="urn:org.restfulobjects:repr-types/action-result";x-ro-domain-type="org.apache.isis.schema.cmd.v1.CommandsDto"</code>
@@ -98,10 +98,10 @@ public class CommandReplayOnPrimaryService {
      * @return
      * @throws NotFoundException - if the command with specified transaction cannot be found.
      */
-    @Action(domainEvent = DownloadCommandsOnMasterSinceDomainEvent.class, semantics = SemanticsOf.SAFE)
+    @Action(domainEvent = DownloadCommandsOnPrimarySinceDomainEvent.class, semantics = SemanticsOf.SAFE)
     @ActionLayout(cssClassFa = "fa-download")
     @MemberOrder(sequence="50")
-    public Clob downloadCommandsOnMasterSince(
+    public Clob downloadCommandsOnPrimarySince(
             @Nullable
             final UUID uniqueId,
             @Nullable
@@ -121,10 +121,10 @@ public class CommandReplayOnPrimaryService {
         final String xml = jaxbService.toXml(commandsDto);
         return new Clob(fileName, "application/xml", xml);
     }
-    public Integer default1DownloadCommandsOnMasterSince() {
+    public Integer default1DownloadCommandsOnPrimarySince() {
         return 25;
     }
-    public String default2DownloadCommandsOnMasterSince() {
+    public String default2DownloadCommandsOnPrimarySince() {
         return "commands_since";
     }
 
diff --git a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/ui/CommandReplayOnSecondaryService.java b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/ui/CommandReplayOnSecondaryService.java
index 5f059d2..629fb3f 100644
--- a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/ui/CommandReplayOnSecondaryService.java
+++ b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/ui/CommandReplayOnSecondaryService.java
@@ -20,6 +20,7 @@ import org.apache.isis.extensions.commandreplay.impl.IsisModuleExtCommandReplayI
 import org.apache.isis.schema.cmd.v2.CommandDto;
 import org.apache.isis.schema.cmd.v2.CommandsDto;
 
+import lombok.val;
 import lombok.extern.log4j.Log4j2;
 
 @DomainService(
@@ -41,7 +42,7 @@ public class CommandReplayOnSecondaryService {
     @ActionLayout(cssClassFa = "fa-bath")
     @MemberOrder(sequence="60.1")
     public CommandJdo findReplayHwmOnSecondary() {
-        return commandJdoRepository.findReplayHwm();
+        return commandJdoRepository.findReplayedHwm();
     }
 
 
@@ -54,15 +55,15 @@ public class CommandReplayOnSecondaryService {
     @ActionLayout(cssClassFa = "fa-upload")
     @MemberOrder(sequence="60.2")
     public void uploadCommandsToSecondary(final Clob commandsDtoAsXml) {
-        final CharSequence chars = commandsDtoAsXml.getChars();
+        val chars = commandsDtoAsXml.getChars();
         List<CommandDto> commandDtoList;
 
         try {
-            final CommandsDto commandsDto = jaxbService.fromXml(CommandsDto.class, chars.toString());
+            val commandsDto = jaxbService.fromXml(CommandsDto.class, chars.toString());
             commandDtoList = commandsDto.getCommandDto();
 
         } catch(Exception ex) {
-            final CommandDto commandDto = jaxbService.fromXml(CommandDto.class, chars.toString());
+            val commandDto = jaxbService.fromXml(CommandDto.class, chars.toString());
             commandDtoList = Collections.singletonList(commandDto);
         }
 
diff --git a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/util/Holder.java b/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/util/Holder.java
deleted file mode 100644
index 257737d..0000000
--- a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/util/Holder.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package org.apache.isis.extensions.commandreplay.impl.util;
-
-import lombok.Data;
-
-@Data
-public class Holder<T> { T object; }
diff --git a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/fetch/RunBackgroundCommandsWithReplicationAndReplayJob_Test.java b/extensions/core/command-replay/impl/src/test/java/org/apache/isis/extensions/commandreplay/impl/fetch/ReplicateAndReplayJob_Test.java
similarity index 94%
rename from extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/fetch/RunBackgroundCommandsWithReplicationAndReplayJob_Test.java
rename to extensions/core/command-replay/impl/src/test/java/org/apache/isis/extensions/commandreplay/impl/fetch/ReplicateAndReplayJob_Test.java
index 4e423cc..3b09f03 100644
--- a/extensions/core/command-replay/impl/src/main/java/org/apache/isis/extensions/commandreplay/impl/fetch/RunBackgroundCommandsWithReplicationAndReplayJob_Test.java
+++ b/extensions/core/command-replay/impl/src/test/java/org/apache/isis/extensions/commandreplay/impl/fetch/ReplicateAndReplayJob_Test.java
@@ -17,7 +17,7 @@ import org.apache.isis.schema.cmd.v2.CommandsDto;
 import lombok.val;
 
 
-public class RunBackgroundCommandsWithReplicationAndReplayJob_Test {
+public class ReplicateAndReplayJob_Test {
 
     @Test
     public void testing_the_unmarshalling() throws Exception {
diff --git a/extensions/core/quartz/adoc/modules/quartz/examples/DemoIsisInteractionTemplate.java b/extensions/core/quartz/adoc/modules/quartz/examples/DemoIsisInteractionTemplate.java
new file mode 100644
index 0000000..216ec96
--- /dev/null
+++ b/extensions/core/quartz/adoc/modules/quartz/examples/DemoIsisInteractionTemplate.java
@@ -0,0 +1,28 @@
+package org.apache.isis.extensions.quartz.jobs;
+
+
+import java.util.Arrays;
+
+import javax.inject.Inject;
+
+import org.quartz.Job;
+import org.quartz.JobExecutionContext;
+
+import org.apache.isis.applib.services.user.UserService;
+import org.apache.isis.core.config.IsisConfiguration;
+import org.apache.isis.core.runtime.iactn.template.AbstractIsisInteractionTemplate;
+import org.apache.isis.core.security.authentication.AuthenticationSession;
+import org.apache.isis.core.security.authentication.standard.SimpleSession;
+
+import lombok.extern.log4j.Log4j2;
+
+//tag::class[]
+@Log4j2
+class DemoIsisInteractionTemplate extends AbstractIsisInteractionTemplate {
+    @Override
+    protected void doExecuteWithTransaction(Object context) {
+        log.debug("Running session via quartz as '{}'", userService.getUser().getName());
+    }
+    @Inject UserService userService;
+}
+//end::class[]
diff --git a/extensions/core/quartz/impl/src/main/java/org/apache/isis/extensions/quartz/jobs/DemoJob.java b/extensions/core/quartz/adoc/modules/quartz/examples/DemoJob.java
similarity index 63%
rename from extensions/core/quartz/impl/src/main/java/org/apache/isis/extensions/quartz/jobs/DemoJob.java
rename to extensions/core/quartz/adoc/modules/quartz/examples/DemoJob.java
index ba51ff8..9392964 100644
--- a/extensions/core/quartz/impl/src/main/java/org/apache/isis/extensions/quartz/jobs/DemoJob.java
+++ b/extensions/core/quartz/adoc/modules/quartz/examples/DemoJob.java
@@ -1,36 +1,37 @@
 package org.apache.isis.extensions.quartz.jobs;
 
 
+import java.util.Arrays;
+
 import javax.inject.Inject;
 
 import org.quartz.Job;
 import org.quartz.JobExecutionContext;
 
+import org.apache.isis.applib.services.user.UserService;
 import org.apache.isis.core.config.IsisConfiguration;
+import org.apache.isis.core.runtime.iactn.template.AbstractIsisInteractionTemplate;
 import org.apache.isis.core.security.authentication.AuthenticationSession;
 import org.apache.isis.core.security.authentication.standard.SimpleSession;
 
-import lombok.val;
 import lombok.extern.log4j.Log4j2;
 
+//tag::class[]
 @Log4j2
 public class DemoJob implements Job {
 
     public void execute(final JobExecutionContext context) {
 
         final AuthenticationSession authSession = newAuthSession(context);
+        new DemoIsisInteractionTemplate().execute(authSession, null);
 
-        log.debug("Running job");
     }
 
     protected AuthenticationSession newAuthSession(JobExecutionContext context) {
-        val user = isisConfiguration.getExtensions().getQuartz().getRunBackgroundCommands().getUser();
-        val roles = isisConfiguration.getExtensions().getQuartz().getRunBackgroundCommands().getRoles();
-        log.debug("background user : {}", user);
-        log.debug("background roles: {}", roles);
-        return new SimpleSession(user, roles);
+        return new SimpleSession("isisModuleExtQuartzDemoUser", Arrays.asList("isisModuleExtQuartzDemoRole"));
     }
 
     @Inject IsisConfiguration isisConfiguration;
 
 }
+//end::class[]
diff --git a/examples/demo/web/src/main/java/demoapp/web/quartz/BackgroundCommandsQuartzJobConfigurerModule.java b/extensions/core/quartz/adoc/modules/quartz/examples/DemoJobQuartzConfigurerModule.java
similarity index 91%
rename from examples/demo/web/src/main/java/demoapp/web/quartz/BackgroundCommandsQuartzJobConfigurerModule.java
rename to extensions/core/quartz/adoc/modules/quartz/examples/DemoJobQuartzConfigurerModule.java
index 86ec11b..da2f046 100644
--- a/examples/demo/web/src/main/java/demoapp/web/quartz/BackgroundCommandsQuartzJobConfigurerModule.java
+++ b/extensions/core/quartz/adoc/modules/quartz/examples/DemoJobQuartzConfigurerModule.java
@@ -14,19 +14,21 @@ import org.springframework.scheduling.quartz.SimpleTriggerFactoryBean;
 import org.springframework.scheduling.quartz.SpringBeanJobFactory;
 
 import org.apache.isis.extensions.quartz.jobs.DemoJob;
+import org.apache.isis.extensions.quartz.spring.AutowiringSpringBeanJobFactory;
 
 import lombok.val;
 import lombok.extern.log4j.Log4j2;
 
+//tag::class[]
 @Configuration
 @Log4j2
-public class BackgroundCommandsQuartzJobConfigurerModule {
+public class DemoJobQuartzConfigurerModule {
 
     @Bean
     public JobDetailFactoryBean jobDetail() {
         val jobDetailFactory = new JobDetailFactoryBean();
         jobDetailFactory.setJobClass(DemoJob.class);
-        jobDetailFactory.setDescription("Run background commands");
+        jobDetailFactory.setDescription("Run demo job");
         jobDetailFactory.setDurability(true);
         return jobDetailFactory;
     }
@@ -63,4 +65,5 @@ public class BackgroundCommandsQuartzJobConfigurerModule {
 
     @Inject ApplicationContext applicationContext;
 }
+//end::class[]
 
diff --git a/extensions/core/quartz/adoc/modules/quartz/pages/about.adoc b/extensions/core/quartz/adoc/modules/quartz/pages/about.adoc
index d37878d..6ce84e5 100644
--- a/extensions/core/quartz/adoc/modules/quartz/pages/about.adoc
+++ b/extensions/core/quartz/adoc/modules/quartz/pages/about.adoc
@@ -2,4 +2,32 @@
 
 :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 [...]
 
-CAUTION: TODO v2 - to document
+CAUTION: TODO v2 - to document.  AbstractIsisInteractionTemplate has been removed.
+
+* Implementation of a Quartz `Job` interface:
++
+[source,java]
+.DemoJob.java
+----
+include::example$DemoJob.java[tag=class]
+----
++
+Domain services (such as `IsisConfiguration`) can be injected into this class.
+
+* The job calls a subclass of `AbstractIsisInteractionTemplate` to perform the work.
+This runs within a transactionm under the user and role specified by `DemoJob`, above:
++
+.DemoIsisInteractionTemplate.java
+[source,java]
+----
+include::example$DemoIsisInteractionTemplate.java[tag=class]
+----
+
+* Finally, the job is configured to run using Spring:
++
+.DemoJobQuartzConfigurerModule.java
+[source,java]
+----
+include::example$DemoJobQuartzConfigurerModule.java[tag=class]
+----
+
diff --git a/extensions/core/quartz/impl/src/main/java/org/apache/isis/extensions/quartz/context/JobExecutionData.java b/extensions/core/quartz/impl/src/main/java/org/apache/isis/extensions/quartz/context/JobExecutionData.java
new file mode 100644
index 0000000..8ae9236
--- /dev/null
+++ b/extensions/core/quartz/impl/src/main/java/org/apache/isis/extensions/quartz/context/JobExecutionData.java
@@ -0,0 +1,36 @@
+package org.apache.isis.extensions.quartz.context;
+
+import org.quartz.JobExecutionContext;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+
+/**
+ * Requires that the job is annotated with the {@link org.quartz.PersistJobDataAfterExecution} annotation.
+ */
+@Log4j2
+@RequiredArgsConstructor
+public class JobExecutionData {
+
+    private final JobExecutionContext context;
+
+    /**
+     * Lookup property from the job detail.
+     */
+    public String getString(String key, final String defaultValue) {
+        try {
+            String v = context.getJobDetail().getJobDataMap().getString(key);
+            return v != null ? v : defaultValue;
+        } catch (Exception e) {
+            return defaultValue;
+        }
+    }
+    /**
+     * Save key into the job detail obtained from context.
+     */
+    public void setString(String key, String value) {
+        context.getJobDetail().getJobDataMap().put(key, value);
+    }
+
+}
+
diff --git a/examples/demo/web/src/main/java/demoapp/web/quartz/AutowiringSpringBeanJobFactory.java b/extensions/core/quartz/impl/src/main/java/org/apache/isis/extensions/quartz/spring/AutowiringSpringBeanJobFactory.java
similarity index 90%
rename from examples/demo/web/src/main/java/demoapp/web/quartz/AutowiringSpringBeanJobFactory.java
rename to extensions/core/quartz/impl/src/main/java/org/apache/isis/extensions/quartz/spring/AutowiringSpringBeanJobFactory.java
index 105309d..38acba0 100644
--- a/examples/demo/web/src/main/java/demoapp/web/quartz/AutowiringSpringBeanJobFactory.java
+++ b/extensions/core/quartz/impl/src/main/java/org/apache/isis/extensions/quartz/spring/AutowiringSpringBeanJobFactory.java
@@ -1,4 +1,4 @@
-package demoapp.web.quartz;
+package org.apache.isis.extensions.quartz.spring;
 
 import org.quartz.spi.TriggerFiredBundle;
 import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
@@ -6,7 +6,7 @@ import org.springframework.context.ApplicationContext;
 import org.springframework.context.ApplicationContextAware;
 import org.springframework.scheduling.quartz.SpringBeanJobFactory;
 
-final class AutowiringSpringBeanJobFactory
+public class AutowiringSpringBeanJobFactory
         extends SpringBeanJobFactory
         implements ApplicationContextAware {