diff --git a/Neos.ContentRepository.Core/Classes/EventStore/EventNormalizer.php b/Neos.ContentRepository.Core/Classes/EventStore/EventNormalizer.php index 020e0f23b80..38528096279 100644 --- a/Neos.ContentRepository.Core/Classes/EventStore/EventNormalizer.php +++ b/Neos.ContentRepository.Core/Classes/EventStore/EventNormalizer.php @@ -157,6 +157,7 @@ public function denormalize(Event $event): EventInterface ); } assert(is_array($eventDataAsArray)); + /** {@see EventInterface::fromArray()} */ return $eventClassName::fromArray($eventDataAsArray); } } diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/ContentRepository/ContentRepositoryStatus.php b/Neos.ContentRepository.Core/Classes/SharedModel/ContentRepository/ContentRepositoryStatus.php index 10b5a0f2d03..c7de2e5bb28 100644 --- a/Neos.ContentRepository.Core/Classes/SharedModel/ContentRepository/ContentRepositoryStatus.php +++ b/Neos.ContentRepository.Core/Classes/SharedModel/ContentRepository/ContentRepositoryStatus.php @@ -15,7 +15,9 @@ namespace Neos\ContentRepository\Core\SharedModel\ContentRepository; use Neos\ContentRepository\Core\Projection\ProjectionStatuses; +use Neos\ContentRepository\Core\Projection\ProjectionStatusType; use Neos\EventStore\Model\EventStore\Status as EventStoreStatus; +use Neos\EventStore\Model\EventStore\StatusType as EventStoreStatusType; /** * @api @@ -27,4 +29,17 @@ public function __construct( public ProjectionStatuses $projectionStatuses, ) { } + + public function isOk(): bool + { + if ($this->eventStoreStatus->type !== EventStoreStatusType::OK) { + return false; + } + foreach ($this->projectionStatuses as $projectionStatus) { + if ($projectionStatus->type !== ProjectionStatusType::OK) { + return false; + } + } + return true; + } } diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/CrCommandController.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/CrCommandController.php index 6f6fe55ff91..52ee49ebd52 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/CrCommandController.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/CrCommandController.php @@ -122,10 +122,9 @@ public function migrateLegacyDataCommand(bool $verbose = false, string $config = $this->quit(); } $this->connection->executeStatement('TRUNCATE ' . $connection->quoteIdentifier($eventTableName)); - // we also need to reset the projections; in order to ensure the system runs deterministically. We - // do this by replaying the just-truncated event stream. + // we also need to reset the projections; in order to ensure the system runs deterministically $projectionService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->projectionReplayServiceFactory); - $projectionService->replayAllProjections(CatchUpOptions::create()); + $projectionService->resetAllProjections(); $this->outputLine('Truncated events'); $liveContentStreamId = ContentStreamId::create(); diff --git a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php index e4672f9885d..c82eebe9106 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php +++ b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php @@ -5,7 +5,6 @@ use Neos\ContentRepository\Core\Factory\ContentRepositoryId; use Neos\ContentRepository\Core\Projection\CatchUpOptions; -use Neos\ContentRepository\Core\Projection\ProjectionStatus; use Neos\ContentRepository\Core\Projection\ProjectionStatusType; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\ContentRepositoryRegistry\Service\ProjectionReplayServiceFactory; @@ -33,11 +32,27 @@ public function __construct( * Note: This command is non-destructive, i.e. it can be executed without side effects even if all dependencies are up-to-date * Therefore it makes sense to include this command into the Continuous Integration * + * To check if the content repository needs to be setup look into cr:status. + * That command will also display information what is about to be migrated. + * * @param string $contentRepository Identifier of the Content Repository to set up + * @param bool $resetProjections Advanced. Can be used in rare cases when the projections cannot be migrated to reset everything in advance. This requires a full replay afterwards. */ - public function setupCommand(string $contentRepository = 'default'): void + public function setupCommand(string $contentRepository = 'default', bool $resetProjections = false): void { $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); + + if ($resetProjections) { + if (!$this->output->askConfirmation(sprintf('> Advanced Mode. The flag --reset-projections will reset all projections in "%s", which leaves you with empty projections to be replayed. Are you sure to proceed? (y/n) ', $contentRepositoryId->value), false)) { + $this->outputLine('Abort.'); + return; + } + + $projectionService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->projectionServiceFactory); + $projectionService->resetAllProjections(); + $this->outputLine('All projections of Content Repository "%s" were resettet.', [$contentRepositoryId->value]); + } + $this->contentRepositoryRegistry->get($contentRepositoryId)->setUp(); $this->outputLine('Content Repository "%s" was set up', [$contentRepositoryId->value]); } @@ -45,6 +60,8 @@ public function setupCommand(string $contentRepository = 'default'): void /** * Determine and output the status of the event store and all registered projections for a given Content Repository * + * In verbose mode it will also display information what should and will be migrated when cr:setup is used. + * * @param string $contentRepository Identifier of the Content Repository to determine the status for * @param bool $verbose If set, more details will be shown * @param bool $quiet If set, no output is generated. This is useful if only the exit code (0 = all OK, 1 = errors or warnings) is of interest @@ -57,8 +74,6 @@ public function statusCommand(string $contentRepository = 'default', bool $verbo $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); $status = $this->contentRepositoryRegistry->get($contentRepositoryId)->status(); - $hasErrorsOrWarnings = false; - $this->output('Event Store: '); $this->outputLine(match ($status->eventStoreStatus->type) { StatusType::OK => 'OK', @@ -68,9 +83,6 @@ public function statusCommand(string $contentRepository = 'default', bool $verbo if ($verbose && $status->eventStoreStatus->details !== '') { $this->outputFormatted($status->eventStoreStatus->details, [], 2); } - if ($status->eventStoreStatus->type !== StatusType::OK) { - $hasErrorsOrWarnings = true; - } $this->outputLine(); foreach ($status->projectionStatuses as $projectionName => $projectionStatus) { $this->output('Projection "%s": ', [$projectionName]); @@ -80,9 +92,6 @@ public function statusCommand(string $contentRepository = 'default', bool $verbo ProjectionStatusType::REPLAY_REQUIRED => 'Replay required!', ProjectionStatusType::ERROR => 'ERROR', }); - if ($projectionStatus->type !== ProjectionStatusType::OK) { - $hasErrorsOrWarnings = true; - } if ($verbose && ($projectionStatus->type !== ProjectionStatusType::OK || $projectionStatus->details)) { $lines = explode(chr(10), $projectionStatus->details ?: 'No details available.'); foreach ($lines as $line) { @@ -91,21 +100,32 @@ public function statusCommand(string $contentRepository = 'default', bool $verbo $this->outputLine(); } } - if ($hasErrorsOrWarnings) { + if (!$status->isOk()) { $this->quit(1); } } /** - * Replays the specified projection of a Content Repository by resetting its state and performing a full catchup + * Replays the specified projection of a Content Repository by resetting its state and performing a full catchup. * * @param string $projection Full Qualified Class Name or alias of the projection to replay (e.g. "contentStream") * @param string $contentRepository Identifier of the Content Repository instance to operate on - * @param bool $quiet If set only fatal errors are rendered to the output + * @param bool $force Replay the projection without confirmation. This may take some time! + * @param bool $quiet If set only fatal errors are rendered to the output (must be used with --force flag to avoid user input) * @param int $until Until which sequence number should projections be replayed? useful for debugging */ - public function replayCommand(string $projection, string $contentRepository = 'default', bool $quiet = false, int $until = 0): void + public function projectionReplayCommand(string $projection, string $contentRepository = 'default', bool $force = false, bool $quiet = false, int $until = 0): void { + if (!$force && $quiet) { + $this->outputLine('Cannot run in quiet mode without --force. Please acknowledge that this command will reset and replay this projection. This may take some time.'); + $this->quit(1); + } + + if (!$force && !$this->output->askConfirmation(sprintf('> This will replay the projection "%s" in "%s", which may take some time. Are you sure to proceed? (y/n) ', $projection, $contentRepository), false)) { + $this->outputLine('Abort.'); + return; + } + $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); $projectionService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->projectionServiceFactory); @@ -131,19 +151,29 @@ public function replayCommand(string $projection, string $contentRepository = 'd * Replays all projections of the specified Content Repository by resetting their states and performing a full catchup * * @param string $contentRepository Identifier of the Content Repository instance to operate on - * @param bool $quiet If set only fatal errors are rendered to the output + * @param bool $force Replay the projection without confirmation. This may take some time! + * @param bool $quiet If set only fatal errors are rendered to the output (must be used with --force flag to avoid user input) */ - public function replayAllCommand(string $contentRepository = 'default', bool $quiet = false): void + public function projectionReplayAllCommand(string $contentRepository = 'default', bool $force = false, bool $quiet = false): void { + if (!$force && $quiet) { + $this->outputLine('Cannot run in quiet mode without --force. Please acknowledge that this command will reset and replay this projection. This may take some time.'); + $this->quit(1); + } + + if (!$force && !$this->output->askConfirmation(sprintf('> This will replay all projections in "%s", which may take some time. Are you sure to proceed? (y/n) ', $contentRepository), false)) { + $this->outputLine('Abort.'); + return; + } + $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); $projectionService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->projectionServiceFactory); if (!$quiet) { $this->outputLine('Replaying events for all projections of Content Repository "%s" ...', [$contentRepositoryId->value]); - // TODO start progress bar } - $projectionService->replayAllProjections(CatchUpOptions::create()); + // TODO progress bar with all events? Like projectionReplayCommand? + $projectionService->replayAllProjections(CatchUpOptions::create(), fn (string $projectionAlias) => $this->outputLine(sprintf(' * replaying %s projection', $projectionAlias))); if (!$quiet) { - // TODO finish progress bar $this->outputLine('Done.'); } } @@ -151,12 +181,14 @@ public function replayAllCommand(string $contentRepository = 'default', bool $qu /** * This will completely prune the data of the specified content repository. * - * @param string $contentRepository name of the content repository where the data should be pruned from. + * @param string $contentRepository Name of the content repository where the data should be pruned from. + * @param bool $force Prune the cr without confirmation. This cannot be reverted! * @return void */ - public function pruneCommand(string $contentRepository = 'default'): void + public function pruneCommand(string $contentRepository = 'default', bool $force = false): void { - if (!$this->output->askConfirmation(sprintf("This will prune your content repository \"%s\". Are you sure to proceed? (y/n) ", $contentRepository), false)) { + if (!$force && !$this->output->askConfirmation(sprintf('> This will prune your content repository "%s". Are you sure to proceed? (y/n) ', $contentRepository), false)) { + $this->outputLine('Abort.'); return; } @@ -166,14 +198,23 @@ public function pruneCommand(string $contentRepository = 'default'): void $contentRepositoryId, new ContentStreamPrunerFactory() ); - $contentStreamPruner->pruneAll(); $workspaceMaintenanceService = $this->contentRepositoryRegistry->buildService( $contentRepositoryId, new WorkspaceMaintenanceServiceFactory() ); + + $projectionService = $this->contentRepositoryRegistry->buildService( + $contentRepositoryId, + $this->projectionServiceFactory + ); + + // reset the events table + $contentStreamPruner->pruneAll(); $workspaceMaintenanceService->pruneAll(); + // reset the projections state + $projectionService->resetAllProjections(); - $this->replayAllCommand($contentRepository); + $this->outputLine('Done.'); } } diff --git a/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayService.php b/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayService.php index 9dec35da709..2dcf16f96cc 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayService.php +++ b/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayService.php @@ -31,14 +31,24 @@ public function replayProjection(string $projectionAliasOrClassName, CatchUpOpti $this->contentRepository->catchUpProjection($projectionClassName, $options); } - public function replayAllProjections(CatchUpOptions $options): void + public function replayAllProjections(CatchUpOptions $options, ?\Closure $progressCallback = null): void { foreach ($this->projectionClassNamesAndAliases() as $classNamesAndAlias) { + if ($progressCallback) { + $progressCallback($classNamesAndAlias['alias']); + } $this->contentRepository->resetProjectionState($classNamesAndAlias['className']); $this->contentRepository->catchUpProjection($classNamesAndAlias['className'], $options); } } + public function resetAllProjections(): void + { + foreach ($this->projectionClassNamesAndAliases() as $classNamesAndAlias) { + $this->contentRepository->resetProjectionState($classNamesAndAlias['className']); + } + } + /** * @return class-string> */ @@ -80,6 +90,4 @@ private static function projectionAlias(string $className): string } return $alias; } - - } diff --git a/Neos.Neos/Classes/Command/CrCommandController.php b/Neos.Neos/Classes/Command/CrCommandController.php index 0ba3ae6dd85..de0afa4aa3f 100644 --- a/Neos.Neos/Classes/Command/CrCommandController.php +++ b/Neos.Neos/Classes/Command/CrCommandController.php @@ -7,19 +7,18 @@ use League\Flysystem\Filesystem; use League\Flysystem\Local\LocalFilesystemAdapter; use Neos\ContentRepository\Core\Factory\ContentRepositoryId; -use Neos\ContentRepository\Core\Service\ContentRepositoryBootstrapper; +use Neos\ContentRepository\Core\Projection\CatchUpOptions; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Export\ExportService; use Neos\ContentRepository\Export\ExportServiceFactory; use Neos\ContentRepository\Export\ImportService; use Neos\ContentRepository\Export\ImportServiceFactory; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; +use Neos\ContentRepositoryRegistry\Service\ProjectionReplayServiceFactory; use Neos\Neos\AssetUsage\Projection\AssetUsageFinder; use Neos\Flow\Annotations as Flow; use Neos\Flow\Cli\CommandController; -use Neos\Flow\Core\Booting\Scripts; use Neos\Media\Domain\Repository\AssetRepository; -use Neos\Neos\Domain\Service\NodeTypeNameFactory; use Neos\Utility\Files; use Neos\Flow\ResourceManagement\ResourceRepository; use Neos\Flow\ResourceManagement\ResourceManager; @@ -39,6 +38,7 @@ public function __construct( private readonly ResourceManager $resourceManager, private readonly PersistenceManagerInterface $persistenceManager, private readonly ContentRepositoryRegistry $contentRepositoryRegistry, + private readonly ProjectionReplayServiceFactory $projectionReplayServiceFactory, ) { parent::__construct(); } @@ -109,7 +109,9 @@ public function importCommand(string $path, string $contentRepository = 'default } $this->outputLine('Replaying projections'); - Scripts::executeCommand('neos.contentrepositoryregistry:cr:replayall', $this->flowSettings, false, ['contentRepository' => $contentRepositoryId->value]); + + $projectionService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->projectionReplayServiceFactory); + $projectionService->replayAllProjections(CatchUpOptions::create()); $this->outputLine('Done'); }