Skip to content

Commit

Permalink
TASK: Add force strategy to the rebase workspace command
Browse files Browse the repository at this point in the history
In addition the number of logged informations when a rebase failed is reduced to a sane size that does not grow exponentially.

Resolves: #4545
  • Loading branch information
mficzel committed Mar 14, 2024
1 parent fe5c14b commit a8857d3
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,70 @@ Feature: Workspace discarding - basic functionality
And the graph projection is fully up to date

Then workspace user-ws-two has status OUTDATED

Scenario: Conflicting changes lead to OUTDATED_CONFLICT which can be recovered from via forced rebase

When the command CreateWorkspace is executed with payload:
| Key | Value |
| workspaceName | "user-ws-one" |
| baseWorkspaceName | "live" |
| newContentStreamId | "user-cs-one" |
| workspaceOwner | "owner-identifier" |
And the graph projection is fully up to date
And the command CreateWorkspace is executed with payload:
| Key | Value |
| workspaceName | "user-ws-two" |
| baseWorkspaceName | "live" |
| newContentStreamId | "user-cs-two" |
| workspaceOwner | "owner-identifier" |
And the graph projection is fully up to date

When the command RemoveNodeAggregate is executed with payload:
| Key | Value |
| nodeAggregateId | "nody-mc-nodeface" |
| nodeVariantSelectionStrategy | "allVariants" |
| coveredDimensionSpacePoint | {} |
| workspaceName | "user-ws-one" |
And the graph projection is fully up to date

When the command SetNodeProperties is executed with payload:
| Key | Value |
| workspaceName | "user-ws-two" |
| nodeAggregateId | "nody-mc-nodeface" |
| originDimensionSpacePoint | {} |
| propertyValues | {"text": "Modified"} |
And the graph projection is fully up to date

And the command CreateNodeAggregateWithNode is executed with payload:
| Key | Value |
| nodeAggregateId | "noderus-secundus" |
| nodeTypeName | "Neos.ContentRepository.Testing:Content" |
| parentNodeAggregateId | "lady-eleonode-rootford" |
| originDimensionSpacePoint | {} |
| workspaceName | "user-ws-two" |
And the graph projection is fully up to date

And the command SetNodeProperties is executed with payload:
| Key | Value |
| workspaceName | "user-ws-two" |
| nodeAggregateId | "noderus-secundus" |
| originDimensionSpacePoint | {} |
| propertyValues | {"text": "The other node"} |
And the graph projection is fully up to date

And the command PublishWorkspace is executed with payload:
| Key | Value |
| workspaceName | "user-ws-one" |
And the graph projection is fully up to date

Then workspace user-ws-two has status OUTDATED

When the command RebaseWorkspace is executed with payload:
| Key | Value |
| workspaceName | "user-ws-two" |
| rebasedContentStreamId | "user-cs-two-rebased" |
| rebaseErrorHandlingStrategy | "force" |
And the graph projection is fully up to date

