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

[isis] 01/01: ISIS-3221 : introduces AsyncCallable for WrapperFactory, to surface details of the child command to custom impls of ExecutorService

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 <da...@haywood-associates.co.uk>
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);