From b1fcbccbb58d9dc6985fba51cdb575ec4b1d89ce Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Wed, 24 Apr 2024 16:05:19 +0200 Subject: [PATCH] FEATURE: Improved CLI output for projection replay Adds debugging infos to the output of the `cr:projectionReplay` command and nested progress bars to the output of `cr:projectionReplayAll` Related: #4777 --- .../Classes/Projection/Projections.php | 7 ++- .../Classes/Command/CrCommandController.php | 55 ++++++++++++++++--- .../Service/ProjectionReplayService.php | 17 ++++++ .../ProjectionReplayServiceFactory.php | 1 + 4 files changed, 71 insertions(+), 9 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/Projection/Projections.php b/Neos.ContentRepository.Core/Classes/Projection/Projections.php index e9910356908..0bc7dffec20 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/Projections.php +++ b/Neos.ContentRepository.Core/Classes/Projection/Projections.php @@ -10,7 +10,7 @@ * @implements \IteratorAggregate * @internal */ -final class Projections implements \IteratorAggregate +final class Projections implements \IteratorAggregate, \Countable { /** * @var array>, ProjectionInterface> @@ -93,4 +93,9 @@ public function getIterator(): \Traversable { yield from $this->projections; } + + public function count(): int + { + return count($this->projections); + } } diff --git a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php index d78d6f92d82..b5d98b29e8e 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php +++ b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php @@ -13,6 +13,8 @@ use Neos\EventStore\Model\Event\SequenceNumber; use Neos\EventStore\Model\EventStore\StatusType; use Neos\Flow\Cli\CommandController; +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\Output; final class CrCommandController extends CommandController @@ -116,6 +118,11 @@ public function statusCommand(string $contentRepository = 'default', bool $verbo */ public function projectionReplayCommand(string $projection, string $contentRepository = 'default', bool $force = false, bool $quiet = false, int $until = 0): void { + if ($quiet) { + $this->output->getOutput()->setVerbosity(Output::VERBOSITY_QUIET); + } + $progressBar = new ProgressBar($this->output->getOutput()); + $progressBar->setFormat(' %current%/%max% [%bar%] %percent:3s%% %elapsed:16s%/%estimated:-16s% %memory:6s%'); 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); @@ -129,19 +136,18 @@ public function projectionReplayCommand(string $projection, string $contentRepos $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); $projectionService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->projectionServiceFactory); + $options = CatchUpOptions::create(); if (!$quiet) { $this->outputLine('Replaying events for projection "%s" of Content Repository "%s" ...', [$projection, $contentRepositoryId->value]); - $this->output->progressStart(); + $progressBar->start(max($until > 0 ? $until : $projectionService->highestSequenceNumber()->value, 1)); + $options->with(progressCallback: fn () => $progressBar->advance()); } - $options = CatchUpOptions::create( - progressCallback: fn () => $this->output->progressAdvance(), - ); if ($until > 0) { $options = $options->with(maximumSequenceNumber: SequenceNumber::fromInteger($until)); } $projectionService->replayProjection($projection, $options); if (!$quiet) { - $this->output->progressFinish(); + $progressBar->finish(); $this->outputLine(); $this->outputLine('Done.'); } @@ -153,9 +159,23 @@ public function projectionReplayCommand(string $projection, string $contentRepos * @param string $contentRepository Identifier of the Content Repository instance to operate on * @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 projectionReplayAllCommand(string $contentRepository = 'default', bool $force = false, bool $quiet = false): void + public function projectionReplayAllCommand(string $contentRepository = 'default', bool $force = false, bool $quiet = false, int $until = 0): void { + if ($quiet) { + $this->output->getOutput()->setVerbosity(Output::VERBOSITY_QUIET); + } + $mainSection = ($this->output->getOutput() instanceof ConsoleOutput) ? $this->output->getOutput()->section() : $this->output->getOutput(); + $mainProgressBar = new ProgressBar($mainSection); + $mainProgressBar->setBarCharacter(''); + $mainProgressBar->setEmptyBarCharacter('░'); + $mainProgressBar->setProgressCharacter(''); + $mainProgressBar->setFormat('debug'); + + $subSection = ($this->output->getOutput() instanceof ConsoleOutput) ? $this->output->getOutput()->section() : $this->output->getOutput(); + $progressBar = new ProgressBar($subSection); + $progressBar->setFormat(' %message% - %current%/%max% [%bar%] %percent:3s%% %elapsed:16s%/%estimated:-16s% %memory:6s%'); 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); @@ -171,9 +191,28 @@ public function projectionReplayAllCommand(string $contentRepository = 'default' if (!$quiet) { $this->outputLine('Replaying events for all projections of Content Repository "%s" ...', [$contentRepositoryId->value]); } - // TODO progress bar with all events? Like projectionReplayCommand? - $projectionService->replayAllProjections(CatchUpOptions::create(), fn (string $projectionAlias) => $this->outputLine(sprintf(' * replaying %s projection', $projectionAlias))); + $options = CatchUpOptions::create(); + if (!$quiet) { + $options = $options->with(progressCallback: fn () => $progressBar->advance()); + } + if ($until > 0) { + $options = $options->with(maximumSequenceNumber: SequenceNumber::fromInteger($until)); + } + $highestSequenceNumber = max($until > 0 ? $until : $projectionService->highestSequenceNumber()->value, 1); + $mainProgressBar->start($projectionService->numberOfProjections()); + $mainProgressCallback = null; + if (!$quiet) { + $mainProgressCallback = static function (string $projectionAlias) use ($mainProgressBar, $progressBar, $highestSequenceNumber) { + $mainProgressBar->advance(); + $progressBar->setMessage($projectionAlias); + $progressBar->start($highestSequenceNumber); + $progressBar->setProgress(0); + }; + } + $projectionService->replayAllProjections($options, $mainProgressCallback); if (!$quiet) { + $mainProgressBar->finish(); + $progressBar->finish(); $this->outputLine('Done.'); } } diff --git a/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayService.php b/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayService.php index 2dcf16f96cc..00a946be01a 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayService.php +++ b/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayService.php @@ -9,6 +9,9 @@ use Neos\ContentRepository\Core\Projection\ProjectionInterface; use Neos\ContentRepository\Core\Projection\Projections; use Neos\ContentRepository\Core\Projection\ProjectionStateInterface; +use Neos\EventStore\EventStoreInterface; +use Neos\EventStore\Model\Event\SequenceNumber; +use Neos\EventStore\Model\EventStream\VirtualStreamName; /** * Content Repository service to perform Projection replays @@ -21,6 +24,7 @@ final class ProjectionReplayService implements ContentRepositoryServiceInterface public function __construct( private readonly Projections $projections, private readonly ContentRepository $contentRepository, + private readonly EventStoreInterface $eventStore, ) { } @@ -49,6 +53,19 @@ public function resetAllProjections(): void } } + public function highestSequenceNumber(): SequenceNumber + { + foreach ($this->eventStore->load(VirtualStreamName::all())->backwards()->limit(1) as $eventEnvelope) { + return $eventEnvelope->sequenceNumber; + } + return SequenceNumber::none(); + } + + public function numberOfProjections(): int + { + return count($this->projections); + } + /** * @return class-string> */ diff --git a/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayServiceFactory.php b/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayServiceFactory.php index 134d34b4f27..337297d9bb6 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayServiceFactory.php +++ b/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayServiceFactory.php @@ -24,6 +24,7 @@ public function build(ContentRepositoryServiceFactoryDependencies $serviceFactor return new ProjectionReplayService( $serviceFactoryDependencies->projections, $serviceFactoryDependencies->contentRepository, + $serviceFactoryDependencies->eventStore, ); } }