Skip to content

Commit

Permalink
Merge pull request neos#4864 from mhsdesign/task/improveEscrCliCommands
Browse files Browse the repository at this point in the history
!!! FEATURE: Improve escr cli commands
  • Loading branch information
mhsdesign authored Feb 15, 2024
2 parents 2d64583 + fc87012 commit d57947a
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ public function denormalize(Event $event): EventInterface
);
}
assert(is_array($eventDataAsArray));
/** {@see EventInterface::fromArray()} */
return $eventClassName::fromArray($eventDataAsArray);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -33,18 +32,36 @@ 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('<comment>Abort.</comment>');
return;
}

$projectionService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->projectionServiceFactory);
$projectionService->resetAllProjections();
$this->outputLine('<success>All projections of Content Repository "%s" were resettet.</success>', [$contentRepositoryId->value]);
}

$this->contentRepositoryRegistry->get($contentRepositoryId)->setUp();
$this->outputLine('<success>Content Repository "%s" was set up</success>', [$contentRepositoryId->value]);
}

/**
* 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
Expand All @@ -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 => '<success>OK</success>',
Expand All @@ -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 "<b>%s</b>": ', [$projectionName]);
Expand All @@ -80,9 +92,6 @@ public function statusCommand(string $contentRepository = 'default', bool $verbo
ProjectionStatusType::REPLAY_REQUIRED => '<comment>Replay required!</comment>',
ProjectionStatusType::ERROR => '<error>ERROR</error>',
});
if ($projectionStatus->type !== ProjectionStatusType::OK) {
$hasErrorsOrWarnings = true;
}
if ($verbose && ($projectionStatus->type !== ProjectionStatusType::OK || $projectionStatus->details)) {
$lines = explode(chr(10), $projectionStatus->details ?: '<comment>No details available.</comment>');
foreach ($lines as $line) {
Expand All @@ -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('<comment>Abort.</comment>');
return;
}

$contentRepositoryId = ContentRepositoryId::fromString($contentRepository);
$projectionService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->projectionServiceFactory);

Expand All @@ -131,32 +151,44 @@ 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('<comment>Abort.</comment>');
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('<success>Done.</success>');
}
}

/**
* 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('<comment>Abort.</comment>');
return;
}

Expand All @@ -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('<success>Done.</success>');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProjectionInterface<ProjectionStateInterface>>
*/
Expand Down Expand Up @@ -80,6 +90,4 @@ private static function projectionAlias(string $className): string
}
return $alias;
}


}
10 changes: 6 additions & 4 deletions Neos.Neos/Classes/Command/CrCommandController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
}
Expand Down Expand Up @@ -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('<success>Done</success>');
}
Expand Down

0 comments on commit d57947a

Please sign in to comment.