Skip to content

Commit 08d729b

Browse files
committed
CAUSEWAY-3989: [v2] backport proper replay-state persisting
1 parent 8b9e1ce commit 08d729b

3 files changed

Lines changed: 60 additions & 32 deletions

File tree

extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/CommandReplayManager.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,9 +209,8 @@ public List<ReplayableCommand> getPendingOrFailed() {
209209
cssClass = "btn-primary",
210210
describedAs = "Executes the list of commands in sequence, after having sorted them by their timestamp. "
211211
+ "If any of the given commands fails, "
212-
+ "the surrounding transaction is rolled back and any successful commands are undone). "
213-
+ "The command, that caused the failure, gets marked as FAILED."
214-
)
212+
+ "its surrounding transaction is rolled back, but any successful commands so far are marked OK). "
213+
+ "The command, that caused the failure, gets marked FAILED.")
215214
public class replayOrRetrySelected {
216215
public class DomainEvent extends ActionDomainEvent<replayOrRetrySelected> { }
217216
@MemberSupport public CommandReplayManager act(final List<ReplayableCommand> selected) {

extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayContext.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,17 @@
1818
*/
1919
package org.apache.causeway.extensions.commandlog.applib.dom.replay;
2020

21+
import java.util.Optional;
22+
import java.util.UUID;
23+
2124
import org.apache.causeway.applib.services.clock.ClockService;
2225
import org.apache.causeway.applib.services.command.CommandExecutorService;
2326
import org.apache.causeway.applib.services.iactnlayer.InteractionService;
2427
import org.apache.causeway.applib.services.repository.RepositoryService;
2528
import org.apache.causeway.applib.services.xactn.TransactionService;
29+
import org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntry;
2630
import org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntryRepository;
31+
import org.springframework.lang.Nullable;
2732

2833
import lombok.Value;
2934
import lombok.experimental.Accessors;
@@ -39,4 +44,11 @@ public final class ReplayContext {
3944
CommandLogEntryRepository commandLogEntryRepository;
4045
CommandExecutorService commandExecutorService;
4146
ClockService clockService;
47+
48+
public Optional<CommandLogEntry> lookupCommandLogEntry(final @Nullable UUID interactionId) {
49+
return interactionId!=null
50+
? commandLogEntryRepository().findByInteractionId(interactionId)
51+
: Optional.empty();
52+
}
53+
4254
}

extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.java

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import java.util.Comparator;
2525
import java.util.Optional;
2626
import java.util.UUID;
27-
import java.util.concurrent.Executors;
2827

2928
import javax.inject.Inject;
3029
import javax.inject.Named;
@@ -40,6 +39,7 @@
4039
import org.apache.causeway.applib.annotation.PropertyLayout;
4140
import org.apache.causeway.applib.annotation.Where;
4241
import org.apache.causeway.applib.jaxb.JavaTimeXMLGregorianCalendarMarshalling;
42+
import org.apache.causeway.applib.services.bookmark.Bookmark;
4343
import org.apache.causeway.applib.services.command.CommandExecutorService.InteractionContextPolicy;
4444
import org.apache.causeway.commons.functional.Try;
4545
import org.apache.causeway.commons.internal.base._Refs.ObjectReference;
@@ -332,33 +332,16 @@ public String viewModelMemento() {
332332
// -- UTIL
333333

334334
Try<ReplayableCommand> tryReplayOrRetry() {
335-
if(disableReplayOrRetry()!=null) {
335+
if(disableReplayOrRetry()!=null)
336336
return Try.success(null); // guard against disallowed invocation
337-
}
338337
return commandLogEntry()
339338
.filter(ReplayableCommand::canReplayOrRetryOrMarkForExclusion)
340-
.map(commandLogEntry->{
341-
final var commandDto = commandLogEntry.getCommandDto();
342-
final var tryResultBookmark = replayContext.transactionService().callTransactional(Propagation.REQUIRES_NEW,
343-
() -> {
344-
final var bookmarkTry = replayContext.commandExecutorService().executeCommand(
345-
InteractionContextPolicy.SWITCH_USER_AND_TIME,
346-
commandDto);
347-
return bookmarkTry.valueAsNullableElseFail();
348-
});
349-
350-
// handle the replay outcome
351-
tryResultBookmark.accept(
352-
ex->onReplayError(commandLogEntry.getInteractionId(), ex),
353-
bookmarkOpt->commandLogEntry.setReplayState(ReplayState.OK));
354-
355-
invalidateCachedRecord();
356-
357-
return tryResultBookmark
358-
.mapSuccessAsNullable(__ -> this);
359-
})
339+
.map(commandLogEntry->tryReplay(commandLogEntry.getCommandDto())
340+
.mapSuccessAsNullable(__ -> this))
341+
// if nothing to do, return with an 'empty success'
360342
.orElseGet(()->Try.success(null));
361343
}
344+
362345
String disableReplayOrRetry() {
363346
return commandRecord()
364347
.map(CommandRecord::canReplayOrRetryOrMarkForExclusion)
@@ -368,6 +351,32 @@ String disableReplayOrRetry() {
368351
}
369352

370353
// -- HELPER
354+
355+
356+
/**
357+
* Replays given command in its own transaction and handles {@link ReplayState} transition to
358+
* either {@link ReplayState#OK} or {@link ReplayState#FAILED}.
359+
*/
360+
private Try<Bookmark> tryReplay(final CommandDto commandDto) {
361+
var tryResultBookmark = replayContext.transactionService()
362+
.callTransactional(Propagation.REQUIRES_NEW, () -> replayContext.commandExecutorService()
363+
.executeCommand(InteractionContextPolicy.SWITCH_USER_AND_TIME, commandDto)
364+
// if we have a replay failure, this throws, which will roll back the surrounding transaction
365+
.valueAsNullableElseFail());
366+
367+
replayContext.transactionService()
368+
.runTransactional(Propagation.REQUIRES_NEW, () -> {
369+
// handle the replay outcome
370+
tryResultBookmark.accept(
371+
this::onReplayError,
372+
bookmarkOpt->onReplaySuccess());
373+
});
374+
375+
// in any outcome case (OK or FAILED) the ReplayState may have changed, hence invalidate local cache
376+
invalidateCachedRecord();
377+
378+
return tryResultBookmark;
379+
}
371380

372381
private void invalidateCachedRecord() {
373382
recordRef.update(__->null); // invalidate cache
@@ -385,17 +394,25 @@ private Optional<CommandRecord> commandRecord() {
385394
}
386395

387396
Optional<CommandLogEntry> commandLogEntry() {
388-
return replayContext.commandLogEntryRepository().findByInteractionId(interactionId());
397+
return replayContext.lookupCommandLogEntry(interactionId());
389398
}
390399

391400
private static boolean canReplayOrRetryOrMarkForExclusion(final CommandLogEntry commandLogEntry) {
392401
return ReplayState.isPendingOrFailed(commandLogEntry.getReplayState());
393402
}
394403

395-
private void onReplayError(final UUID interactionId, final Throwable ex) {
396-
Executors.newSingleThreadExecutor()
397-
.submit(()->replayContext.interactionService().runAnonymous(()->
398-
replayContext.commandLogEntryRepository().findByInteractionId(interactionId)
399-
.ifPresent(entry->entry.saveAnalysis(ex.toString()))));
404+
/**
405+
* Handles the error case in its own scheduled thread, as the current transaction will roll back.
406+
*/
407+
private void onReplayError(final Throwable ex) {
408+
commandLogEntry() // refetch from persistence
409+
.ifPresent(entry->entry.saveAnalysis(ex.toString()));
410+
}
411+
/**
412+
* Handles the happy case.
413+
*/
414+
private void onReplaySuccess() {
415+
commandLogEntry() // refetch from persistence
416+
.ifPresent(entry->entry.saveAnalysis(null));
400417
}
401418
}

0 commit comments

Comments
 (0)