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);