Skip to content

BUG: Workspace creation not thread-safe #5591

Open
@bwaidelich

Description

@bwaidelich

Observation

The following exception occurs when trying to log in to the Neos backend:

Exception #1651153651 in line 101 of Packages/Libraries/neos/eventstore-doctrineadapter/src/DoctrineEventStore.php: Expected version: 0, actual version: 2

Analysis

The contentGraph projection failed and stopped with status ERROR (see cr_default_subscriptions table) and the following error_message:

An exception occurred while executing a query: SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '<workspace-name>' for key 'cr_default_p_graph_workspace.PRIMARY'

And there are multiple WorkspaceWasCreated events for the same workspace name, which should never happen 🤯
SQL query to debug that:

SELECT JSON_UNQUOTE(JSON_EXTRACT(payload, '$.workspaceName')) workspace, COUNT(*) count FROM cr_default_events WHERE type = 'WorkspaceWasCreated' GROUP BY JSON_EXTRACT(payload, '$.workspaceName') ORDER BY count DESC

(count should be 1 for every workspace)

Bug

We're using the red model to enforce invariants, but there's a race condition between the check and the event publication:

$this->requireWorkspaceToNotExist($command->workspaceName, $commandHandlingDependencies);
$baseWorkspace = $commandHandlingDependencies->findWorkspaceByName($command->baseWorkspaceName);
if ($baseWorkspace === null) {
throw new BaseWorkspaceDoesNotExist(sprintf(
'The workspace %s (base workspace of %s) does not exist',
$command->baseWorkspaceName->value,
$command->workspaceName->value
), 1513890708);
}
$sourceContentStreamVersion = $commandHandlingDependencies->getContentStreamVersion($baseWorkspace->currentContentStreamId);
$this->requireContentStreamToNotBeClosed($baseWorkspace->currentContentStreamId, $commandHandlingDependencies);
$this->requireContentStreamToNotExistYet($command->newContentStreamId, $commandHandlingDependencies);
// When the workspace is created, we first have to fork the content stream
yield $this->forkContentStream(
$command->newContentStreamId,
$baseWorkspace->currentContentStreamId,
$sourceContentStreamVersion,
sprintf('Create workspace %s with base %s', $command->workspaceName->value, $baseWorkspace->workspaceName->value)
);
yield new EventsToPublish(
WorkspaceEventStreamName::fromWorkspaceName($command->workspaceName)->getEventStreamName(),
Events::with(
new WorkspaceWasCreated(
$command->workspaceName,
$command->baseWorkspaceName,
$command->newContentStreamId,
)
),
ExpectedVersion::ANY(),
);

Fix

An easy fix would be to replace ExpectedVersion::ANY() with ExpectedVersion::NO_STREAM().
This would mean that a workspace can't ever be re-created once it was deleted (via WorkspaceWasRemoved).
But maybe that's the behavior we want to have since re-using a previously existing workspace sounds dangerous..

Alternatively we'll have to add a version and removed column to the workspace read model so that we can enforce constraints using optimistic locking (like we do with content streams)

Work around

  1. do a database backup!
  2. delete invalid WorkspaceWasCreated events from the cr_default_events table
  3. run the ./flow subscription:reactivate contentGraph command to re-activate and catch up the contentGraph projection

Related: #5058

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions