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