From 71621df42c7c9ef6308677b402e94238ca1e2972 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 27 Oct 2024 00:10:21 +0200 Subject: [PATCH] FEATURE: Throw `WorkspaceRebaseFailed` during publication or partial discard as well ... instead of enforcing to rebase from outside. This speeds things up as we need one fork less;) see also https://github.com/neos/neos-development-collection/pull/5300 --- Classes/CommandHandler/CommandSimulator.php | 44 +++++++-- Classes/Feature/RebaseableCommand.php | 8 +- Classes/Feature/RebaseableCommands.php | 2 +- Classes/Feature/WorkspaceCommandHandler.php | 96 +++++++------------ .../CommandThatFailedDuringRebase.php | 7 +- .../CommandsThatFailedDuringRebase.php | 17 +++- .../Exception/WorkspaceRebaseFailed.php | 44 ++++++++- 7 files changed, 133 insertions(+), 85 deletions(-) diff --git a/Classes/CommandHandler/CommandSimulator.php b/Classes/CommandHandler/CommandSimulator.php index 718186ac..35dc102e 100644 --- a/Classes/CommandHandler/CommandSimulator.php +++ b/Classes/CommandHandler/CommandSimulator.php @@ -9,6 +9,8 @@ use Neos\ContentRepository\Core\EventStore\EventNormalizer; use Neos\ContentRepository\Core\EventStore\EventsToPublish; use Neos\ContentRepository\Core\Feature\RebaseableCommand; +use Neos\ContentRepository\Core\Feature\WorkspaceRebase\CommandsThatFailedDuringRebase; +use Neos\ContentRepository\Core\Feature\WorkspaceRebase\CommandThatFailedDuringRebase; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphProjectionInterface; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\EventStore\Helper\InMemoryEventStore; @@ -41,15 +43,18 @@ * * @internal */ -final readonly class CommandSimulator +final class CommandSimulator { + private CommandsThatFailedDuringRebase $commandsThatFailedDuringRebase; + public function __construct( - private ContentGraphProjectionInterface $contentRepositoryProjection, - private EventNormalizer $eventNormalizer, - private CommandBus $commandBus, - private InMemoryEventStore $inMemoryEventStore, - private WorkspaceName $workspaceNameToSimulateIn, + private readonly ContentGraphProjectionInterface $contentRepositoryProjection, + private readonly EventNormalizer $eventNormalizer, + private readonly CommandBus $commandBus, + private readonly InMemoryEventStore $inMemoryEventStore, + private readonly WorkspaceName $workspaceNameToSimulateIn, ) { + $this->commandsThatFailedDuringRebase = new CommandsThatFailedDuringRebase(); } /** @@ -74,7 +79,20 @@ private function handle(RebaseableCommand $rebaseableCommand): void // when https://github.com/neos/neos-development-collection/pull/5298 is merged $commandInWorkspace = $rebaseableCommand->originalCommand->createCopyForWorkspace($this->workspaceNameToSimulateIn); - $eventsToPublish = $this->commandBus->handle($commandInWorkspace); + try { + $eventsToPublish = $this->commandBus->handle($commandInWorkspace); + } catch (\Exception $exception) { + $this->commandsThatFailedDuringRebase = $this->commandsThatFailedDuringRebase->withAppended( + new CommandThatFailedDuringRebase( + $rebaseableCommand->originalSequenceNumber, + $rebaseableCommand->originalCommand, + $exception + ) + ); + + return; + } + if (!$eventsToPublish instanceof EventsToPublish) { throw new \RuntimeException(sprintf('CommandSimulator expects direct EventsToPublish to be returned when handling %s', $rebaseableCommand->originalCommand::class)); } @@ -109,7 +127,7 @@ private function handle(RebaseableCommand $rebaseableCommand): void // fetch all events that were now committed. Plus one because the first sequence number is one too otherwise we get one event to many. // (all elephants shall be placed shamefully placed on my head) - $eventStream = $this->inMemoryEventStore->load(VirtualStreamName::all())->withMinimumSequenceNumber( + $eventStream = $this->eventStream()->withMinimumSequenceNumber( $sequenceNumberBeforeCommit->next() ); @@ -136,4 +154,14 @@ public function eventStream(): EventStreamInterface { return $this->inMemoryEventStore->load(VirtualStreamName::all()); } + + public function hasCommandsThatFailed(): bool + { + return !$this->commandsThatFailedDuringRebase->isEmpty(); + } + + public function getCommandsThatFailed(): CommandsThatFailedDuringRebase + { + return $this->commandsThatFailedDuringRebase; + } } diff --git a/Classes/Feature/RebaseableCommand.php b/Classes/Feature/RebaseableCommand.php index b61a5a7f..2979d3f3 100644 --- a/Classes/Feature/RebaseableCommand.php +++ b/Classes/Feature/RebaseableCommand.php @@ -11,6 +11,7 @@ use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface; use Neos\EventStore\Model\Event\EventId; use Neos\EventStore\Model\Event\EventMetadata; +use Neos\EventStore\Model\Event\SequenceNumber; /** * @internal @@ -20,11 +21,11 @@ public function __construct( public RebasableToOtherWorkspaceInterface $originalCommand, public EventMetadata $initiatingMetaData, - // todo SequenceNumber $originalSequenceNumber + public SequenceNumber $originalSequenceNumber ) { } - public static function extractFromEventMetaData(EventMetadata $eventMetadata): self + public static function extractFromEventMetaData(EventMetadata $eventMetadata, SequenceNumber $sequenceNumber): self { if (!isset($eventMetadata->value['commandClass'])) { throw new \RuntimeException('Command cannot be extracted from metadata, missing commandClass.', 1729847804); @@ -45,7 +46,8 @@ public static function extractFromEventMetaData(EventMetadata $eventMetadata): s $commandInstance = $commandToRebaseClass::fromArray($commandToRebasePayload); return new self( $commandInstance, - InitiatingEventMetadata::extractInitiatingMetadata($eventMetadata) + InitiatingEventMetadata::extractInitiatingMetadata($eventMetadata), + $sequenceNumber ); } diff --git a/Classes/Feature/RebaseableCommands.php b/Classes/Feature/RebaseableCommands.php index 1976e6d3..5f414632 100644 --- a/Classes/Feature/RebaseableCommands.php +++ b/Classes/Feature/RebaseableCommands.php @@ -30,7 +30,7 @@ public static function extractFromEventStream(EventStreamInterface $eventStream) $commands = []; foreach ($eventStream as $eventEnvelope) { if ($eventEnvelope->event->metadata && isset($eventEnvelope->event->metadata?->value['commandClass'])) { - $commands[$eventEnvelope->sequenceNumber->value] = RebaseableCommand::extractFromEventMetaData($eventEnvelope->event->metadata); + $commands[] = RebaseableCommand::extractFromEventMetaData($eventEnvelope->event->metadata, $eventEnvelope->sequenceNumber); } } diff --git a/Classes/Feature/WorkspaceCommandHandler.php b/Classes/Feature/WorkspaceCommandHandler.php index 1fe932fc..aa12f889 100644 --- a/Classes/Feature/WorkspaceCommandHandler.php +++ b/Classes/Feature/WorkspaceCommandHandler.php @@ -253,29 +253,16 @@ private function publishWorkspace( ): \Generator { $commandSimulator = $this->commandSimulatorFactory->createSimulator($baseWorkspace->workspaceName); - $commandsThatFailed = $commandSimulator->run( - static function ($handle) use ($rebaseableCommands): CommandsThatFailedDuringRebase { - $commandsThatFailed = new CommandsThatFailedDuringRebase(); - foreach ($rebaseableCommands as $sequenceNumber => $rebaseableCommand) { - try { - $handle($rebaseableCommand); - } catch (\Exception $e) { - $commandsThatFailed = $commandsThatFailed->add( - new CommandThatFailedDuringRebase( - $sequenceNumber, - $rebaseableCommand->originalCommand, - $e - ) - ); - } + $commandSimulator->run( + static function ($handle) use ($rebaseableCommands): void { + foreach ($rebaseableCommands as $rebaseableCommand) { + $handle($rebaseableCommand); } - - return $commandsThatFailed; } ); - if (!$commandsThatFailed->isEmpty()) { - throw new WorkspaceRebaseFailed($commandsThatFailed, 'Publication with rebase failed', 1729845795); + if ($commandSimulator->hasCommandsThatFailed()) { + throw WorkspaceRebaseFailed::duringPublish($commandSimulator->getCommandsThatFailed()); } yield new EventsToPublish( @@ -411,30 +398,17 @@ private function handleRebaseWorkspace( $commandSimulator = $this->commandSimulatorFactory->createSimulator($baseWorkspace->workspaceName); - $commandsThatFailed = $commandSimulator->run( - static function ($handle) use ($rebaseableCommands): CommandsThatFailedDuringRebase { - $commandsThatFailed = new CommandsThatFailedDuringRebase(); - foreach ($rebaseableCommands as $sequenceNumber => $rebaseableCommand) { - try { - $handle($rebaseableCommand); - } catch (\Exception $e) { - $commandsThatFailed = $commandsThatFailed->add( - new CommandThatFailedDuringRebase( - $sequenceNumber, - $rebaseableCommand->originalCommand, - $e - ) - ); - } + $commandSimulator->run( + static function ($handle) use ($rebaseableCommands): void { + foreach ($rebaseableCommands as $rebaseableCommand) { + $handle($rebaseableCommand); } - - return $commandsThatFailed; } ); if ( $command->rebaseErrorHandlingStrategy === RebaseErrorHandlingStrategy::STRATEGY_FAIL - && !$commandsThatFailed->isEmpty() + && $commandSimulator->hasCommandsThatFailed() ) { yield $this->reopenContentStream( $workspace->currentContentStreamId, @@ -443,7 +417,7 @@ static function ($handle) use ($rebaseableCommands): CommandsThatFailedDuringReb ); // throw an exception that contains all the information about what exactly failed - throw new WorkspaceRebaseFailed($commandsThatFailed, 'Rebase failed', 1711713880); + throw WorkspaceRebaseFailed::duringRebase($commandSimulator->getCommandsThatFailed()); } // if we got so far without an exception (or if we don't care), we can switch the workspace's active content stream. @@ -556,27 +530,27 @@ private function handlePublishIndividualNodesFromWorkspace( $commandSimulator = $this->commandSimulatorFactory->createSimulator($baseWorkspace->workspaceName); - try { - $highestSequenceNumberForMatching = $commandSimulator->run( - static function ($handle) use ($commandSimulator, $matchingCommands, $remainingCommands): SequenceNumber { - foreach ($matchingCommands as $matchingCommand) { - $handle($matchingCommand); - } - $highestSequenceNumberForMatching = $commandSimulator->currentSequenceNumber(); - foreach ($remainingCommands as $remainingCommand) { - $handle($remainingCommand); - } - return $highestSequenceNumberForMatching; + $highestSequenceNumberForMatching = $commandSimulator->run( + static function ($handle) use ($commandSimulator, $matchingCommands, $remainingCommands): SequenceNumber { + foreach ($matchingCommands as $matchingCommand) { + $handle($matchingCommand); } - ); - } catch (\Exception $exception) { + $highestSequenceNumberForMatching = $commandSimulator->currentSequenceNumber(); + foreach ($remainingCommands as $remainingCommand) { + $handle($remainingCommand); + } + return $highestSequenceNumberForMatching; + } + ); + + if ($commandSimulator->hasCommandsThatFailed()) { yield $this->reopenContentStream( $workspace->currentContentStreamId, $currentWorkspaceContentStreamState, $commandHandlingDependencies ); - throw $exception; + throw WorkspaceRebaseFailed::duringPublish($commandSimulator->getCommandsThatFailed()); } if ($highestSequenceNumberForMatching->equals(SequenceNumber::none())) { @@ -691,21 +665,21 @@ private function handleDiscardIndividualNodesFromWorkspace( $commandSimulator = $this->commandSimulatorFactory->createSimulator($baseWorkspace->workspaceName); - try { - $commandSimulator->run( - static function ($handle) use ($commandsToKeep): void { - foreach ($commandsToKeep as $matchingCommand) { - $handle($matchingCommand); - } + $commandSimulator->run( + static function ($handle) use ($commandsToKeep): void { + foreach ($commandsToKeep as $matchingCommand) { + $handle($matchingCommand); } - ); - } catch (\Exception $exception) { + } + ); + + if ($commandSimulator->hasCommandsThatFailed()) { yield $this->reopenContentStream( $workspace->currentContentStreamId, $currentWorkspaceContentStreamState, $commandHandlingDependencies ); - throw $exception; + throw WorkspaceRebaseFailed::duringDiscard($commandSimulator->getCommandsThatFailed()); } yield from $this->forkNewContentStreamAndApplyEvents( diff --git a/Classes/Feature/WorkspaceRebase/CommandThatFailedDuringRebase.php b/Classes/Feature/WorkspaceRebase/CommandThatFailedDuringRebase.php index 42e499af..c5c39df6 100644 --- a/Classes/Feature/WorkspaceRebase/CommandThatFailedDuringRebase.php +++ b/Classes/Feature/WorkspaceRebase/CommandThatFailedDuringRebase.php @@ -15,19 +15,20 @@ namespace Neos\ContentRepository\Core\Feature\WorkspaceRebase; use Neos\ContentRepository\Core\CommandHandler\CommandInterface; +use Neos\EventStore\Model\Event\SequenceNumber; /** - * @internal implementation detail of WorkspaceCommandHandler + * @api part of the exception exposed when rebasing failed */ final readonly class CommandThatFailedDuringRebase { /** - * @param int $sequenceNumber the event store sequence number of the event containing the command to be rebased + * @param SequenceNumber $sequenceNumber the event store sequence number of the event containing the command to be rebased * @param CommandInterface $command the command that failed * @param \Throwable $exception how the command failed */ public function __construct( - public int $sequenceNumber, + public SequenceNumber $sequenceNumber, public CommandInterface $command, public \Throwable $exception ) { diff --git a/Classes/Feature/WorkspaceRebase/CommandsThatFailedDuringRebase.php b/Classes/Feature/WorkspaceRebase/CommandsThatFailedDuringRebase.php index b9f6e792..24f1087e 100644 --- a/Classes/Feature/WorkspaceRebase/CommandsThatFailedDuringRebase.php +++ b/Classes/Feature/WorkspaceRebase/CommandsThatFailedDuringRebase.php @@ -19,7 +19,7 @@ * * @api part of the exception exposed when rebasing failed */ -final readonly class CommandsThatFailedDuringRebase implements \IteratorAggregate +final readonly class CommandsThatFailedDuringRebase implements \IteratorAggregate, \Countable { /** * @var array @@ -31,12 +31,14 @@ public function __construct(CommandThatFailedDuringRebase ...$items) $this->items = array_values($items); } - public function add(CommandThatFailedDuringRebase $item): self + public function withAppended(CommandThatFailedDuringRebase $item): self { - $items = $this->items; - $items[] = $item; + return new self(...[...$this->items, $item]); + } - return new self(...$items); + public function first(): ?CommandThatFailedDuringRebase + { + return $this->items[0] ?? null; } public function isEmpty(): bool @@ -48,4 +50,9 @@ public function getIterator(): \Traversable { yield from $this->items; } + + public function count(): int + { + return count($this->items); + } } diff --git a/Classes/Feature/WorkspaceRebase/Exception/WorkspaceRebaseFailed.php b/Classes/Feature/WorkspaceRebase/Exception/WorkspaceRebaseFailed.php index 52b1bdd8..b1bae671 100644 --- a/Classes/Feature/WorkspaceRebase/Exception/WorkspaceRebaseFailed.php +++ b/Classes/Feature/WorkspaceRebase/Exception/WorkspaceRebaseFailed.php @@ -21,12 +21,48 @@ */ final class WorkspaceRebaseFailed extends \Exception { - public function __construct( + private function __construct( public readonly CommandsThatFailedDuringRebase $commandsThatFailedDuringRebase, - string $message = "", - int $code = 0, - ?\Throwable $previous = null + string $message, + int $code, + ?\Throwable $previous, ) { parent::__construct($message, $code, $previous); } + + public static function duringRebase(CommandsThatFailedDuringRebase $commandsThatFailedDuringRebase): self + { + return new self( + $commandsThatFailedDuringRebase, + sprintf('Rebase failed: %s', self::renderMessage($commandsThatFailedDuringRebase)), + 1729974936, + $commandsThatFailedDuringRebase->first()?->exception + ); + } + + public static function duringPublish(CommandsThatFailedDuringRebase $commandsThatFailedDuringRebase): self + { + return new self( + $commandsThatFailedDuringRebase, + sprintf('Publication failed: %s', self::renderMessage($commandsThatFailedDuringRebase)), + 1729974980, + $commandsThatFailedDuringRebase->first()?->exception + ); + } + + public static function duringDiscard(CommandsThatFailedDuringRebase $commandsThatFailedDuringRebase): self + { + return new self( + $commandsThatFailedDuringRebase, + sprintf('Discard failed: %s', self::renderMessage($commandsThatFailedDuringRebase)), + 1729974982, + $commandsThatFailedDuringRebase->first()?->exception + ); + } + + private static function renderMessage(CommandsThatFailedDuringRebase $commandsThatFailedDuringRebase): string + { + $firstFailure = $commandsThatFailedDuringRebase->first(); + return sprintf('"%s" and %d further failures', $firstFailure?->exception->getMessage(), count($commandsThatFailedDuringRebase) - 1); + } }