Then workspace user-ws-two has status UP_TO_DATE
And I expect a node identified by user-cs-two-rebased;noderus-secundus;{} to exist in the content graph
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
use Neos\ContentRepository\Core\Feature\WorkspacePublication\Command\DiscardIndividualNodesFromWorkspace;
use Neos\ContentRepository\Core\Feature\WorkspacePublication\Command\DiscardWorkspace;
use Neos\ContentRepository\Core\Feature\WorkspacePublication\Command\PublishIndividualNodesFromWorkspace;
use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Dto\RebaseErrorHandlingStrategy;
use Neos\ContentRepository\Core\Feature\WorkspaceRebase\WorkspaceRebaseStatistics;
use Neos\ContentRepository\Core\Feature\ContentStreamCreation\Command\CreateContentStream;
use Neos\ContentRepository\Core\Feature\ContentStreamForking\Command\ForkContentStream;
Expand Down Expand Up @@ -385,41 +386,26 @@ private function handleRebaseWorkspace(
$rebaseStatistics = new WorkspaceRebaseStatistics();
$this->withContentStreamIdToUse(
$command->rebasedContentStreamId,
function () use ($originalCommands, $contentRepository, $rebaseStatistics, $workspaceContentStreamName, $baseWorkspace): void {
function () use ($originalCommands, $contentRepository, $rebaseStatistics): void {
foreach ($originalCommands as $i => $originalCommand) {
// We no longer need to adjust commands as the workspace stays the same
try {
$contentRepository->handle($originalCommand)->block();
// if we came this far, we know the command was applied successfully.
$rebaseStatistics->commandRebaseSuccess();
} catch (\Exception $e) {
$fullCommandListSoFar = '';
for ($a = 0; $a <= $i; $a++) {
$fullCommandListSoFar .= "\n - " . get_class($originalCommands[$a]);

if ($originalCommands[$a] instanceof \JsonSerializable) {
$fullCommandListSoFar .= ' ' . json_encode($originalCommands[$a]);
}
}

$rebaseStatistics->commandRebaseError(sprintf(
"The content stream %s cannot be rebased. Error with command %d (%s)"
. " - see nested exception for details.\n\n The base workspace %s is at content stream %s."
. "\n The full list of commands applied so far is: %s",
$workspaceContentStreamName->value,
$i,
"Error with command %s in sequence-number %d",
get_class($originalCommand),
$baseWorkspace->workspaceName->value,
$baseWorkspace->currentContentStreamId->value,
$fullCommandListSoFar
$i
), $e);
}
}
}
);

// if we got so far without an Exception, we can switch the Workspace's active Content stream.
if (!$rebaseStatistics->hasErrors()) {
if ($command->rebaseErrorHandlingStrategy === RebaseErrorHandlingStrategy::STRATEGY_FORCE || $rebaseStatistics->hasErrors() === false) {
$events = Events::with(
new WorkspaceWasRebased(
$command->workspaceName,
Expand Down Expand Up @@ -479,7 +465,7 @@ private function extractCommandsFromContentStreamMetadata(
* The "fromArray" might be declared via {@see RebasableToOtherWorkspaceInterface::fromArray()}
* or any other command can just implement it.
*/
$commands[] = $commandToRebaseClass::fromArray($commandToRebasePayload);
$commands[$eventEnvelope->sequenceNumber->value] = $commandToRebaseClass::fromArray($commandToRebasePayload);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
namespace Neos\ContentRepository\Core\Feature\WorkspaceRebase\Command;

use Neos\ContentRepository\Core\CommandHandler\CommandInterface;
use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Dto\RebaseErrorHandlingStrategy;
use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId;
use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName;

Expand All @@ -31,20 +32,29 @@
*/
private function __construct(
public WorkspaceName $workspaceName,
public ContentStreamId $rebasedContentStreamId
public ContentStreamId $rebasedContentStreamId,
public RebaseErrorHandlingStrategy $rebaseErrorHandlingStrategy
) {
}

public static function create(WorkspaceName $workspaceName): self
{
return new self($workspaceName, ContentStreamId::create());
return new self($workspaceName, ContentStreamId::create(), RebaseErrorHandlingStrategy::STRATEGY_FAIL);
}

/**
* Call this method if you want to run this command fully deterministically, f.e. during test cases
*/
public function withRebasedContentStreamId(ContentStreamId $newContentStreamId): self
{
return new self($this->workspaceName, $newContentStreamId);
return new self($this->workspaceName, $newContentStreamId, $this->rebaseErrorHandlingStrategy);
}

/**
* Call this method if you want to run this command with a specific error handling strategy like force
*/
public function withErrorHandlingStrategy(RebaseErrorHandlingStrategy $rebaseErrorHandlingStrategy): self
{
return new self($this->workspaceName, $this->rebasedContentStreamId, $rebaseErrorHandlingStrategy);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

/*
* This file is part of the Neos.ContentRepository package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

declare(strict_types=1);

namespace Neos\ContentRepository\Core\Feature\WorkspaceRebase\Dto;

/**
* The strategy how to handle errors during workspace rebase
*
* - fail (default) ensures conflicts are not ignored but reported
* - force will rebase even if some conflicting events could have to be rebased
*
* @api DTO of {@see RebaseWorkspace} command
*/
enum RebaseErrorHandlingStrategy: string implements \JsonSerializable
{
/**
* This strategy rebasing will fail if conflicts are detected and the "WorkspaceRebaseFailed" event is added.
*/
case STRATEGY_FAIL = 'fail';

/**
* This strategy means all events that can be applied are rebased and conflicting events are ignored
*/
case STRATEGY_FORCE = 'force';

/**
* @return string
*/
public function jsonSerialize(): string
{
return $this->value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
namespace Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Features;

use Behat\Gherkin\Node\TableNode;
use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Dto\RebaseErrorHandlingStrategy;
use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId;
use Neos\ContentRepository\Core\Feature\ContentStreamEventStreamName;
use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateRootWorkspace;
Expand Down Expand Up @@ -105,6 +106,9 @@ public function theCommandRebaseWorkspaceIsExecutedWithPayload(TableNode $payloa
if (isset($commandArguments['rebasedContentStreamId'])) {
$command = $command->withRebasedContentStreamId(ContentStreamId::fromString($commandArguments['rebasedContentStreamId']));
}
if (isset($commandArguments['rebaseErrorHandlingStrategy'])) {
$command = $command->withErrorHandlingStrategy(RebaseErrorHandlingStrategy::from($commandArguments['rebaseErrorHandlingStrategy']));
}

$this->lastCommandOrEventResult = $this->currentContentRepository->handle($command);
}
Expand Down
24 changes: 18 additions & 6 deletions Neos.Neos/Classes/Command/WorkspaceCommandController.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
use Neos\ContentRepository\Core\Feature\WorkspacePublication\Command\DiscardWorkspace;
use Neos\ContentRepository\Core\Feature\WorkspacePublication\Command\PublishWorkspace;
use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Command\RebaseWorkspace;
use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Dto\RebaseErrorHandlingStrategy;
use Neos\ContentRepository\Core\Projection\Workspace\Workspace;
use Neos\ContentRepository\Core\Projection\Workspace\WorkspaceStatus;
use Neos\ContentRepository\Core\Service\WorkspaceMaintenanceServiceFactory;
use Neos\ContentRepository\Core\SharedModel\Exception\WorkspaceDoesNotExist;
use Neos\ContentRepository\Core\SharedModel\User\UserId;
Expand Down Expand Up @@ -112,23 +114,33 @@ public function discardCommand(string $workspace, string $contentRepositoryIdent
*
* @param string $workspace Name of the workspace, for example "user-john"
* @param string $contentRepositoryIdentifier
* @param bool $force Rebase all events that do not conflict
* @throws StopCommandException
*/
public function rebaseCommand(string $workspace, string $contentRepositoryIdentifier = 'default'): void
public function rebaseCommand(string $workspace, string $contentRepositoryIdentifier = 'default', bool $force = false): void
{
$contentRepositoryId = ContentRepositoryId::fromString($contentRepositoryIdentifier);
$contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId);

try {
$contentRepository->handle(
RebaseWorkspace::create(
WorkspaceName::fromString($workspace),
)
)->block();
$rebaseCommand = RebaseWorkspace::create(
WorkspaceName::fromString($workspace),
);
if ($force) {
$rebaseCommand = $rebaseCommand->withErrorHandlingStrategy(RebaseErrorHandlingStrategy::STRATEGY_FORCE);
}
$contentRepository->handle($rebaseCommand)->block();
} catch (WorkspaceDoesNotExist $exception) {
$this->outputLine('Workspace "%s" does not exist', [$workspace]);
$this->quit(1);
}

$workspaceObject = $contentRepository->getWorkspaceFinder()->findOneByName(WorkspaceName::fromString($workspace));
if ($workspaceObject && $workspaceObject->status === WorkspaceStatus::OUTDATED_CONFLICT) {
$this->outputLine('Rebasing of workspace %s is not possible due to conflicts. You can try the --force option.', [$workspace]);
$this->quit(1);
}

$this->outputLine('Rebased workspace %s', [$workspace]);
}

Expand Down

0 comments on commit a8857d3

Please sign in to comment.