Skip to content

Commit

Permalink
Merge pull request #4965 from neos/rebaseFailedException
Browse files Browse the repository at this point in the history
Rebase failed exception
  • Loading branch information
bwaidelich committed Apr 10, 2024
2 parents 7cf6bee + 3c8a348 commit d102455
Show file tree
Hide file tree
Showing 13 changed files with 202 additions and 126 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ Feature: Workspace discarding - basic functionality
| Key | Value |
| text | "Modified in live workspace" |

Scenario: Conflicting changes lead to OUTDATED_CONFLICT which can be recovered from via discard
Scenario: Conflicting changes lead to OUTDATED which can be recovered from via discard

When the command CreateWorkspace is executed with payload:
| Key | Value |
Expand Down Expand Up @@ -147,12 +147,12 @@ Feature: Workspace discarding - basic functionality

Then workspace user-ws-two has status OUTDATED

When the command RebaseWorkspace is executed with payload:
When the command RebaseWorkspace is executed with payload and exceptions are caught:
| Key | Value |
| workspaceName | "user-ws-two" |
| rebasedContentStreamId | "user-cs-two-rebased" |

Then workspace user-ws-two has status OUTDATED_CONFLICT
Then workspace user-ws-two has status OUTDATED

When the command DiscardWorkspace is executed with payload:
| Key | Value |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,11 @@
use Neos\ContentRepository\Core\Feature\WorkspacePublication\Event\WorkspaceWasPublished;
use Neos\ContentRepository\Core\Feature\WorkspacePublication\Exception\BaseWorkspaceHasBeenModifiedInTheMeantime;
use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Command\RebaseWorkspace;
use Neos\ContentRepository\Core\Feature\WorkspaceRebase\CommandsThatFailedDuringRebase;
use Neos\ContentRepository\Core\Feature\WorkspaceRebase\CommandThatFailedDuringRebase;
use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Dto\RebaseErrorHandlingStrategy;
use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Event\WorkspaceRebaseFailed;
use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Event\WorkspaceWasRebased;
use Neos\ContentRepository\Core\Feature\WorkspaceRebase\WorkspaceRebaseStatistics;
use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Exception\WorkspaceRebaseFailed;
use Neos\ContentRepository\Core\Projection\Workspace\Workspace;
use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamAlreadyExists;
use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamDoesNotExistYet;
Expand Down Expand Up @@ -351,23 +352,27 @@ private function publishContentStream(
/**
* @throws BaseWorkspaceDoesNotExist
* @throws WorkspaceDoesNotExist
* @throws \Exception
* @throws WorkspaceRebaseFailed
*/
private function handleRebaseWorkspace(
RebaseWorkspace $command,
ContentRepository $contentRepository,
): EventsToPublish {
$workspace = $this->requireWorkspace($command->workspaceName, $contentRepository);
$baseWorkspace = $this->requireBaseWorkspace($workspace, $contentRepository);
$oldWorkspaceContentStreamId = $workspace->currentContentStreamId;
$oldWorkspaceContentStreamIdState = $contentRepository->getContentStreamFinder()
->findStateForContentStream($oldWorkspaceContentStreamId);
if ($oldWorkspaceContentStreamIdState === null) {
throw new \DomainException('Cannot rebase a workspace with a stateless content stream', 1711718314);
}

// 0) close old content stream
$contentRepository->handle(
CloseContentStream::create(
$workspace->currentContentStreamId,
)
CloseContentStream::create($oldWorkspaceContentStreamId)
)->block();

// - fork a new content stream
// 1) fork a new content stream
$rebasedContentStreamId = $command->rebasedContentStreamId;
$contentRepository->handle(
ForkContentStream::create(
Expand All @@ -381,31 +386,31 @@ private function handleRebaseWorkspace(
$workspace->currentContentStreamId
);

// - extract the commands from the to-be-rebased content stream; and applies them on the new content stream
// 2) extract the commands from the to-be-rebased content stream; and applies them on the new content stream
$originalCommands = $this->extractCommandsFromContentStreamMetadata($workspaceContentStreamName);
$rebaseStatistics = new WorkspaceRebaseStatistics();
$commandsThatFailed = new CommandsThatFailedDuringRebase();
ContentStreamIdOverride::applyContentStreamIdToClosure(
$command->rebasedContentStreamId,
function () use ($originalCommands, $contentRepository, $rebaseStatistics): void {
foreach ($originalCommands as $i => $originalCommand) {
function () use ($originalCommands, $contentRepository, &$commandsThatFailed): void {
foreach ($originalCommands as $sequenceNumber => $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) {
$rebaseStatistics->commandRebaseError(sprintf(
"Error with command %s in sequence-number %d",
get_class($originalCommand),
$i
), $e);
$commandsThatFailed = $commandsThatFailed->add(
new CommandThatFailedDuringRebase(
$sequenceNumber,
$originalCommand,
$e
)
);
}
}
}
);

// if we got so far without an Exception, we can switch the Workspace's active Content stream.
if ($command->rebaseErrorHandlingStrategy === RebaseErrorHandlingStrategy::STRATEGY_FORCE || $rebaseStatistics->hasErrors() === false) {
// 3) if we got so far without an exception (or if we don't care), we can switch the workspace's active content stream.
if ($command->rebaseErrorHandlingStrategy === RebaseErrorHandlingStrategy::STRATEGY_FORCE || $commandsThatFailed->isEmpty()) {
$events = Events::with(
new WorkspaceWasRebased(
$command->workspaceName,
Expand All @@ -420,22 +425,21 @@ function () use ($originalCommands, $contentRepository, $rebaseStatistics): void
ExpectedVersion::ANY()
);
} else {
// an error occurred during the rebase; so we need to record this using a "WorkspaceRebaseFailed" event.

$event = Events::with(
new WorkspaceRebaseFailed(
$command->workspaceName,
$rebasedContentStreamId,
$workspace->currentContentStreamId,
$rebaseStatistics->getErrors()
// 3.E) In case of an exception, reopen the old content stream...
$contentRepository->handle(
ReopenContentStream::create(
$oldWorkspaceContentStreamId,
$oldWorkspaceContentStreamIdState,
)
);
)->block();

return new EventsToPublish(
$workspaceStreamName,
$event,
ExpectedVersion::ANY()
);
// ... remove the newly created one...
$contentRepository->handle(RemoveContentStream::create(
$rebasedContentStreamId
))->block();

// ...and throw an exception that contains all the information about what exactly failed
throw new WorkspaceRebaseFailed($commandsThatFailed, 'Rebase failed', 1711713880);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?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;

use Neos\ContentRepository\Core\CommandHandler\CommandInterface;

/**
* @internal implementation detail of WorkspaceCommandHandler
*/
final readonly class CommandThatFailedDuringRebase
{
/**
* @param int $sequenceNumber the event store sequence number of the event containing the command to be rebased
* @param CommandInterface $command the command that failed
* @param \Throwable $exception how the command failed
*/
public function __construct(
public int $sequenceNumber,
public CommandInterface $command,
public \Throwable $exception
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

/*
* This file is part of the Neos.ContentRepository.Core 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;

/**
* @implements \IteratorAggregate<int,CommandThatFailedDuringRebase>
*
* @api part of the exception exposed when rebasing failed
*/
final readonly class CommandsThatFailedDuringRebase implements \IteratorAggregate
{
/**
* @var array<int,CommandThatFailedDuringRebase>
*/
private array $items;

public function __construct(CommandThatFailedDuringRebase ...$items)
{
$this->items = array_values($items);
}

public function add(CommandThatFailedDuringRebase $item): self
{
$items = $this->items;
$items[] = $item;

return new self(...$items);
}

public function isEmpty(): bool
{
return $this->items === [];
}

/**
* @return \Traversable<int,CommandThatFailedDuringRebase>
*/
public function getIterator(): \Traversable
{
yield from $this->items;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

/**
* @api events are the persistence-API of the content repository
* @deprecated this is no longer logged, instead an exception is thrown
* @see Neos\ContentRepository\Core\Feature\WorkspaceRebase\Exception\WorkspaceRebaseFailed
*/
final readonly class WorkspaceRebaseFailed implements EventInterface
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

/*
* This file is part of the Neos.ContentRepository.Core 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\Exception;

use Neos\ContentRepository\Core\Feature\WorkspaceRebase\CommandsThatFailedDuringRebase;

/**
* @api this exception contains information about what exactly went wrong during rebase
*/
final class WorkspaceRebaseFailed extends \Exception
{
public function __construct(
public readonly CommandsThatFailedDuringRebase $commandsThatFailedDuringRebase,
string $message = "",
int $code = 0,
?\Throwable $previous = null
) {
parent::__construct($message, $code, $previous);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface;
use Neos\ContentRepository\Core\Feature\WorkspaceEventStreamName;
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\EventStore\EventStoreInterface;

Expand All @@ -29,15 +30,18 @@ public function __construct(
* @throws \Neos\ContentRepository\Core\Feature\WorkspaceCreation\Exception\BaseWorkspaceDoesNotExist
* @throws \Neos\ContentRepository\Core\SharedModel\Exception\WorkspaceDoesNotExist
*/
public function rebaseOutdatedWorkspaces(): array
public function rebaseOutdatedWorkspaces(?RebaseErrorHandlingStrategy $strategy = null): array
{
$outdatedWorkspaces = $this->contentRepository->getWorkspaceFinder()->findOutdated();

foreach ($outdatedWorkspaces as $workspace) {
/* @var Workspace $workspace */
$this->contentRepository->handle(RebaseWorkspace::create(
$rebaseCommand = RebaseWorkspace::create(
$workspace->workspaceName,
))->block();
);
if ($strategy) {
$rebaseCommand = $rebaseCommand->withErrorHandlingStrategy($strategy);
}
$this->contentRepository->handle($rebaseCommand)->block();
}

return $outdatedWorkspaces;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,16 @@ public function theCommandRebaseWorkspaceIsExecutedWithPayload(TableNode $payloa

$this->lastCommandOrEventResult = $this->currentContentRepository->handle($command);
}

/**
* @When /^the command RebaseWorkspace is executed with payload and exceptions are caught:$/
*/
public function theCommandRebaseWorkspaceIsExecutedWithPayloadAndExceptionsAreCaught(TableNode $payloadTable)
{
try {
$this->theCommandRebaseWorkspaceIsExecutedWithPayload($payloadTable);
} catch (\Exception $e) {
$this->lastCommandException = $e;
}
}
}

0 comments on commit d102455

Please sign in to comment.