You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@causeway.apache.org by ah...@apache.org on 2026/04/29 09:29:24 UTC
(causeway) branch maintenance-branch updated: CAUSEWAY-3989: [v2] backport proper replay-state persisting
This is an automated email from the ASF dual-hosted git repository.
andi-huber pushed a commit to branch maintenance-branch
in repository https://gitbox.apache.org/repos/asf/causeway.git
The following commit(s) were added to refs/heads/maintenance-branch by this push:
new 08d729be530 CAUSEWAY-3989: [v2] backport proper replay-state persisting
08d729be530 is described below
commit 08d729be530961cf28dbff5a211bd41b52ac37a1
Author: andi-huber <ah...@apache.org>
AuthorDate: Wed Apr 29 11:29:16 2026 +0200
CAUSEWAY-3989: [v2] backport proper replay-state persisting
---
.../applib/dom/replay/CommandReplayManager.java | 5 +-
.../applib/dom/replay/ReplayContext.java | 12 ++++
.../applib/dom/replay/ReplayableCommand.java | 75 +++++++++++++---------
3 files changed, 60 insertions(+), 32 deletions(-)
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager.java
index 9c676138e9d..becf8d46ef6 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager.java
@@ -209,9 +209,8 @@ public List<ReplayableCommand> getPendingOrFailed() {
cssClass = "btn-primary",
describedAs = "Executes the list of commands in sequence, after having sorted them by their timestamp. "
+ "If any of the given commands fails, "
- + "the surrounding transaction is rolled back and any successful commands are undone). "
- + "The command, that caused the failure, gets marked as FAILED."
- )
+ + "its surrounding transaction is rolled back, but any successful commands so far are marked OK). "
+ + "The command, that caused the failure, gets marked FAILED.")
public class replayOrRetrySelected {
public class DomainEvent extends ActionDomainEvent<replayOrRetrySelected> { }
@MemberSupport public CommandReplayManager act(final List<ReplayableCommand> selected) {
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayContext.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayContext.java
index 1e739bb56a7..726253a7a09 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayContext.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayContext.java
@@ -18,12 +18,17 @@
*/
package org.apache.causeway.extensions.commandlog.applib.dom.replay;
+import java.util.Optional;
+import java.util.UUID;
+
import org.apache.causeway.applib.services.clock.ClockService;
import org.apache.causeway.applib.services.command.CommandExecutorService;
import org.apache.causeway.applib.services.iactnlayer.InteractionService;
import org.apache.causeway.applib.services.repository.RepositoryService;
import org.apache.causeway.applib.services.xactn.TransactionService;
+import org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntry;
import org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntryRepository;
+import org.springframework.lang.Nullable;
import lombok.Value;
import lombok.experimental.Accessors;
@@ -39,4 +44,11 @@ public final class ReplayContext {
CommandLogEntryRepository commandLogEntryRepository;
CommandExecutorService commandExecutorService;
ClockService clockService;
+
+ public Optional<CommandLogEntry> lookupCommandLogEntry(final @Nullable UUID interactionId) {
+ return interactionId!=null
+ ? commandLogEntryRepository().findByInteractionId(interactionId)
+ : Optional.empty();
+ }
+
}
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.java
index 0ef6eb255ff..a796d2639c5 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.java
@@ -24,7 +24,6 @@
import java.util.Comparator;
import java.util.Optional;
import java.util.UUID;
-import java.util.concurrent.Executors;
import javax.inject.Inject;
import javax.inject.Named;
@@ -40,6 +39,7 @@
import org.apache.causeway.applib.annotation.PropertyLayout;
import org.apache.causeway.applib.annotation.Where;
import org.apache.causeway.applib.jaxb.JavaTimeXMLGregorianCalendarMarshalling;
+import org.apache.causeway.applib.services.bookmark.Bookmark;
import org.apache.causeway.applib.services.command.CommandExecutorService.InteractionContextPolicy;
import org.apache.causeway.commons.functional.Try;
import org.apache.causeway.commons.internal.base._Refs.ObjectReference;
@@ -332,33 +332,16 @@ public String viewModelMemento() {
// -- UTIL
Try<ReplayableCommand> tryReplayOrRetry() {
- if(disableReplayOrRetry()!=null) {
+ if(disableReplayOrRetry()!=null)
return Try.success(null); // guard against disallowed invocation
- }
return commandLogEntry()
.filter(ReplayableCommand::canReplayOrRetryOrMarkForExclusion)
- .map(commandLogEntry->{
- final var commandDto = commandLogEntry.getCommandDto();
- final var tryResultBookmark = replayContext.transactionService().callTransactional(Propagation.REQUIRES_NEW,
- () -> {
- final var bookmarkTry = replayContext.commandExecutorService().executeCommand(
- InteractionContextPolicy.SWITCH_USER_AND_TIME,
- commandDto);
- return bookmarkTry.valueAsNullableElseFail();
- });
-
- // handle the replay outcome
- tryResultBookmark.accept(
- ex->onReplayError(commandLogEntry.getInteractionId(), ex),
- bookmarkOpt->commandLogEntry.setReplayState(ReplayState.OK));
-
- invalidateCachedRecord();
-
- return tryResultBookmark
- .mapSuccessAsNullable(__ -> this);
- })
+ .map(commandLogEntry->tryReplay(commandLogEntry.getCommandDto())
+ .mapSuccessAsNullable(__ -> this))
+ // if nothing to do, return with an 'empty success'
.orElseGet(()->Try.success(null));
}
+
String disableReplayOrRetry() {
return commandRecord()
.map(CommandRecord::canReplayOrRetryOrMarkForExclusion)
@@ -368,6 +351,32 @@ String disableReplayOrRetry() {
}
// -- HELPER
+
+
+ /**
+ * Replays given command in its own transaction and handles {@link ReplayState} transition to
+ * either {@link ReplayState#OK} or {@link ReplayState#FAILED}.
+ */
+ private Try<Bookmark> tryReplay(final CommandDto commandDto) {
+ var tryResultBookmark = replayContext.transactionService()
+ .callTransactional(Propagation.REQUIRES_NEW, () -> replayContext.commandExecutorService()
+ .executeCommand(InteractionContextPolicy.SWITCH_USER_AND_TIME, commandDto)
+ // if we have a replay failure, this throws, which will roll back the surrounding transaction
+ .valueAsNullableElseFail());
+
+ replayContext.transactionService()
+ .runTransactional(Propagation.REQUIRES_NEW, () -> {
+ // handle the replay outcome
+ tryResultBookmark.accept(
+ this::onReplayError,
+ bookmarkOpt->onReplaySuccess());
+ });
+
+ // in any outcome case (OK or FAILED) the ReplayState may have changed, hence invalidate local cache
+ invalidateCachedRecord();
+
+ return tryResultBookmark;
+ }
private void invalidateCachedRecord() {
recordRef.update(__->null); // invalidate cache
@@ -385,17 +394,25 @@ private Optional<CommandRecord> commandRecord() {
}
Optional<CommandLogEntry> commandLogEntry() {
- return replayContext.commandLogEntryRepository().findByInteractionId(interactionId());
+ return replayContext.lookupCommandLogEntry(interactionId());
}
private static boolean canReplayOrRetryOrMarkForExclusion(final CommandLogEntry commandLogEntry) {
return ReplayState.isPendingOrFailed(commandLogEntry.getReplayState());
}
- private void onReplayError(final UUID interactionId, final Throwable ex) {
- Executors.newSingleThreadExecutor()
- .submit(()->replayContext.interactionService().runAnonymous(()->
- replayContext.commandLogEntryRepository().findByInteractionId(interactionId)
- .ifPresent(entry->entry.saveAnalysis(ex.toString()))));
+ /**
+ * Handles the error case in its own scheduled thread, as the current transaction will roll back.
+ */
+ private void onReplayError(final Throwable ex) {
+ commandLogEntry() // refetch from persistence
+ .ifPresent(entry->entry.saveAnalysis(ex.toString()));
+ }
+ /**
+ * Handles the happy case.
+ */
+ private void onReplaySuccess() {
+ commandLogEntry() // refetch from persistence
+ .ifPresent(entry->entry.saveAnalysis(null));
}
}
\ No newline at end of file