This is an automated email from the ASF dual-hosted git repository. danhaywood pushed a commit to branch ISIS-3221 in repository https://gitbox.apache.org/repos/asf/isis.git
commit 1d4f992223945d049059f1c6f07e2b7df3ceded0 Author: Dan Haywood <[email protected]> AuthorDate: Mon Sep 26 17:46:52 2022 +0100 ISIS-3221 : introduces AsyncCallable for WrapperFactory, to surface details of the child command to custom impls of ExecutorService --- api/applib/src/main/java/module-info.java | 3 +- .../isis/applib/services/command/Command.java | 8 +- .../applib/services/wrapper/WrapperFactory.java | 19 ++- .../services/wrapper/callable/AsyncCallable.java | 112 ++++++++++++++++++ .../wrapper/WrapperFactoryDefault.java | 129 ++++++++++++--------- .../subscriber/CommandSubscriberForCommandLog.java | 6 +- 6 files changed, 215 insertions(+), 62 deletions(-) diff --git a/api/applib/src/main/java/module-info.java b/api/applib/src/main/java/module-info.java index 7b0c5d7023..5bc777efd5 100644 --- a/api/applib/src/main/java/module-info.java +++ b/api/applib/src/main/java/module-info.java @@ -105,6 +105,7 @@ module org.apache.isis.applib { exports org.apache.isis.applib.services.userreg.events; exports org.apache.isis.applib.services.userreg; exports org.apache.isis.applib.services.userui; + exports org.apache.isis.applib.services.wrapper.callable; exports org.apache.isis.applib.services.wrapper.control; exports org.apache.isis.applib.services.wrapper.events; exports org.apache.isis.applib.services.wrapper.listeners; @@ -151,4 +152,4 @@ module org.apache.isis.applib { opens org.apache.isis.applib.layout.menubars; -} \ No newline at end of file +} diff --git a/api/applib/src/main/java/org/apache/isis/applib/services/command/Command.java b/api/applib/src/main/java/org/apache/isis/applib/services/command/Command.java index 7dcd0d8d5f..836e883508 100644 --- a/api/applib/src/main/java/org/apache/isis/applib/services/command/Command.java +++ b/api/applib/src/main/java/org/apache/isis/applib/services/command/Command.java @@ -162,7 +162,7 @@ public class Command implements HasInteractionId, HasUsername, HasCommandDto { /** * For async commands created through the {@link WrapperFactory}, - * captures the parent command. + * captures the {@link Command#getInteractionId() interactionId} of the parent command. * * <p> * Will return <code>null</code> if there is no parent. @@ -174,7 +174,7 @@ public class Command implements HasInteractionId, HasUsername, HasCommandDto { */ @ToString.Exclude @Getter - private Command parent; + private UUID parentInteractionId; /** * For an command that has actually been executed, holds the date/time at @@ -309,8 +309,8 @@ public class Command implements HasInteractionId, HasUsername, HasCommandDto { * {@link WrapperFactory}. * </p> */ - public void setParent(final Command parent) { - Command.this.parent = parent; + public void setParentInteractionId(final UUID parentInteractionId) { + Command.this.parentInteractionId = parentInteractionId; } /** * <b>NOT API</b>: intended to be called only by the framework. diff --git a/api/applib/src/main/java/org/apache/isis/applib/services/wrapper/WrapperFactory.java b/api/applib/src/main/java/org/apache/isis/applib/services/wrapper/WrapperFactory.java index 8a94168677..6d7373844d 100644 --- a/api/applib/src/main/java/org/apache/isis/applib/services/wrapper/WrapperFactory.java +++ b/api/applib/src/main/java/org/apache/isis/applib/services/wrapper/WrapperFactory.java @@ -19,13 +19,19 @@ package org.apache.isis.applib.services.wrapper; import java.util.List; +import java.util.concurrent.ExecutorService; import org.apache.isis.applib.exceptions.recoverable.InteractionException; +import org.apache.isis.applib.services.command.Command; import org.apache.isis.applib.services.factory.FactoryService; +import org.apache.isis.applib.services.iactnlayer.InteractionContext; +import org.apache.isis.applib.services.wrapper.callable.AsyncCallable; import org.apache.isis.applib.services.wrapper.control.AsyncControl; import org.apache.isis.applib.services.wrapper.control.SyncControl; import org.apache.isis.applib.services.wrapper.events.InteractionEvent; import org.apache.isis.applib.services.wrapper.listeners.InteractionListener; +import org.apache.isis.schema.cmd.v2.CommandDto; +import org.springframework.transaction.annotation.Propagation; /** * @@ -267,6 +273,17 @@ public interface WrapperFactory { InteractionListener listener); void notifyListeners(InteractionEvent ev); - // ... + + // + // -- SPI for ExecutorServices + // + + + /** + * Provides a mechanism for custom implementations of {@link java.util.concurrent.ExecutorService}, as installed + * using {@link AsyncControl#with(ExecutorService)}, to actually execute the {@link AsyncCallable} that they + * are passed initially during {@link WrapperFactory#asyncWrap(Object, AsyncControl)} and its brethren. + */ + <R> R execute(AsyncCallable<R> asyncCallable); } diff --git a/api/applib/src/main/java/org/apache/isis/applib/services/wrapper/callable/AsyncCallable.java b/api/applib/src/main/java/org/apache/isis/applib/services/wrapper/callable/AsyncCallable.java new file mode 100644 index 0000000000..c728a6ffa8 --- /dev/null +++ b/api/applib/src/main/java/org/apache/isis/applib/services/wrapper/callable/AsyncCallable.java @@ -0,0 +1,112 @@ +/* + * 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.applib.services.wrapper.callable; + +import java.io.Serializable; +import java.util.UUID; +import java.util.concurrent.ExecutorService; + +import org.apache.isis.applib.services.command.Command; +import org.apache.isis.applib.services.iactnlayer.InteractionContext; +import org.apache.isis.applib.services.wrapper.control.AsyncControl; +import org.apache.isis.schema.cmd.v2.CommandDto; +import org.springframework.transaction.annotation.Propagation; + +/** + * Provides access to the details of the asynchronous callable (representing a child command to be executed + * asynchronously) when using + * {@link org.apache.isis.applib.services.wrapper.WrapperFactory#asyncWrap(Object, AsyncControl)} and its brethren. + * + * <p> + * To explain in a little more depth; we can execute commands (actions etc) asynchronously using + * {@link org.apache.isis.applib.services.wrapper.WrapperFactory#asyncWrap(Object, AsyncControl)} or similar. + * The {@link AsyncControl} parameter allows various aspects of this to be controlled, one such being the + * implementation of the {@link java.util.concurrent.ExecutorService} (using + * {@link AsyncControl#with(ExecutorService)}). + * </p> + * + * <p> + * The default {@link ExecutorService} is just {@link java.util.concurrent.ForkJoinPool}, and this and similar + * implementations will hold the provided callable in memory and execute it in due course. For these out-of-the-box + * implementations, the {@link java.util.concurrent.Callable} is a black box and they have no need to look inside + * it. So long as the implementation of the Callable is not serialized then deserialized (ie is only ever held in + * memory), then all will work fine. + * </p> + * + * <p> + * This interface, though, is intended to expose the details of the passed {@link java.util.concurrent.Callable}, + * most notably the {@link CommandDto} to be executed. The main use case this supports is to allow a custom + * implementation of {@link ExecutorService} to be provided that could do more sophisticated things, for example + * persisting the callable somewhere, either exploiting the fact that the object is serializable, or perhaps by + * unpacking the parts and persisting (for example, as a <code>CommandLogEntry</code> courtesy of the + * commandlog extension). + * </p> + * + * <p> + * These custom implementations of {@link ExecutorService} must however reinitialize the state of the callable, + * either by injecting in services using {@link org.apache.isis.applib.services.inject.ServiceInjector} and then + * just <code>call()</code>ing it, or alternatively and more straightforwardly simply executing it using + * {@link org.apache.isis.applib.services.wrapper.WrapperFactory#execute(AsyncCallable)}. + * </p> + * + * @since 2.0 {@index} + */ +public interface AsyncCallable<R> extends Serializable { + + /** + * The requested {@link InteractionContext} to execute the command, as inferred from the {@link AsyncControl} + * that was used to call + * {@link org.apache.isis.applib.services.wrapper.WrapperFactory#asyncWrap(Object, AsyncControl)} and its ilk. + */ + InteractionContext getInteractionContext(); + + /** + * The transaction propagation to use when creating a new {@link org.apache.isis.applib.services.iactn.Interaction} + * in which to execute the child command. + */ + Propagation getPropagation(); + + /** + * Details of the actual child command (action or property edit) to be performed. + * + * <p> + * (Ultimately this is handed onto the {@link org.apache.isis.applib.services.command.CommandExecutorService}). + * </p> + */ + CommandDto getCommandDto(); + + /** + * The type of the object returned by the child command once finally executed. + */ + Class<R> getReturnType(); + + /** + * The unique {@link Command#getInteractionId() interactionId} of the parent {@link Command}, which is to say the + * {@link Command} that was active in the original interaction where + * {@link org.apache.isis.applib.services.wrapper.WrapperFactory#asyncWrap(Object, AsyncControl)} (or its brethren) + * was called. + * + * <p> + * This can be useful for custom implementations of {@link ExecutorService} that use the commandlog + * extension's <code>CommandLogEntry</code>, to link parent and child commands together. + * </p> + */ + UUID getParentInteractionId(); + +} 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 0711040424..705c07ea2f 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 @@ -19,14 +19,9 @@ package org.apache.isis.core.runtimeservices.wrapper; import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collections; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; import java.util.function.BiConsumer; import javax.annotation.PostConstruct; @@ -35,6 +30,7 @@ import javax.inject.Inject; import javax.inject.Named; import javax.inject.Provider; +import org.apache.isis.applib.services.wrapper.callable.AsyncCallable; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; @@ -44,7 +40,6 @@ import org.apache.isis.applib.annotation.PriorityPrecedence; import org.apache.isis.applib.locale.UserLocale; import org.apache.isis.applib.services.bookmark.Bookmark; import org.apache.isis.applib.services.bookmark.BookmarkService; -import org.apache.isis.applib.services.command.Command; import org.apache.isis.applib.services.command.CommandExecutorService; import org.apache.isis.applib.services.factory.FactoryService; import org.apache.isis.applib.services.iactn.InteractionProvider; @@ -107,10 +102,7 @@ import org.apache.isis.schema.cmd.v2.CommandDto; import static org.apache.isis.applib.services.metamodel.MetaModelService.Mode.RELAXED; import static org.apache.isis.applib.services.wrapper.control.SyncControl.control; -import lombok.Data; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import lombok.val; +import lombok.*; @Service @Named(WrapperFactoryDefault.LOGICAL_TYPE_NAME) @@ -395,16 +387,15 @@ public class WrapperFactoryDefault implements WrapperFactory { asyncControl.setBookmark(Bookmark.forOidDto(oidDto)); val executorService = asyncControl.getExecutorService(); - val future = executorService.submit( - new ExecCommand<R>( + AsyncTask<R> task = serviceInjector.injectServicesInto( + new AsyncTask<R>( asyncInteractionContext, Propagation.REQUIRES_NEW, commandDto, asyncControl.getReturnType(), - command, - serviceInjector) + command.getInteractionId()) // this command becomes the parent of child command ); - + val future = executorService.submit(task); asyncControl.setFuture(future); return null; @@ -580,52 +571,84 @@ public class WrapperFactoryDefault implements WrapperFactory { } @RequiredArgsConstructor - private static class ExecCommand<R> implements Callable<R> { - - private final InteractionContext interactionContext; - private final Propagation propagation; - private final CommandDto commandDto; - private final Class<R> returnType; - private final Command parentCommand; - private final ServiceInjector serviceInjector; - - @Inject InteractionService interactionService; - @Inject TransactionService transactionService; - @Inject CommandExecutorService commandExecutorService; - @Inject Provider<InteractionProvider> interactionProviderProvider; - @Inject BookmarkService bookmarkService; - @Inject RepositoryService repositoryService; - @Inject MetaModelService metaModelService; + private static class AsyncTask<R> implements Callable<R>, AsyncCallable<R> { + + @Getter private final InteractionContext interactionContext; + @Getter private final Propagation propagation; + @Getter private final CommandDto commandDto; + @Getter private final Class<R> returnType; + @Getter private final UUID parentInteractionId; + + /** + * Note that is a <code>transient</code> field in order that {@link org.apache.isis.applib.services.wrapper.callable.AsyncCallable} can be declared as + * {@link java.io.Serializable}. + * + * <p> + * Because this field needs to be populated, the {@link java.util.concurrent.ExecutorService} that ultimately + * executes the task will need to be a custom implementation because it must reinitialize this field first, + * using the {@link ServiceInjector} service. Alternatively, it could call + * {@link WrapperFactory#execute(AsyncCallable)} directly, which achieves the same thing. + * </p> + */ + @Inject transient WrapperFactory wrapperFactory; + /** + * If the {@link java.util.concurrent.ExecutorService} used to execute this task (as defined by + * {@link AsyncControl#with(ExecutorService)} is not custom, then it can simply invoke this method, but it is + * important that it has not serialized/deserialized the object since important transient state would be lost. + * + * <p> + * On the other hand, a custom implementation of {@link ExecutorService} is free to serialize this object, and + * deserialize it later. When deserializing it can either reinitialize the necessary state using the + * {@link ServiceInjector} service, then call this method, or it can instead call + * {@link WrapperFactory#execute(AsyncCallable)} directly, which achieves the same thing. + * </p> + */ @Override public R call() { - serviceInjector.injectServicesInto(this); - return interactionService.call(interactionContext, this::updateDomainObjectHonoringTransactionalPropagation); + if (wrapperFactory == null) { + throw new IllegalStateException( + "The transient wrapperFactory is null; suggests that this async task been serialized and " + + "then deserialized, but is now being executed by an ExecutorService that has not re-injected necessary services."); + } + return wrapperFactory.execute(this); } + } - private R updateDomainObjectHonoringTransactionalPropagation() { - return transactionService.callTransactional(propagation, this::updateDomainObject) - .ifFailureFail() - .getValue().orElse(null); - } + @Inject InteractionService interactionService; + @Inject TransactionService transactionService; + @Inject CommandExecutorService commandExecutorService; + @Inject Provider<InteractionProvider> interactionProviderProvider; + @Inject BookmarkService bookmarkService; + @Inject RepositoryService repositoryService; + @Inject MetaModelService metaModelService; - private R updateDomainObject() { - val childCommand = interactionProviderProvider.get().currentInteractionElseFail().getCommand(); - childCommand.updater().setParent(parentCommand); + public <R> R execute(AsyncCallable<R> asyncCallable) { + serviceInjector.injectServicesInto(this); + return interactionService.call(asyncCallable.getInteractionContext(), () -> updateDomainObjectHonoringTransactionalPropagation(asyncCallable)); + } - val bookmark = commandExecutorService.executeCommand(commandDto, childCommand.updater()); - if (bookmark == null) { - return null; - } - R domainObject = bookmarkService.lookup(bookmark, returnType).orElse(null); - if (metaModelService.sortOf(bookmark, RELAXED).isEntity()) { - domainObject = repositoryService.detach(domainObject); - } - return domainObject; + private <R> R updateDomainObjectHonoringTransactionalPropagation(AsyncCallable<R> asyncCallable) { + return transactionService.callTransactional(asyncCallable.getPropagation(), () -> updateDomainObject(asyncCallable)) + .ifFailureFail() + .getValue().orElse(null); + } - } + private <R> R updateDomainObject(AsyncCallable<R> asyncCallable) { + val childCommand = interactionProviderProvider.get().currentInteractionElseFail().getCommand(); + childCommand.updater().setParentInteractionId(asyncCallable.getParentInteractionId()); + val bookmark = commandExecutorService.executeCommand(asyncCallable.getCommandDto(), childCommand.updater()); + if (bookmark == null) { + return null; + } + R domainObject = bookmarkService.lookup(bookmark, asyncCallable.getReturnType()).orElse(null); + if (metaModelService.sortOf(bookmark, RELAXED).isEntity()) { + domainObject = repositoryService.detach(domainObject); + } + return domainObject; } + } diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/isis/extensions/commandlog/applib/subscriber/CommandSubscriberForCommandLog.java b/extensions/core/commandlog/applib/src/main/java/org/apache/isis/extensions/commandlog/applib/subscriber/CommandSubscriberForCommandLog.java index c33abf534a..ea60960670 100644 --- a/extensions/core/commandlog/applib/src/main/java/org/apache/isis/extensions/commandlog/applib/subscriber/CommandSubscriberForCommandLog.java +++ b/extensions/core/commandlog/applib/src/main/java/org/apache/isis/extensions/commandlog/applib/subscriber/CommandSubscriberForCommandLog.java @@ -74,11 +74,11 @@ public class CommandSubscriberForCommandLog implements CommandSubscriber { log.debug("proposed: \n{}", commandDtoXml); } } else { - val parent = command.getParent(); + val parentInteractionId = command.getParentInteractionId(); val parentEntryIfAny = - parent != null + parentInteractionId != null ? commandLogEntryRepository - .findByInteractionId(parent.getInteractionId()) + .findByInteractionId(parentInteractionId) .orElse(null) : null; commandLogEntryRepository.createEntryAndPersist(command, parentEntryIfAny);
