From 31534e7335761cde7bf7f3851a5b6ed0d2d10c6f Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 23 Oct 2024 10:33:40 +0200 Subject: [PATCH 01/10] TASK: Allow command handlers to yield `EventsToPublish` Using generators will allow us to omit the direct publishing via ->publishEvents or sub command handling via ->handle For exception cases - on a ConcurrencyException - it will be possible to notice the failure in the command handler and act accordingly (by emitting another event and than raising another error). If no error strategy is provided the `ConcurrencyException` will be rethrown into the outer world: ```php $publishResult = yield new EventsToPublish(...); if ($publishResult instanceof EventsToPublishFailed) { yield new EventsToPublish(...); throw new BaseWorkspaceHasBeenModifiedInTheMeantime(); } ``` Catchups will be invoked after each yield to ensure that the system is atomic. --- .../Classes/CommandHandler/CommandBus.php | 6 +- .../CommandHandlerInterface.php | 9 +- .../Classes/ContentRepository.php | 88 ++++--- .../Classes/EventStore/EventPersister.php | 2 +- .../EventStore/EventsToPublishFailed.php | 21 ++ .../Feature/ContentStreamCommandHandler.php | 2 + .../Classes/Feature/ContentStreamHandling.php | 224 ++++++++++++++++++ .../Feature/WorkspaceCommandHandler.php | 114 +++++++-- 8 files changed, 406 insertions(+), 60 deletions(-) create mode 100644 Neos.ContentRepository.Core/Classes/EventStore/EventsToPublishFailed.php create mode 100644 Neos.ContentRepository.Core/Classes/Feature/ContentStreamHandling.php diff --git a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php index b77ce854cf6..b5bd49569a2 100644 --- a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php +++ b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php @@ -7,6 +7,7 @@ use Neos\ContentRepository\Core\CommandHandlingDependencies; use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\EventStore\EventsToPublish; +use Neos\ContentRepository\Core\EventStore\EventsToPublishFailed; /** * Implementation Detail of {@see ContentRepository::handle}, which does the command dispatching to the different @@ -26,7 +27,10 @@ public function __construct(CommandHandlerInterface ...$handlers) $this->handlers = $handlers; } - public function handle(CommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish + /** + * @return EventsToPublish|\Generator + */ + public function handle(CommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish|\Generator { // TODO fail if multiple handlers can handle the same command foreach ($this->handlers as $handler) { diff --git a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php index e190337c370..1196fd957e2 100644 --- a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php +++ b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php @@ -5,8 +5,9 @@ namespace Neos\ContentRepository\Core\CommandHandler; use Neos\ContentRepository\Core\CommandHandlingDependencies; -use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\EventStore\EventsToPublish; +use Neos\ContentRepository\Core\EventStore\EventsToPublishFailed; +use Neos\ContentRepository\Core\EventStore\EventsToPublishToStreams; /** * Common interface for all Content Repository command handlers @@ -19,5 +20,9 @@ interface CommandHandlerInterface { public function canHandle(CommandInterface $command): bool; - public function handle(CommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish; + + /** + * @return EventsToPublish|\Generator + */ + public function handle(CommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish|\Generator; } diff --git a/Neos.ContentRepository.Core/Classes/ContentRepository.php b/Neos.ContentRepository.Core/Classes/ContentRepository.php index 696b2de305d..33ad60e147b 100644 --- a/Neos.ContentRepository.Core/Classes/ContentRepository.php +++ b/Neos.ContentRepository.Core/Classes/ContentRepository.php @@ -24,6 +24,7 @@ use Neos\ContentRepository\Core\EventStore\EventPersister; use Neos\ContentRepository\Core\EventStore\Events; use Neos\ContentRepository\Core\EventStore\EventsToPublish; +use Neos\ContentRepository\Core\EventStore\EventsToPublishFailed; use Neos\ContentRepository\Core\Factory\ContentRepositoryFactory; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\Projection\CatchUp; @@ -47,6 +48,7 @@ use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\Core\SharedModel\Workspace\Workspaces; use Neos\EventStore\EventStoreInterface; +use Neos\EventStore\Exception\ConcurrencyException; use Neos\EventStore\Model\Event\EventMetadata; use Neos\EventStore\Model\EventEnvelope; use Neos\EventStore\Model\EventStream\VirtualStreamName; @@ -101,38 +103,60 @@ public function handle(CommandInterface $command): void { // the commands only calculate which events they want to have published, but do not do the // publishing themselves - $eventsToPublish = $this->commandBus->handle($command, $this->commandHandlingDependencies); - - // TODO meaningful exception message - $initiatingUserId = $this->userIdProvider->getUserId(); - $initiatingTimestamp = $this->clock->now()->format(\DateTimeInterface::ATOM); - - // Add "initiatingUserId" and "initiatingTimestamp" metadata to all events. - // This is done in order to keep information about the _original_ metadata when an - // event is re-applied during publishing/rebasing - // "initiatingUserId": The identifier of the user that originally triggered this event. This will never - // be overridden if it is set once. - // "initiatingTimestamp": The timestamp of the original event. The "recordedAt" timestamp will always be - // re-created and reflects the time an event was actually persisted in a stream, - // the "initiatingTimestamp" will be kept and is never overridden again. - // TODO: cleanup - $eventsToPublish = new EventsToPublish( - $eventsToPublish->streamName, - Events::fromArray( - $eventsToPublish->events->map(function (EventInterface|DecoratedEvent $event) use ( - $initiatingUserId, - $initiatingTimestamp - ) { - $metadata = $event instanceof DecoratedEvent ? $event->eventMetadata?->value ?? [] : []; - $metadata['initiatingUserId'] ??= $initiatingUserId; - $metadata['initiatingTimestamp'] ??= $initiatingTimestamp; - return DecoratedEvent::create($event, metadata: EventMetadata::fromArray($metadata)); - }) - ), - $eventsToPublish->expectedVersion, - ); - - $this->eventPersister->publishEvents($this, $eventsToPublish); + $eventsToPublishOrGenerator = $this->commandBus->handle($command, $this->commandHandlingDependencies); + + // TODO only apply metadata if is copied event?? In generator below we ignore this... + + if ($eventsToPublishOrGenerator instanceof EventsToPublish) { + // TODO meaningful exception message + $initiatingUserId = $this->userIdProvider->getUserId(); + $initiatingTimestamp = $this->clock->now()->format(\DateTimeInterface::ATOM); + + // Add "initiatingUserId" and "initiatingTimestamp" metadata to all events. + // This is done in order to keep information about the _original_ metadata when an + // event is re-applied during publishing/rebasing + // "initiatingUserId": The identifier of the user that originally triggered this event. This will never + // be overridden if it is set once. + // "initiatingTimestamp": The timestamp of the original event. The "recordedAt" timestamp will always be + // re-created and reflects the time an event was actually persisted in a stream, + // the "initiatingTimestamp" will be kept and is never overridden again. + // TODO: cleanup + $eventsToPublish = new EventsToPublish( + $eventsToPublishOrGenerator->streamName, + Events::fromArray( + $eventsToPublishOrGenerator->events->map(function (EventInterface|DecoratedEvent $event) use ( + $initiatingUserId, + $initiatingTimestamp + ) { + $metadata = $event instanceof DecoratedEvent ? $event->eventMetadata?->value ?? [] : []; + $metadata['initiatingUserId'] ??= $initiatingUserId; + $metadata['initiatingTimestamp'] ??= $initiatingTimestamp; + return DecoratedEvent::create($event, metadata: EventMetadata::fromArray($metadata)); + }) + ), + $eventsToPublishOrGenerator->expectedVersion, + ); + $this->eventPersister->publishEvents($this, $eventsToPublish); + } else { + foreach ($eventsToPublishOrGenerator as $eventsToPublish) { + assert($eventsToPublish instanceof EventsToPublish); // just for the ide + + try { + $this->eventPersister->publishEvents($this, $eventsToPublish); + } catch (ConcurrencyException $e) { + $errorStrategy = $eventsToPublishOrGenerator->send(new EventsToPublishFailed( + $eventsToPublish->expectedVersion, + $e + )); + + if ($errorStrategy instanceof EventsToPublish) { + $this->eventPersister->publishEvents($this, $errorStrategy); + } + // if we dont already throw an error throw an error now???? todo + throw $e; + } + } + } } diff --git a/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php b/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php index 7c53549dac8..cfbc738997b 100644 --- a/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php +++ b/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php @@ -4,6 +4,7 @@ namespace Neos\ContentRepository\Core\EventStore; +use Neos\ContentRepository\Core\CommandHandler\EventsPublishResult; use Neos\ContentRepository\Core\ContentRepository; use Neos\EventStore\EventStoreInterface; use Neos\EventStore\Exception\ConcurrencyException; @@ -24,7 +25,6 @@ public function __construct( } /** - * @param EventsToPublish $eventsToPublish * @throws ConcurrencyException in case the expectedVersion does not match */ public function publishEvents(ContentRepository $contentRepository, EventsToPublish $eventsToPublish): void diff --git a/Neos.ContentRepository.Core/Classes/EventStore/EventsToPublishFailed.php b/Neos.ContentRepository.Core/Classes/EventStore/EventsToPublishFailed.php new file mode 100644 index 00000000000..7f5ef383991 --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/EventStore/EventsToPublishFailed.php @@ -0,0 +1,21 @@ +getShortName()); diff --git a/Neos.ContentRepository.Core/Classes/Feature/ContentStreamHandling.php b/Neos.ContentRepository.Core/Classes/Feature/ContentStreamHandling.php new file mode 100644 index 00000000000..76323bde6b9 --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/Feature/ContentStreamHandling.php @@ -0,0 +1,224 @@ +requireContentStreamToNotExistYet($contentStreamId, $commandHandlingDependencies); + $streamName = ContentStreamEventStreamName::fromContentStreamId($contentStreamId) + ->getEventStreamName(); + + return new EventsToPublish( + $streamName, + Events::with( + new ContentStreamWasCreated( + $contentStreamId, + ) + ), + ExpectedVersion::NO_STREAM() + ); + } + + /** + * @param ContentStreamId $contentStreamId The id of the content stream to close + * @param CommandHandlingDependencies $commandHandlingDependencies + * @return EventsToPublish + */ + private function closeContentStream( + ContentStreamId $contentStreamId, + CommandHandlingDependencies $commandHandlingDependencies, + ): EventsToPublish { + $this->requireContentStreamToExist($contentStreamId, $commandHandlingDependencies); + $expectedVersion = $this->getExpectedVersionOfContentStream($contentStreamId, $commandHandlingDependencies); + $this->requireContentStreamToNotBeClosed($contentStreamId, $commandHandlingDependencies); + $streamName = ContentStreamEventStreamName::fromContentStreamId($contentStreamId)->getEventStreamName(); + + return new EventsToPublish( + $streamName, + Events::with( + new ContentStreamWasClosed( + $contentStreamId, + ), + ), + $expectedVersion + ); + } + + /** + * @param ContentStreamId $contentStreamId The id of the content stream to reopen + * @param ContentStreamStatus $previousState The state the content stream was in before closing and is to be reset to + */ + private function reopenContentStream( + ContentStreamId $contentStreamId, + ContentStreamStatus $previousState, + CommandHandlingDependencies $commandHandlingDependencies, + ): EventsToPublish { + $this->requireContentStreamToExist($contentStreamId, $commandHandlingDependencies); + // todo dont we want to make sure this always succeeds? e.g. ANY? + $expectedVersion = $this->getExpectedVersionOfContentStream($contentStreamId, $commandHandlingDependencies); + $this->requireContentStreamToBeClosed($contentStreamId, $commandHandlingDependencies); + $streamName = ContentStreamEventStreamName::fromContentStreamId($contentStreamId)->getEventStreamName(); + + return new EventsToPublish( + $streamName, + Events::with( + new ContentStreamWasReopened( + $contentStreamId, + $previousState, + ), + ), + $expectedVersion + ); + } + + /** + * @param ContentStreamId $newContentStreamId The id of the new content stream + * @param ContentStreamId $sourceContentStreamId The id of the content stream to fork + * @throws ContentStreamAlreadyExists + * @throws ContentStreamDoesNotExistYet + */ + private function forkContentStream( + ContentStreamId $newContentStreamId, + ContentStreamId $sourceContentStreamId, + CommandHandlingDependencies $commandHandlingDependencies + ): EventsToPublish { + $this->requireContentStreamToExist($sourceContentStreamId, $commandHandlingDependencies); + $this->requireContentStreamToNotBeClosed($sourceContentStreamId, $commandHandlingDependencies); + $this->requireContentStreamToNotExistYet($newContentStreamId, $commandHandlingDependencies); + + $sourceContentStreamVersion = $commandHandlingDependencies->getContentStreamVersion($sourceContentStreamId); + + $streamName = ContentStreamEventStreamName::fromContentStreamId($newContentStreamId) + ->getEventStreamName(); + + return new EventsToPublish( + $streamName, + Events::with( + new ContentStreamWasForked( + $newContentStreamId, + $sourceContentStreamId, + $sourceContentStreamVersion, + ), + ), + // NO_STREAM to ensure the "fork" happens as the first event of the new content stream + ExpectedVersion::NO_STREAM() + ); + } + + /** + * @param ContentStreamId $contentStreamId The id of the content stream to remove + */ + private function removeContentStream( + ContentStreamId $contentStreamId, + CommandHandlingDependencies $commandHandlingDependencies + ): EventsToPublish { + $this->requireContentStreamToExist($contentStreamId, $commandHandlingDependencies); + $expectedVersion = $this->getExpectedVersionOfContentStream($contentStreamId, $commandHandlingDependencies); + + $streamName = ContentStreamEventStreamName::fromContentStreamId( + $contentStreamId + )->getEventStreamName(); + + return new EventsToPublish( + $streamName, + Events::with( + new ContentStreamWasRemoved( + $contentStreamId, + ), + ), + $expectedVersion + ); + } + + /** + * @param ContentStreamId $contentStreamId + * @param CommandHandlingDependencies $commandHandlingDependencies + * @throws ContentStreamAlreadyExists + */ + private function requireContentStreamToNotExistYet( + ContentStreamId $contentStreamId, + CommandHandlingDependencies $commandHandlingDependencies + ): void { + if ($commandHandlingDependencies->contentStreamExists($contentStreamId)) { + throw new ContentStreamAlreadyExists( + 'Content stream "' . $contentStreamId->value . '" already exists.', + 1521386345 + ); + } + } + + /** + * @param ContentStreamId $contentStreamId + * @param CommandHandlingDependencies $commandHandlingDependencies + * @throws ContentStreamDoesNotExistYet + */ + private function requireContentStreamToExist( + ContentStreamId $contentStreamId, + CommandHandlingDependencies $commandHandlingDependencies + ): void { + if (!$commandHandlingDependencies->contentStreamExists($contentStreamId)) { + throw new ContentStreamDoesNotExistYet( + 'Content stream "' . $contentStreamId->value . '" does not exist yet.', + 1521386692 + ); + } + } + + private function requireContentStreamToNotBeClosed( + ContentStreamId $contentStreamId, + CommandHandlingDependencies $commandHandlingDependencies + ): void { + if ($commandHandlingDependencies->getContentStreamStatus($contentStreamId) === ContentStreamStatus::CLOSED) { + throw new ContentStreamIsClosed( + 'Content stream "' . $contentStreamId->value . '" is closed.', + 1710260081 + ); + } + } + + private function requireContentStreamToBeClosed( + ContentStreamId $contentStreamId, + CommandHandlingDependencies $commandHandlingDependencies + ): void { + if ($commandHandlingDependencies->getContentStreamStatus($contentStreamId) !== ContentStreamStatus::CLOSED) { + throw new ContentStreamIsNotClosed( + 'Content stream "' . $contentStreamId->value . '" is not closed.', + 1710405911 + ); + } + } + + private function getExpectedVersionOfContentStream( + ContentStreamId $contentStreamId, + CommandHandlingDependencies $commandHandlingDependencies + ): ExpectedVersion { + $version = $commandHandlingDependencies->getContentStreamVersion($contentStreamId); + return ExpectedVersion::fromVersion($version); + } +} diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php index 1851a976560..959dea4e357 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php @@ -16,6 +16,7 @@ use Neos\ContentRepository\Core\CommandHandler\CommandHandlerInterface; use Neos\ContentRepository\Core\CommandHandler\CommandInterface; +use Neos\ContentRepository\Core\CommandHandler\EventsPublishResult; use Neos\ContentRepository\Core\CommandHandlingDependencies; use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\EventStore\DecoratedEvent; @@ -24,6 +25,8 @@ use Neos\ContentRepository\Core\EventStore\EventPersister; use Neos\ContentRepository\Core\EventStore\Events; use Neos\ContentRepository\Core\EventStore\EventsToPublish; +use Neos\ContentRepository\Core\EventStore\EventsToPublishFailed; +use Neos\ContentRepository\Core\EventStore\EventsToPublishToStreams; use Neos\ContentRepository\Core\Feature\Common\MatchableWithNodeIdToPublishOrDiscardInterface; use Neos\ContentRepository\Core\Feature\Common\PublishableToWorkspaceInterface; use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface; @@ -62,17 +65,16 @@ use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Dto\RebaseErrorHandlingStrategy; use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Event\WorkspaceWasRebased; use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Exception\WorkspaceRebaseFailed; -use Neos\ContentRepository\Core\SharedModel\Workspace\Workspace; use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamAlreadyExists; use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamDoesNotExistYet; use Neos\ContentRepository\Core\SharedModel\Exception\WorkspaceDoesNotExist; use Neos\ContentRepository\Core\SharedModel\Exception\WorkspaceHasNoBaseWorkspaceName; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; +use Neos\ContentRepository\Core\SharedModel\Workspace\Workspace; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\EventStore\EventStoreInterface; use Neos\EventStore\Exception\ConcurrencyException; use Neos\EventStore\Model\Event\EventType; -use Neos\EventStore\Model\EventEnvelope; use Neos\EventStore\Model\EventStream\ExpectedVersion; /** @@ -80,6 +82,8 @@ */ final readonly class WorkspaceCommandHandler implements CommandHandlerInterface { + use ContentStreamHandling; + public function __construct( private EventPersister $eventPersister, private EventStoreInterface $eventStore, @@ -92,7 +96,7 @@ public function canHandle(CommandInterface $command): bool return method_exists($this, 'handle' . (new \ReflectionClass($command))->getShortName()); } - public function handle(CommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish + public function handle(CommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish|\Generator { /** @phpstan-ignore-next-line */ return match ($command::class) { @@ -196,44 +200,107 @@ private function handleCreateRootWorkspace( private function handlePublishWorkspace( PublishWorkspace $command, CommandHandlingDependencies $commandHandlingDependencies, - ): EventsToPublish { + ): \Generator { $workspace = $this->requireWorkspace($command->workspaceName, $commandHandlingDependencies); $baseWorkspace = $this->requireBaseWorkspace($workspace, $commandHandlingDependencies); - $this->publishContentStream( - $commandHandlingDependencies, + $publishResult = yield $this->getCopiedEventsToPublishForContentStream( $workspace->currentContentStreamId, $baseWorkspace->workspaceName, $baseWorkspace->currentContentStreamId ); + if ($publishResult instanceof EventsToPublishFailed) { + throw new BaseWorkspaceHasBeenModifiedInTheMeantime(sprintf( + 'The base workspace has been modified in the meantime; please rebase.' + . ' Expected version %d of source content stream %s', + $publishResult->expectedVersion->value, + $baseWorkspace->currentContentStreamId + )); + } + // After publishing a workspace, we need to again fork from Base. - $commandHandlingDependencies->handle( - ForkContentStream::create( - $command->newContentStreamId, - $baseWorkspace->currentContentStreamId, - ) + yield $this->forkContentStream( + $command->newContentStreamId, + $baseWorkspace->currentContentStreamId, + $commandHandlingDependencies ); - $streamName = WorkspaceEventStreamName::fromWorkspaceName($command->workspaceName)->getEventStreamName(); - $events = Events::with( - new WorkspaceWasPublished( - $command->workspaceName, - $baseWorkspace->workspaceName, - $command->newContentStreamId, - $workspace->currentContentStreamId, - ) + // if we got so far without an Exception, we can switch the Workspace's active Content stream. + yield new EventsToPublish( + WorkspaceEventStreamName::fromWorkspaceName($command->workspaceName)->getEventStreamName(), + Events::with( + new WorkspaceWasPublished( + $command->workspaceName, + $baseWorkspace->workspaceName, + $command->newContentStreamId, + $workspace->currentContentStreamId, + ) + ), + ExpectedVersion::ANY() ); + } + + /** + * @throws BaseWorkspaceHasBeenModifiedInTheMeantime + * @throws \Exception + */ + private function getCopiedEventsToPublishForContentStream( + ContentStreamId $contentStreamId, + WorkspaceName $baseWorkspaceName, + ContentStreamId $baseContentStreamId, + ): EventsToPublish { + $baseWorkspaceContentStreamName = ContentStreamEventStreamName::fromContentStreamId( + $baseContentStreamId + ); + + // TODO: please check the code below in-depth. it does: + // - copy all events from the "user" content stream which implement @see{}"PublishableToOtherContentStreamsInterface" + // - extract the initial ContentStreamWasForked event, + // to read the version of the source content stream when the fork occurred + // - ensure that no other changes have been done in the meantime in the base content stream + + $workspaceContentStream = iterator_to_array($this->eventStore->load( + ContentStreamEventStreamName::fromContentStreamId($contentStreamId)->getEventStreamName() + )); + + $events = []; + $contentStreamWasForkedEvent = null; + foreach ($workspaceContentStream as $eventEnvelope) { + $event = $this->eventNormalizer->denormalize($eventEnvelope->event); + + if ($event instanceof ContentStreamWasForked) { + if ($contentStreamWasForkedEvent !== null) { + throw new \RuntimeException( + 'Invariant violation: The content stream "' . $contentStreamId->value + . '" has two forked events.', + 1658740373 + ); + } + $contentStreamWasForkedEvent = $event; + } elseif ($event instanceof PublishableToWorkspaceInterface) { + /** @var EventInterface $copiedEvent */ + $copiedEvent = $event->withWorkspaceNameAndContentStreamId($baseWorkspaceName, $baseContentStreamId); + // We need to add the event metadata here for rebasing in nested workspace situations + // (and for exporting) + $events[] = DecoratedEvent::create($copiedEvent, metadata: $eventEnvelope->event->metadata, causationId: $eventEnvelope->event->causationId, correlationId: $eventEnvelope->event->correlationId); + } + } + + if ($contentStreamWasForkedEvent === null) { + throw new \RuntimeException('Invariant violation: The content stream "' . $contentStreamId->value + . '" has NO forked event.', 1658740407); + } - // if we got so far without an Exception, we can switch the Workspace's active Content stream. return new EventsToPublish( - $streamName, - $events, - ExpectedVersion::ANY() + $baseWorkspaceContentStreamName->getEventStreamName(), + Events::fromArray($events), + ExpectedVersion::fromVersion($contentStreamWasForkedEvent->versionOfSourceContentStream) ); } /** + * @deprecated FIXME REMOVE with https://github.com/neos/neos-development-collection/pull/5301 * @throws BaseWorkspaceHasBeenModifiedInTheMeantime * @throws \Exception */ @@ -256,7 +323,6 @@ private function publishContentStream( $workspaceContentStream = iterator_to_array($this->eventStore->load( ContentStreamEventStreamName::fromContentStreamId($contentStreamId)->getEventStreamName() )); - /** @var array $workspaceContentStream */ $events = []; $contentStreamWasForkedEvent = null; From ed7d9d299a9e2a3d2f5f4c12b316b6d5eb93955b Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 23 Oct 2024 10:36:42 +0200 Subject: [PATCH 02/10] TASK: Remove duplicated step in behat test --- .../W8-IndividualNodePublication/02-BasicFeatures.feature | 1 - 1 file changed, 1 deletion(-) diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/02-BasicFeatures.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/02-BasicFeatures.feature index 487007ba37d..f5e31578cf4 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/02-BasicFeatures.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/W8-IndividualNodePublication/02-BasicFeatures.feature @@ -41,7 +41,6 @@ Feature: Individual node publication Scenario: It is possible to publish a single node; and only this one is live. # create nodes in user WS Given I am in workspace "user-test" - And I am in workspace "user-test" And I am in dimension space point {} And the following CreateNodeAggregateWithNode commands are executed: | nodeAggregateId | nodeTypeName | parentNodeAggregateId | nodeName | tetheredDescendantNodeAggregateIds | From fe368c7690ac8a3ea0e317ab2b15d0d4f300cf7c Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 23 Oct 2024 10:39:45 +0200 Subject: [PATCH 03/10] TASK: Cleanup `RebasableToOtherWorkspaceInterface` extraction And enforce type of `RebasableToOtherWorkspaceInterface` earlier. To be noted: The rebase and discard operation previously only required `fromArray` to be implemented, now we also strict check against: RebasableToOtherWorkspaceInterface --- .../CommandHandlerInterface.php | 1 - .../Classes/EventStore/EventPersister.php | 1 - .../Feature/WorkspaceCommandHandler.php | 34 +++++++------------ 3 files changed, 12 insertions(+), 24 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php index 1196fd957e2..42381f463f8 100644 --- a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php +++ b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php @@ -7,7 +7,6 @@ use Neos\ContentRepository\Core\CommandHandlingDependencies; use Neos\ContentRepository\Core\EventStore\EventsToPublish; use Neos\ContentRepository\Core\EventStore\EventsToPublishFailed; -use Neos\ContentRepository\Core\EventStore\EventsToPublishToStreams; /** * Common interface for all Content Repository command handlers diff --git a/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php b/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php index cfbc738997b..c01aa27b45e 100644 --- a/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php +++ b/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php @@ -4,7 +4,6 @@ namespace Neos\ContentRepository\Core\EventStore; -use Neos\ContentRepository\Core\CommandHandler\EventsPublishResult; use Neos\ContentRepository\Core\ContentRepository; use Neos\EventStore\EventStoreInterface; use Neos\EventStore\Exception\ConcurrencyException; diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php index 959dea4e357..c1a8c24c186 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php @@ -16,7 +16,6 @@ use Neos\ContentRepository\Core\CommandHandler\CommandHandlerInterface; use Neos\ContentRepository\Core\CommandHandler\CommandInterface; -use Neos\ContentRepository\Core\CommandHandler\EventsPublishResult; use Neos\ContentRepository\Core\CommandHandlingDependencies; use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\EventStore\DecoratedEvent; @@ -26,7 +25,6 @@ use Neos\ContentRepository\Core\EventStore\Events; use Neos\ContentRepository\Core\EventStore\EventsToPublish; use Neos\ContentRepository\Core\EventStore\EventsToPublishFailed; -use Neos\ContentRepository\Core\EventStore\EventsToPublishToStreams; use Neos\ContentRepository\Core\Feature\Common\MatchableWithNodeIdToPublishOrDiscardInterface; use Neos\ContentRepository\Core\Feature\Common\PublishableToWorkspaceInterface; use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherWorkspaceInterface; @@ -470,7 +468,7 @@ function () use ($originalCommands, $commandHandlingDependencies, &$commandsThat } /** - * @return array + * @return array */ private function extractCommandsFromContentStreamMetadata( ContentStreamEventStreamName $workspaceContentStreamName, @@ -485,17 +483,18 @@ private function extractCommandsFromContentStreamMetadata( if (isset($metadata['commandClass'])) { $commandToRebaseClass = $metadata['commandClass']; $commandToRebasePayload = $metadata['commandPayload']; - if (!method_exists($commandToRebaseClass, 'fromArray')) { + + if (array_diff(class_implements($commandToRebaseClass) ?: [], [CommandInterface::class, RebasableToOtherWorkspaceInterface::class]) === []) { throw new \RuntimeException(sprintf( - 'Command "%s" can\'t be rebased because it does not implement a static "fromArray" constructor', - $commandToRebaseClass + 'Command "%s" can\'t be rebased because it does not implement %s', + $commandToRebaseClass, + RebasableToOtherWorkspaceInterface::class ), 1547815341); } - /** - * The "fromArray" might be declared via {@see RebasableToOtherWorkspaceInterface::fromArray()} - * or any other command can just implement it. - */ - $commands[$eventEnvelope->sequenceNumber->value] = $commandToRebaseClass::fromArray($commandToRebasePayload); + /** @var class-string $commandToRebaseClass */ + /** @var CommandInterface&RebasableToOtherWorkspaceInterface $commandInstance */ + $commandInstance = $commandToRebaseClass::fromArray($commandToRebasePayload); + $commands[$eventEnvelope->sequenceNumber->value] = $commandInstance; } } @@ -535,8 +534,6 @@ private function handlePublishIndividualNodesFromWorkspace( $matchingCommands = []; $remainingCommands = []; $this->separateMatchingAndRemainingCommands($command, $workspace, $matchingCommands, $remainingCommands); - /** @var array $matchingCommands */ - /** @var array $remainingCommands */ // 3) fork a new contentStream, based on the base WS, and apply MATCHING $commandHandlingDependencies->handle( @@ -674,9 +671,7 @@ private function handleDiscardIndividualNodesFromWorkspace( // 2) filter commands, only keeping the ones NOT MATCHING the nodes from the command // (i.e. the modifications we want to keep) - /** @var array $commandsToDiscard */ $commandsToDiscard = []; - /** @var array $commandsToKeep */ $commandsToKeep = []; $this->separateMatchingAndRemainingCommands($command, $workspace, $commandsToDiscard, $commandsToKeep); @@ -695,13 +690,6 @@ private function handleDiscardIndividualNodesFromWorkspace( $command->newContentStreamId, function () use ($commandsToKeep, $commandHandlingDependencies, $baseWorkspace): void { foreach ($commandsToKeep as $matchingCommand) { - if (!($matchingCommand instanceof RebasableToOtherWorkspaceInterface)) { - throw new \RuntimeException( - 'ERROR: The command ' . get_class($matchingCommand) - . ' does not implement ' . RebasableToOtherWorkspaceInterface::class . '; but it should!' - ); - } - $commandHandlingDependencies->handle($matchingCommand->createCopyForWorkspace( $baseWorkspace->workspaceName, )); @@ -749,6 +737,8 @@ function () use ($commandsToKeep, $commandHandlingDependencies, $baseWorkspace): /** * @param array &$matchingCommands * @param array &$remainingCommands + * @param-out array $matchingCommands + * @param-out array $remainingCommands */ private function separateMatchingAndRemainingCommands( PublishIndividualNodesFromWorkspace|DiscardIndividualNodesFromWorkspace $command, From bb3c2a7208d5edf023d4bd059c0ac8cc24c7444c Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 23 Oct 2024 11:14:40 +0200 Subject: [PATCH 04/10] TASK: Avoid use of sub commands in WorkspaceCommandHandler rebase, discard & publish individual will be migrated via --- .../Feature/WorkspaceCommandHandler.php | 145 ++++++++---------- 1 file changed, 63 insertions(+), 82 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php index c1a8c24c186..1668323ea25 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php @@ -119,7 +119,7 @@ public function handle(CommandInterface $command, CommandHandlingDependencies $c private function handleCreateWorkspace( CreateWorkspace $command, CommandHandlingDependencies $commandHandlingDependencies, - ): EventsToPublish { + ): \Generator { $this->requireWorkspaceToNotExist($command->workspaceName, $commandHandlingDependencies); if ($commandHandlingDependencies->findWorkspaceByName($command->baseWorkspaceName) === null) { throw new BaseWorkspaceDoesNotExist(sprintf( @@ -131,57 +131,50 @@ private function handleCreateWorkspace( $baseWorkspaceContentGraph = $commandHandlingDependencies->getContentGraph($command->baseWorkspaceName); // When the workspace is created, we first have to fork the content stream - $commandHandlingDependencies->handle( - ForkContentStream::create( - $command->newContentStreamId, - $baseWorkspaceContentGraph->getContentStreamId(), - ) - ); - - $events = Events::with( - new WorkspaceWasCreated( - $command->workspaceName, - $command->baseWorkspaceName, - $command->newContentStreamId, - ) + yield $this->forkContentStream( + $command->newContentStreamId, + $baseWorkspaceContentGraph->getContentStreamId(), + $commandHandlingDependencies ); - return new EventsToPublish( + yield new EventsToPublish( WorkspaceEventStreamName::fromWorkspaceName($command->workspaceName)->getEventStreamName(), - $events, + Events::with( + new WorkspaceWasCreated( + $command->workspaceName, + $command->baseWorkspaceName, + $command->newContentStreamId, + ) + ), ExpectedVersion::ANY() ); } /** * @param CreateRootWorkspace $command - * @return EventsToPublish * @throws WorkspaceAlreadyExists * @throws ContentStreamAlreadyExists */ private function handleCreateRootWorkspace( CreateRootWorkspace $command, CommandHandlingDependencies $commandHandlingDependencies, - ): EventsToPublish { + ): \Generator { $this->requireWorkspaceToNotExist($command->workspaceName, $commandHandlingDependencies); $newContentStreamId = $command->newContentStreamId; - $commandHandlingDependencies->handle( - CreateContentStream::create( - $newContentStreamId, - ) - ); - - $events = Events::with( - new RootWorkspaceWasCreated( - $command->workspaceName, - $newContentStreamId - ) + yield $this->createContentStream( + $newContentStreamId, + $commandHandlingDependencies ); - return new EventsToPublish( + yield new EventsToPublish( WorkspaceEventStreamName::fromWorkspaceName($command->workspaceName)->getEventStreamName(), - $events, + Events::with( + new RootWorkspaceWasCreated( + $command->workspaceName, + $newContentStreamId + ) + ), ExpectedVersion::ANY() ); } @@ -792,31 +785,27 @@ private function commandMatchesAtLeastOneNode( private function handleDiscardWorkspace( DiscardWorkspace $command, CommandHandlingDependencies $commandHandlingDependencies, - ): EventsToPublish { + ): \Generator { $workspace = $this->requireWorkspace($command->workspaceName, $commandHandlingDependencies); $baseWorkspace = $this->requireBaseWorkspace($workspace, $commandHandlingDependencies); $newContentStream = $command->newContentStreamId; - $commandHandlingDependencies->handle( - ForkContentStream::create( - $newContentStream, - $baseWorkspace->currentContentStreamId, - ) + yield $this->forkContentStream( + $newContentStream, + $baseWorkspace->currentContentStreamId, + $commandHandlingDependencies ); // if we got so far without an Exception, we can switch the Workspace's active Content stream. - $streamName = WorkspaceEventStreamName::fromWorkspaceName($command->workspaceName)->getEventStreamName(); - $events = Events::with( - new WorkspaceWasDiscarded( - $command->workspaceName, - $newContentStream, - $workspace->currentContentStreamId, - ) - ); - - return new EventsToPublish( - $streamName, - $events, + yield new EventsToPublish( + WorkspaceEventStreamName::fromWorkspaceName($command->workspaceName)->getEventStreamName(), + Events::with( + new WorkspaceWasDiscarded( + $command->workspaceName, + $newContentStream, + $workspace->currentContentStreamId, + ) + ), ExpectedVersion::ANY() ); } @@ -832,7 +821,7 @@ private function handleDiscardWorkspace( private function handleChangeBaseWorkspace( ChangeBaseWorkspace $command, CommandHandlingDependencies $commandHandlingDependencies, - ): EventsToPublish { + ): \Generator { $workspace = $this->requireWorkspace($command->workspaceName, $commandHandlingDependencies); $this->requireEmptyWorkspace($workspace); $this->requireBaseWorkspace($workspace, $commandHandlingDependencies); @@ -840,25 +829,21 @@ private function handleChangeBaseWorkspace( $this->requireNonCircularRelationBetweenWorkspaces($workspace, $baseWorkspace, $commandHandlingDependencies); - $commandHandlingDependencies->handle( - ForkContentStream::create( - $command->newContentStreamId, - $baseWorkspace->currentContentStreamId, - ) - ); - - $streamName = WorkspaceEventStreamName::fromWorkspaceName($command->workspaceName)->getEventStreamName(); - $events = Events::with( - new WorkspaceBaseWorkspaceWasChanged( - $command->workspaceName, - $command->baseWorkspaceName, - $command->newContentStreamId, - ) + yield $this->forkContentStream( + $command->newContentStreamId, + $baseWorkspace->currentContentStreamId, + $commandHandlingDependencies ); - return new EventsToPublish( - $streamName, - $events, + yield new EventsToPublish( + WorkspaceEventStreamName::fromWorkspaceName($command->workspaceName)->getEventStreamName(), + Events::with( + new WorkspaceBaseWorkspaceWasChanged( + $command->workspaceName, + $command->baseWorkspaceName, + $command->newContentStreamId, + ) + ), ExpectedVersion::ANY() ); } @@ -869,25 +854,21 @@ private function handleChangeBaseWorkspace( private function handleDeleteWorkspace( DeleteWorkspace $command, CommandHandlingDependencies $commandHandlingDependencies, - ): EventsToPublish { + ): \Generator { $workspace = $this->requireWorkspace($command->workspaceName, $commandHandlingDependencies); - $commandHandlingDependencies->handle( - RemoveContentStream::create( - $workspace->currentContentStreamId - ) - ); - - $events = Events::with( - new WorkspaceWasRemoved( - $command->workspaceName, - ) + yield $this->removeContentStream( + $workspace->currentContentStreamId, + $commandHandlingDependencies ); - $streamName = WorkspaceEventStreamName::fromWorkspaceName($command->workspaceName)->getEventStreamName(); - return new EventsToPublish( - $streamName, - $events, + yield new EventsToPublish( + WorkspaceEventStreamName::fromWorkspaceName($command->workspaceName)->getEventStreamName(), + Events::with( + new WorkspaceWasRemoved( + $command->workspaceName, + ) + ), ExpectedVersion::ANY() ); } From 2617356e84bb107a3f64f3c4493e081a2e7e62e5 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 23 Oct 2024 11:20:21 +0200 Subject: [PATCH 05/10] TASK: Prepare full removal of legacy content stream commands --- .../Command/PerformanceMeasurementService.php | 10 +- ...ForkContentStream_ConstraintChecks.feature | 11 +- .../Command/CloseContentStream.php | 5 +- .../Feature/ContentStreamCommandHandler.php | 172 +----------------- .../Command/CreateContentStream.php | 45 ----- .../Command/RemoveContentStream.php | 6 +- .../Features/Bootstrap/CRTestSuiteTrait.php | 2 - .../Features/ContentStreamForking.php | 60 ------ .../Bootstrap/Features/WorkspaceCreation.php | 30 ++- 9 files changed, 35 insertions(+), 306 deletions(-) delete mode 100644 Neos.ContentRepository.Core/Classes/Feature/ContentStreamCreation/Command/CreateContentStream.php delete mode 100644 Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/ContentStreamForking.php diff --git a/Neos.ContentRepository.BehavioralTests/Classes/Command/PerformanceMeasurementService.php b/Neos.ContentRepository.BehavioralTests/Classes/Command/PerformanceMeasurementService.php index cc67508bade..2688faa7a66 100644 --- a/Neos.ContentRepository.BehavioralTests/Classes/Command/PerformanceMeasurementService.php +++ b/Neos.ContentRepository.BehavioralTests/Classes/Command/PerformanceMeasurementService.php @@ -26,7 +26,6 @@ use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; use Neos\ContentRepository\Core\Feature\Common\InterdimensionalSiblings; use Neos\ContentRepository\Core\Feature\ContentStreamEventStreamName; -use Neos\ContentRepository\Core\Feature\ContentStreamForking\Command\ForkContentStream; use Neos\ContentRepository\Core\Feature\NodeCreation\Event\NodeAggregateWithNodeWasCreated; use Neos\ContentRepository\Core\Feature\NodeModification\Dto\SerializedPropertyValues; use Neos\ContentRepository\Core\Feature\RootNodeCreation\Event\RootNodeAggregateWithNodeWasCreated; @@ -154,9 +153,10 @@ private function createHierarchy( public function forkContentStream(): void { - $this->contentRepository->handle(ForkContentStream::create( - ContentStreamId::create(), - $this->contentStreamId, - )); + throw new \BadMethodCallException('not implemented'); + // $this->contentRepository->handle(ForkContentStream::create( + // ContentStreamId::create(), + // $this->contentStreamId, + // )); } } diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ContentStreamForking/01-ForkContentStream_ConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ContentStreamForking/01-ForkContentStream_ConstraintChecks.feature index 8e1d7e8a71d..b27aa459a09 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ContentStreamForking/01-ForkContentStream_ConstraintChecks.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ContentStreamForking/01-ForkContentStream_ConstraintChecks.feature @@ -49,12 +49,13 @@ Feature: ForkContentStream Without Dimensions | propertyValues | {"text": {"value": "original value", "type": "string"}} | | propertiesToUnset | {} | - Scenario: Try to fork a content stream that is closed: + Scenario: Try to fork a content stream (use it as base workspace) that is closed: When the command CloseContentStream is executed with payload: | Key | Value | | contentStreamId | "cs-identifier" | - When the command ForkContentStream is executed with payload and exceptions are caught: - | Key | Value | - | contentStreamId | "user-cs-identifier" | - | sourceContentStreamId | "cs-identifier" | + When the command CreateWorkspace is executed with payload and exceptions are caught: + | Key | Value | + | workspaceName | "user-test" | + | baseWorkspaceName | "live" | + | newContentStreamId | "user-cs-identifier" | Then the last command should have thrown an exception of type "ContentStreamIsClosed" diff --git a/Neos.ContentRepository.Core/Classes/Feature/ContentStreamClosing/Command/CloseContentStream.php b/Neos.ContentRepository.Core/Classes/Feature/ContentStreamClosing/Command/CloseContentStream.php index 16dfd711342..3234b633586 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/ContentStreamClosing/Command/CloseContentStream.php +++ b/Neos.ContentRepository.Core/Classes/Feature/ContentStreamClosing/Command/CloseContentStream.php @@ -18,9 +18,8 @@ use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; /** - * @internal implementation detail. You must not use this command directly. - * Direct use may lead to hard to revert senseless state in your content repository. - * Please use the higher level workspace commands instead. + * @internal only exposed for testing purposes. You must not use this command directly. + * Direct use will leave your content stream in a closed state. */ final readonly class CloseContentStream implements CommandInterface { diff --git a/Neos.ContentRepository.Core/Classes/Feature/ContentStreamCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/ContentStreamCommandHandler.php index 54245c7b61e..2b129e8286a 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/ContentStreamCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/ContentStreamCommandHandler.php @@ -18,30 +18,19 @@ use Neos\ContentRepository\Core\CommandHandler\CommandInterface; use Neos\ContentRepository\Core\CommandHandlingDependencies; use Neos\ContentRepository\Core\ContentRepository; -use Neos\ContentRepository\Core\EventStore\Events; use Neos\ContentRepository\Core\EventStore\EventsToPublish; use Neos\ContentRepository\Core\Feature\ContentStreamClosing\Command\CloseContentStream; use Neos\ContentRepository\Core\Feature\ContentStreamClosing\Command\ReopenContentStream; -use Neos\ContentRepository\Core\Feature\ContentStreamClosing\Event\ContentStreamWasClosed; -use Neos\ContentRepository\Core\Feature\ContentStreamClosing\Event\ContentStreamWasReopened; -use Neos\ContentRepository\Core\Feature\ContentStreamCreation\Command\CreateContentStream; -use Neos\ContentRepository\Core\Feature\ContentStreamCreation\Event\ContentStreamWasCreated; use Neos\ContentRepository\Core\Feature\ContentStreamForking\Command\ForkContentStream; -use Neos\ContentRepository\Core\Feature\ContentStreamForking\Event\ContentStreamWasForked; use Neos\ContentRepository\Core\Feature\ContentStreamRemoval\Command\RemoveContentStream; -use Neos\ContentRepository\Core\Feature\ContentStreamRemoval\Event\ContentStreamWasRemoved; use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamAlreadyExists; use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamDoesNotExistYet; -use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamIsClosed; -use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamIsNotClosed; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamStatus; -use Neos\EventStore\Model\EventStream\ExpectedVersion; /** * INTERNALS. Only to be used from WorkspaceCommandHandler!!! * * @internal from userland, you'll use ContentRepository::handle to dispatch commands + * FIXME try to fully get rid of this handler :D and the external commands! */ final class ContentStreamCommandHandler implements CommandHandlerInterface { @@ -55,7 +44,6 @@ public function canHandle(CommandInterface $command): bool public function handle(CommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish { return match ($command::class) { - CreateContentStream::class => $this->handleCreateContentStream($command, $commandHandlingDependencies), CloseContentStream::class => $this->handleCloseContentStream($command, $commandHandlingDependencies), ReopenContentStream::class => $this->handleReopenContentStream($command, $commandHandlingDependencies), ForkContentStream::class => $this->handleForkContentStream($command, $commandHandlingDependencies), @@ -64,67 +52,18 @@ public function handle(CommandInterface $command, CommandHandlingDependencies $c }; } - /** - * @throws ContentStreamAlreadyExists - */ - private function handleCreateContentStream( - CreateContentStream $command, - CommandHandlingDependencies $commandHandlingDependencies, - ): EventsToPublish { - $this->requireContentStreamToNotExistYet($command->contentStreamId, $commandHandlingDependencies); - $streamName = ContentStreamEventStreamName::fromContentStreamId($command->contentStreamId) - ->getEventStreamName(); - - return new EventsToPublish( - $streamName, - Events::with( - new ContentStreamWasCreated( - $command->contentStreamId, - ) - ), - ExpectedVersion::NO_STREAM() - ); - } - private function handleCloseContentStream( CloseContentStream $command, CommandHandlingDependencies $commandHandlingDependencies, ): EventsToPublish { - $this->requireContentStreamToExist($command->contentStreamId, $commandHandlingDependencies); - $expectedVersion = $this->getExpectedVersionOfContentStream($command->contentStreamId, $commandHandlingDependencies); - $this->requireContentStreamToNotBeClosed($command->contentStreamId, $commandHandlingDependencies); - $streamName = ContentStreamEventStreamName::fromContentStreamId($command->contentStreamId)->getEventStreamName(); - - return new EventsToPublish( - $streamName, - Events::with( - new ContentStreamWasClosed( - $command->contentStreamId, - ), - ), - $expectedVersion - ); + return $this->closeContentStream($command->contentStreamId, $commandHandlingDependencies); } private function handleReopenContentStream( ReopenContentStream $command, CommandHandlingDependencies $commandHandlingDependencies, ): EventsToPublish { - $this->requireContentStreamToExist($command->contentStreamId, $commandHandlingDependencies); - $expectedVersion = $this->getExpectedVersionOfContentStream($command->contentStreamId, $commandHandlingDependencies); - $this->requireContentStreamToBeClosed($command->contentStreamId, $commandHandlingDependencies); - $streamName = ContentStreamEventStreamName::fromContentStreamId($command->contentStreamId)->getEventStreamName(); - - return new EventsToPublish( - $streamName, - Events::with( - new ContentStreamWasReopened( - $command->contentStreamId, - $command->previousState, - ), - ), - $expectedVersion - ); + return $this->reopenContentStream($command->contentStreamId, $command->previousState, $commandHandlingDependencies); } /** @@ -135,114 +74,13 @@ private function handleForkContentStream( ForkContentStream $command, CommandHandlingDependencies $commandHandlingDependencies ): EventsToPublish { - $this->requireContentStreamToExist($command->sourceContentStreamId, $commandHandlingDependencies); - $this->requireContentStreamToNotBeClosed($command->sourceContentStreamId, $commandHandlingDependencies); - $this->requireContentStreamToNotExistYet($command->newContentStreamId, $commandHandlingDependencies); - - $sourceContentStreamVersion = $commandHandlingDependencies->getContentStreamVersion($command->sourceContentStreamId); - - $streamName = ContentStreamEventStreamName::fromContentStreamId($command->newContentStreamId) - ->getEventStreamName(); - - return new EventsToPublish( - $streamName, - Events::with( - new ContentStreamWasForked( - $command->newContentStreamId, - $command->sourceContentStreamId, - $sourceContentStreamVersion, - ), - ), - // NO_STREAM to ensure the "fork" happens as the first event of the new content stream - ExpectedVersion::NO_STREAM() - ); + return $this->forkContentStream($command->newContentStreamId, $command->sourceContentStreamId, $commandHandlingDependencies); } private function handleRemoveContentStream( RemoveContentStream $command, CommandHandlingDependencies $commandHandlingDependencies ): EventsToPublish { - $this->requireContentStreamToExist($command->contentStreamId, $commandHandlingDependencies); - $expectedVersion = $this->getExpectedVersionOfContentStream($command->contentStreamId, $commandHandlingDependencies); - - $streamName = ContentStreamEventStreamName::fromContentStreamId( - $command->contentStreamId - )->getEventStreamName(); - - return new EventsToPublish( - $streamName, - Events::with( - new ContentStreamWasRemoved( - $command->contentStreamId, - ), - ), - $expectedVersion - ); - } - - /** - * @param ContentStreamId $contentStreamId - * @param CommandHandlingDependencies $commandHandlingDependencies - * @throws ContentStreamAlreadyExists - */ - protected function requireContentStreamToNotExistYet( - ContentStreamId $contentStreamId, - CommandHandlingDependencies $commandHandlingDependencies - ): void { - if ($commandHandlingDependencies->contentStreamExists($contentStreamId)) { - throw new ContentStreamAlreadyExists( - 'Content stream "' . $contentStreamId->value . '" already exists.', - 1521386345 - ); - } - } - - /** - * @param ContentStreamId $contentStreamId - * @param CommandHandlingDependencies $commandHandlingDependencies - * @throws ContentStreamDoesNotExistYet - */ - protected function requireContentStreamToExist( - ContentStreamId $contentStreamId, - CommandHandlingDependencies $commandHandlingDependencies - ): void { - if (!$commandHandlingDependencies->contentStreamExists($contentStreamId)) { - throw new ContentStreamDoesNotExistYet( - 'Content stream "' . $contentStreamId->value . '" does not exist yet.', - 1521386692 - ); - } - } - - protected function requireContentStreamToNotBeClosed( - ContentStreamId $contentStreamId, - CommandHandlingDependencies $commandHandlingDependencies - ): void { - if ($commandHandlingDependencies->getContentStreamStatus($contentStreamId) === ContentStreamStatus::CLOSED) { - throw new ContentStreamIsClosed( - 'Content stream "' . $contentStreamId->value . '" is closed.', - 1710260081 - ); - } - } - - protected function requireContentStreamToBeClosed( - ContentStreamId $contentStreamId, - CommandHandlingDependencies $commandHandlingDependencies - ): void { - if ($commandHandlingDependencies->getContentStreamStatus($contentStreamId) !== ContentStreamStatus::CLOSED) { - throw new ContentStreamIsNotClosed( - 'Content stream "' . $contentStreamId->value . '" is not closed.', - 1710405911 - ); - } - } - - protected function getExpectedVersionOfContentStream( - ContentStreamId $contentStreamId, - CommandHandlingDependencies $commandHandlingDependencies - ): ExpectedVersion { - $version = $commandHandlingDependencies->getContentStreamVersion($contentStreamId); - return ExpectedVersion::fromVersion($version); + return $this->removeContentStream($command->contentStreamId, $commandHandlingDependencies); } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/ContentStreamCreation/Command/CreateContentStream.php b/Neos.ContentRepository.Core/Classes/Feature/ContentStreamCreation/Command/CreateContentStream.php deleted file mode 100644 index 8644048817e..00000000000 --- a/Neos.ContentRepository.Core/Classes/Feature/ContentStreamCreation/Command/CreateContentStream.php +++ /dev/null @@ -1,45 +0,0 @@ -readPayloadTable($payloadTable); - $command = ForkContentStream::create( - ContentStreamId::fromString($commandArguments['contentStreamId']), - ContentStreamId::fromString($commandArguments['sourceContentStreamId']), - ); - - $this->currentContentRepository->handle($command); - } - - /** - * @Given /^the command ForkContentStream is executed with payload and exceptions are caught:$/ - * @param TableNode $payloadTable - * @throws \Exception - */ - public function theCommandForkContentStreamIsExecutedWithPayloadAndExceptionsAreCaught(TableNode $payloadTable): void - { - try { - $this->theCommandForkContentStreamIsExecutedWithPayload($payloadTable); - } catch (\Exception $exception) { - $this->lastCommandException = $exception; - } - } -} diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/WorkspaceCreation.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/WorkspaceCreation.php index b876e98a7b4..91122fe5dba 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/WorkspaceCreation.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/Features/WorkspaceCreation.php @@ -21,6 +21,7 @@ use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateWorkspace; use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Command\RebaseWorkspace; use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Dto\RebaseErrorHandlingStrategy; +use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Exception\WorkspaceRebaseFailed; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\CRTestSuiteRuntimeVariables; @@ -37,22 +38,6 @@ abstract protected function readPayloadTable(TableNode $payloadTable): array; abstract protected function publishEvent(string $eventType, StreamName $streamName, array $eventPayload): void; - /** - * @When /^the command CreateContentStream is executed with payload:$/ - * @param TableNode $payloadTable - * @throws \Exception - */ - public function theCommandCreateContentStreamIsExecutedWithPayload(TableNode $payloadTable) - { - $commandArguments = $this->readPayloadTable($payloadTable); - - $command = CreateContentStream::create( - ContentStreamId::fromString($commandArguments['contentStreamId']), - ); - - $this->currentContentRepository->handle($command); - } - /** * @When /^the command CreateRootWorkspace is executed with payload:$/ * @param TableNode $payloadTable @@ -100,6 +85,19 @@ public function theCommandCreateWorkspaceIsExecutedWithPayload(TableNode $payloa $this->currentContentRepository->handle($command); } + /** + * @When /^the command CreateWorkspace is executed with payload and exceptions are caught:$/ + * @param TableNode $payloadTable + * @throws \Exception + */ + public function theCommandCreateWorkspaceIsExecutedWithPayloadAndExceptionsAreCaught(TableNode $payloadTable) + { + try { + $this->theCommandCreateWorkspaceIsExecutedWithPayload($payloadTable); + } catch (\Exception $e) { + $this->lastCommandException = $e; + } + } /** * @When /^the command RebaseWorkspace is executed with payload:$/ From fc52166dd0e8d86bd9ef855d3403707efd826b19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20M=C3=BCller?= Date: Wed, 23 Oct 2024 14:08:24 +0200 Subject: [PATCH 06/10] Update Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ContentStreamForking/01-ForkContentStream_ConstraintChecks.feature Co-authored-by: Bastian Waidelich --- .../01-ForkContentStream_ConstraintChecks.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ContentStreamForking/01-ForkContentStream_ConstraintChecks.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ContentStreamForking/01-ForkContentStream_ConstraintChecks.feature index b27aa459a09..5b3a1fd7723 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ContentStreamForking/01-ForkContentStream_ConstraintChecks.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/ContentStreamForking/01-ForkContentStream_ConstraintChecks.feature @@ -49,7 +49,7 @@ Feature: ForkContentStream Without Dimensions | propertyValues | {"text": {"value": "original value", "type": "string"}} | | propertiesToUnset | {} | - Scenario: Try to fork a content stream (use it as base workspace) that is closed: + Scenario: Try to create a workspace with the base workspace referring to a closed content stream When the command CloseContentStream is executed with payload: | Key | Value | | contentStreamId | "cs-identifier" | From 858962a90ab37223b4804a3a02b963f28e18d049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Mu=CC=88ller?= Date: Wed, 23 Oct 2024 16:07:20 +0200 Subject: [PATCH 07/10] Cleanup pass --- ...erformanceMeasurementCommandController.php | 14 ---- .../Command/PerformanceMeasurementService.php | 1 - .../Classes/ContentRepository.php | 70 ++++++++++--------- .../Classes/Feature/ContentStreamHandling.php | 4 +- 4 files changed, 37 insertions(+), 52 deletions(-) diff --git a/Neos.ContentRepository.BehavioralTests/Classes/Command/PerformanceMeasurementCommandController.php b/Neos.ContentRepository.BehavioralTests/Classes/Command/PerformanceMeasurementCommandController.php index a34583a39e7..be846202a64 100644 --- a/Neos.ContentRepository.BehavioralTests/Classes/Command/PerformanceMeasurementCommandController.php +++ b/Neos.ContentRepository.BehavioralTests/Classes/Command/PerformanceMeasurementCommandController.php @@ -50,18 +50,4 @@ public function preparePerformanceTestCommand(int $nodesPerLevel, int $levels): fn() => $this->performanceMeasurementService->createNodesForPerformanceTest($nodesPerLevel, $levels) ); } - - /** - * Test the performance of forking a content stream and measure the time taken. - * - * @internal - */ - public function testPerformanceCommand(): void - { - $time = microtime(true); - $this->performanceMeasurementService->forkContentStream(); - - $timeElapsed = microtime(true) - $time; - $this->outputLine('Time: ' . $timeElapsed); - } } diff --git a/Neos.ContentRepository.BehavioralTests/Classes/Command/PerformanceMeasurementService.php b/Neos.ContentRepository.BehavioralTests/Classes/Command/PerformanceMeasurementService.php index 2688faa7a66..13a263febe1 100644 --- a/Neos.ContentRepository.BehavioralTests/Classes/Command/PerformanceMeasurementService.php +++ b/Neos.ContentRepository.BehavioralTests/Classes/Command/PerformanceMeasurementService.php @@ -107,7 +107,6 @@ public function createNodesForPerformanceTest(int $nodesPerLevel, int $levels): ExpectedVersion::ANY() )); echo $sumSoFar; - #$this->outputLine(microtime(true) - $time . ' elapsed'); } diff --git a/Neos.ContentRepository.Core/Classes/ContentRepository.php b/Neos.ContentRepository.Core/Classes/ContentRepository.php index 33ad60e147b..71c2eb5158c 100644 --- a/Neos.ContentRepository.Core/Classes/ContentRepository.php +++ b/Neos.ContentRepository.Core/Classes/ContentRepository.php @@ -105,55 +105,26 @@ public function handle(CommandInterface $command): void // publishing themselves $eventsToPublishOrGenerator = $this->commandBus->handle($command, $this->commandHandlingDependencies); - // TODO only apply metadata if is copied event?? In generator below we ignore this... - if ($eventsToPublishOrGenerator instanceof EventsToPublish) { - // TODO meaningful exception message - $initiatingUserId = $this->userIdProvider->getUserId(); - $initiatingTimestamp = $this->clock->now()->format(\DateTimeInterface::ATOM); - - // Add "initiatingUserId" and "initiatingTimestamp" metadata to all events. - // This is done in order to keep information about the _original_ metadata when an - // event is re-applied during publishing/rebasing - // "initiatingUserId": The identifier of the user that originally triggered this event. This will never - // be overridden if it is set once. - // "initiatingTimestamp": The timestamp of the original event. The "recordedAt" timestamp will always be - // re-created and reflects the time an event was actually persisted in a stream, - // the "initiatingTimestamp" will be kept and is never overridden again. - // TODO: cleanup - $eventsToPublish = new EventsToPublish( - $eventsToPublishOrGenerator->streamName, - Events::fromArray( - $eventsToPublishOrGenerator->events->map(function (EventInterface|DecoratedEvent $event) use ( - $initiatingUserId, - $initiatingTimestamp - ) { - $metadata = $event instanceof DecoratedEvent ? $event->eventMetadata?->value ?? [] : []; - $metadata['initiatingUserId'] ??= $initiatingUserId; - $metadata['initiatingTimestamp'] ??= $initiatingTimestamp; - return DecoratedEvent::create($event, metadata: EventMetadata::fromArray($metadata)); - }) - ), - $eventsToPublishOrGenerator->expectedVersion, - ); + $eventsToPublish = $this->enrich($eventsToPublishOrGenerator); $this->eventPersister->publishEvents($this, $eventsToPublish); } else { foreach ($eventsToPublishOrGenerator as $eventsToPublish) { assert($eventsToPublish instanceof EventsToPublish); // just for the ide - + $eventsToPublish = $this->enrich($eventsToPublish); try { $this->eventPersister->publishEvents($this, $eventsToPublish); - } catch (ConcurrencyException $e) { + } catch (ConcurrencyException $concurrencyException) { $errorStrategy = $eventsToPublishOrGenerator->send(new EventsToPublishFailed( $eventsToPublish->expectedVersion, - $e + $concurrencyException )); if ($errorStrategy instanceof EventsToPublish) { $this->eventPersister->publishEvents($this, $errorStrategy); } // if we dont already throw an error throw an error now???? todo - throw $e; + throw $concurrencyException; } } } @@ -329,4 +300,35 @@ public function getContentDimensionSource(): ContentDimensionSourceInterface { return $this->contentDimensionSource; } + + private function enrich(EventsToPublish $eventsToPublish): EventsToPublish + { + $initiatingUserId = $this->userIdProvider->getUserId(); + $initiatingTimestamp = $this->clock->now()->format(\DateTimeInterface::ATOM); + + // Add "initiatingUserId" and "initiatingTimestamp" metadata to all events. + // This is done in order to keep information about the _original_ metadata when an + // event is re-applied during publishing/rebasing + // "initiatingUserId": The identifier of the user that originally triggered this event. This will never + // be overridden if it is set once. + // "initiatingTimestamp": The timestamp of the original event. The "recordedAt" timestamp will always be + // re-created and reflects the time an event was actually persisted in a stream, + // the "initiatingTimestamp" will be kept and is never overridden again. + return new EventsToPublish( + $eventsToPublish->streamName, + Events::fromArray( + $eventsToPublish->events->map(function (EventInterface|DecoratedEvent $event) use ( + $initiatingUserId, + $initiatingTimestamp + ) { + $metadata = $event instanceof DecoratedEvent ? $event->eventMetadata?->value ?? [] : []; + $metadata['initiatingUserId'] ??= $initiatingUserId; + $metadata['initiatingTimestamp'] ??= $initiatingTimestamp; + + return DecoratedEvent::create($event, metadata: EventMetadata::fromArray($metadata)); + }) + ), + $eventsToPublish->expectedVersion, + ); + } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/ContentStreamHandling.php b/Neos.ContentRepository.Core/Classes/Feature/ContentStreamHandling.php index 76323bde6b9..5c2697ec9f3 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/ContentStreamHandling.php +++ b/Neos.ContentRepository.Core/Classes/Feature/ContentStreamHandling.php @@ -80,8 +80,6 @@ private function reopenContentStream( CommandHandlingDependencies $commandHandlingDependencies, ): EventsToPublish { $this->requireContentStreamToExist($contentStreamId, $commandHandlingDependencies); - // todo dont we want to make sure this always succeeds? e.g. ANY? - $expectedVersion = $this->getExpectedVersionOfContentStream($contentStreamId, $commandHandlingDependencies); $this->requireContentStreamToBeClosed($contentStreamId, $commandHandlingDependencies); $streamName = ContentStreamEventStreamName::fromContentStreamId($contentStreamId)->getEventStreamName(); @@ -93,7 +91,7 @@ private function reopenContentStream( $previousState, ), ), - $expectedVersion + ExpectedVersion::ANY() ); } From 63db14339f614a243e24700804d83a3f778b45fe Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 23 Oct 2024 23:10:46 +0200 Subject: [PATCH 08/10] TASK: Adjust cr enrichEventsToPublishWithMetadata --- .../Classes/ContentRepository.php | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/ContentRepository.php b/Neos.ContentRepository.Core/Classes/ContentRepository.php index 71c2eb5158c..497370291c9 100644 --- a/Neos.ContentRepository.Core/Classes/ContentRepository.php +++ b/Neos.ContentRepository.Core/Classes/ContentRepository.php @@ -106,12 +106,12 @@ public function handle(CommandInterface $command): void $eventsToPublishOrGenerator = $this->commandBus->handle($command, $this->commandHandlingDependencies); if ($eventsToPublishOrGenerator instanceof EventsToPublish) { - $eventsToPublish = $this->enrich($eventsToPublishOrGenerator); + $eventsToPublish = $this->enrichEventsToPublishWithMetadata($eventsToPublishOrGenerator); $this->eventPersister->publishEvents($this, $eventsToPublish); } else { foreach ($eventsToPublishOrGenerator as $eventsToPublish) { assert($eventsToPublish instanceof EventsToPublish); // just for the ide - $eventsToPublish = $this->enrich($eventsToPublish); + $eventsToPublish = $this->enrichEventsToPublishWithMetadata($eventsToPublish); try { $this->eventPersister->publishEvents($this, $eventsToPublish); } catch (ConcurrencyException $concurrencyException) { @@ -121,7 +121,7 @@ public function handle(CommandInterface $command): void )); if ($errorStrategy instanceof EventsToPublish) { - $this->eventPersister->publishEvents($this, $errorStrategy); + $this->eventPersister->publishEvents($this, $this->enrichEventsToPublishWithMetadata($errorStrategy)); } // if we dont already throw an error throw an error now???? todo throw $concurrencyException; @@ -301,19 +301,21 @@ public function getContentDimensionSource(): ContentDimensionSourceInterface return $this->contentDimensionSource; } - private function enrich(EventsToPublish $eventsToPublish): EventsToPublish + /** + * Add "initiatingUserId" and "initiatingTimestamp" metadata to all events. + * This is done in order to keep information about the _original_ metadata when an + * event is re-applied during publishing/rebasing + * "initiatingUserId": The identifier of the user that originally triggered this event. This will never + * be overridden if it is set once. + * "initiatingTimestamp": The timestamp of the original event. The "recordedAt" timestamp will always be + * re-created and reflects the time an event was actually persisted in a stream, + * the "initiatingTimestamp" will be kept and is never overridden again. + */ + private function enrichEventsToPublishWithMetadata(EventsToPublish $eventsToPublish): EventsToPublish { $initiatingUserId = $this->userIdProvider->getUserId(); $initiatingTimestamp = $this->clock->now()->format(\DateTimeInterface::ATOM); - // Add "initiatingUserId" and "initiatingTimestamp" metadata to all events. - // This is done in order to keep information about the _original_ metadata when an - // event is re-applied during publishing/rebasing - // "initiatingUserId": The identifier of the user that originally triggered this event. This will never - // be overridden if it is set once. - // "initiatingTimestamp": The timestamp of the original event. The "recordedAt" timestamp will always be - // re-created and reflects the time an event was actually persisted in a stream, - // the "initiatingTimestamp" will be kept and is never overridden again. return new EventsToPublish( $eventsToPublish->streamName, Events::fromArray( From f40b63312c1410cbaf3daf35f9c3fe23389f33ef Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 23 Oct 2024 22:03:07 +0200 Subject: [PATCH 09/10] TASK: Remove `EventsToPublishFailed` and rethrow exception via ->throw --- .../Classes/CommandHandler/CommandBus.php | 2 +- .../CommandHandlerInterface.php | 3 +-- .../Classes/ContentRepository.php | 22 +++++++++++-------- .../EventStore/EventsToPublishFailed.php | 21 ------------------ .../Feature/WorkspaceCommandHandler.php | 8 ++++--- 5 files changed, 20 insertions(+), 36 deletions(-) delete mode 100644 Neos.ContentRepository.Core/Classes/EventStore/EventsToPublishFailed.php diff --git a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php index b5bd49569a2..3d959ebcd51 100644 --- a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php +++ b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandBus.php @@ -28,7 +28,7 @@ public function __construct(CommandHandlerInterface ...$handlers) } /** - * @return EventsToPublish|\Generator + * @return EventsToPublish|\Generator */ public function handle(CommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish|\Generator { diff --git a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php index 42381f463f8..9a82c678b15 100644 --- a/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php +++ b/Neos.ContentRepository.Core/Classes/CommandHandler/CommandHandlerInterface.php @@ -6,7 +6,6 @@ use Neos\ContentRepository\Core\CommandHandlingDependencies; use Neos\ContentRepository\Core\EventStore\EventsToPublish; -use Neos\ContentRepository\Core\EventStore\EventsToPublishFailed; /** * Common interface for all Content Repository command handlers @@ -21,7 +20,7 @@ interface CommandHandlerInterface public function canHandle(CommandInterface $command): bool; /** - * @return EventsToPublish|\Generator + * @return EventsToPublish|\Generator */ public function handle(CommandInterface $command, CommandHandlingDependencies $commandHandlingDependencies): EventsToPublish|\Generator; } diff --git a/Neos.ContentRepository.Core/Classes/ContentRepository.php b/Neos.ContentRepository.Core/Classes/ContentRepository.php index 497370291c9..0db25916435 100644 --- a/Neos.ContentRepository.Core/Classes/ContentRepository.php +++ b/Neos.ContentRepository.Core/Classes/ContentRepository.php @@ -24,7 +24,6 @@ use Neos\ContentRepository\Core\EventStore\EventPersister; use Neos\ContentRepository\Core\EventStore\Events; use Neos\ContentRepository\Core\EventStore\EventsToPublish; -use Neos\ContentRepository\Core\EventStore\EventsToPublishFailed; use Neos\ContentRepository\Core\Factory\ContentRepositoryFactory; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\Projection\CatchUp; @@ -114,17 +113,22 @@ public function handle(CommandInterface $command): void $eventsToPublish = $this->enrichEventsToPublishWithMetadata($eventsToPublish); try { $this->eventPersister->publishEvents($this, $eventsToPublish); - } catch (ConcurrencyException $concurrencyException) { - $errorStrategy = $eventsToPublishOrGenerator->send(new EventsToPublishFailed( - $eventsToPublish->expectedVersion, - $concurrencyException - )); + } catch (ConcurrencyException $e) { + // we pass the exception into the generator, so it could be try-caught and reacted upon: + // + // try { + // yield EventsToPublish(); + // } catch (ConcurrencyException $e) { + // yield $restoreState(); + // throw $e; + // } + + $errorStrategy = $eventsToPublishOrGenerator->throw($e); if ($errorStrategy instanceof EventsToPublish) { - $this->eventPersister->publishEvents($this, $this->enrichEventsToPublishWithMetadata($errorStrategy)); + $eventsToPublish = $this->enrichEventsToPublishWithMetadata($errorStrategy); + $this->eventPersister->publishEvents($this, $this->enrichEventsToPublishWithMetadata($eventsToPublish)); } - // if we dont already throw an error throw an error now???? todo - throw $concurrencyException; } } } diff --git a/Neos.ContentRepository.Core/Classes/EventStore/EventsToPublishFailed.php b/Neos.ContentRepository.Core/Classes/EventStore/EventsToPublishFailed.php deleted file mode 100644 index 7f5ef383991..00000000000 --- a/Neos.ContentRepository.Core/Classes/EventStore/EventsToPublishFailed.php +++ /dev/null @@ -1,21 +0,0 @@ -requireWorkspace($command->workspaceName, $commandHandlingDependencies); $baseWorkspace = $this->requireBaseWorkspace($workspace, $commandHandlingDependencies); - $publishResult = yield $this->getCopiedEventsToPublishForContentStream( + $publishContentStream = $this->getCopiedEventsToPublishForContentStream( $workspace->currentContentStreamId, $baseWorkspace->workspaceName, $baseWorkspace->currentContentStreamId ); - if ($publishResult instanceof EventsToPublishFailed) { + try { + yield $publishContentStream; + } catch (ConcurrencyException $exception) { throw new BaseWorkspaceHasBeenModifiedInTheMeantime(sprintf( 'The base workspace has been modified in the meantime; please rebase.' . ' Expected version %d of source content stream %s', - $publishResult->expectedVersion->value, + $publishContentStream->expectedVersion->value, $baseWorkspace->currentContentStreamId )); } From bf9c3b7c9c1f8e963a876e0149940a2e2de2977a Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Thu, 24 Oct 2024 18:17:17 +0200 Subject: [PATCH 10/10] TASK: Revert PerformanceMeasurement adjustments we will open a dedicated pr to get rid of the forkCommandUseHere --- .../PerformanceMeasurementCommandController.php | 14 ++++++++++++++ .../Command/PerformanceMeasurementService.php | 11 ++++++----- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/Neos.ContentRepository.BehavioralTests/Classes/Command/PerformanceMeasurementCommandController.php b/Neos.ContentRepository.BehavioralTests/Classes/Command/PerformanceMeasurementCommandController.php index be846202a64..a34583a39e7 100644 --- a/Neos.ContentRepository.BehavioralTests/Classes/Command/PerformanceMeasurementCommandController.php +++ b/Neos.ContentRepository.BehavioralTests/Classes/Command/PerformanceMeasurementCommandController.php @@ -50,4 +50,18 @@ public function preparePerformanceTestCommand(int $nodesPerLevel, int $levels): fn() => $this->performanceMeasurementService->createNodesForPerformanceTest($nodesPerLevel, $levels) ); } + + /** + * Test the performance of forking a content stream and measure the time taken. + * + * @internal + */ + public function testPerformanceCommand(): void + { + $time = microtime(true); + $this->performanceMeasurementService->forkContentStream(); + + $timeElapsed = microtime(true) - $time; + $this->outputLine('Time: ' . $timeElapsed); + } } diff --git a/Neos.ContentRepository.BehavioralTests/Classes/Command/PerformanceMeasurementService.php b/Neos.ContentRepository.BehavioralTests/Classes/Command/PerformanceMeasurementService.php index 13a263febe1..cc67508bade 100644 --- a/Neos.ContentRepository.BehavioralTests/Classes/Command/PerformanceMeasurementService.php +++ b/Neos.ContentRepository.BehavioralTests/Classes/Command/PerformanceMeasurementService.php @@ -26,6 +26,7 @@ use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; use Neos\ContentRepository\Core\Feature\Common\InterdimensionalSiblings; use Neos\ContentRepository\Core\Feature\ContentStreamEventStreamName; +use Neos\ContentRepository\Core\Feature\ContentStreamForking\Command\ForkContentStream; use Neos\ContentRepository\Core\Feature\NodeCreation\Event\NodeAggregateWithNodeWasCreated; use Neos\ContentRepository\Core\Feature\NodeModification\Dto\SerializedPropertyValues; use Neos\ContentRepository\Core\Feature\RootNodeCreation\Event\RootNodeAggregateWithNodeWasCreated; @@ -107,6 +108,7 @@ public function createNodesForPerformanceTest(int $nodesPerLevel, int $levels): ExpectedVersion::ANY() )); echo $sumSoFar; + #$this->outputLine(microtime(true) - $time . ' elapsed'); } @@ -152,10 +154,9 @@ private function createHierarchy( public function forkContentStream(): void { - throw new \BadMethodCallException('not implemented'); - // $this->contentRepository->handle(ForkContentStream::create( - // ContentStreamId::create(), - // $this->contentStreamId, - // )); + $this->contentRepository->handle(ForkContentStream::create( + ContentStreamId::create(), + $this->contentStreamId, + )); } }