Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEATURE: Overhaul content stream pruner to work on event stream #5297

Merged
merged 31 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
8638de8
TASK: Remove `removed` state from ContentStream model and database
mhsdesign Oct 17, 2024
0d0ffa5
BUGFIX: 4913 make `pruneAll` independent of projection
mhsdesign Oct 17, 2024
58581c8
BUGFIX: Also remove closed content streams in prune
mhsdesign Oct 17, 2024
1750160
TASK: Remove obsolete `REBASE_ERROR` content stream state
mhsdesign Oct 17, 2024
8422b05
Merge remote-tracking branch 'origin/9.0' into bugfix/harden-content-…
mhsdesign Oct 24, 2024
6989b34
TASK: Use `ContentStreamWasRemoved` event instead of `RemoveContentSt…
mhsdesign Oct 24, 2024
cacc70c
TASK: Build content stream for pruning state by reading the event stream
mhsdesign Oct 24, 2024
7aa6b6c
TASK: Harden `contentstream:prune` to work on event stream and catch …
mhsdesign Oct 24, 2024
b8a93cf
Linte is basically ente with i
mhsdesign Oct 24, 2024
733490e
TASK: Also handle `WorkspaceWasPublished` event
mhsdesign Oct 24, 2024
8f0a24e
Merge remote-tracking branch 'origin/9.0' into bugfix/harden-content-…
mhsdesign Oct 24, 2024
3dec67f
TASK: Prune also dangling streams of legacy `WorkspaceRebaseFailed` i…
mhsdesign Oct 24, 2024
21ae3b9
Merge remote-tracking branch 'origin/9.0' into bugfix/harden-content-…
mhsdesign Oct 28, 2024
b279a38
TASK: Remove now obsolete content stream status from projection
mhsdesign Oct 26, 2024
2541e7c
TASK: Avoid use of `CloseContentStream` command in tests and use event
mhsdesign Oct 26, 2024
d715cf7
TASK: Remove legacy low level ContentStream commands and command hand…
mhsdesign Oct 26, 2024
4d64cbd
TASK: Ensure that `cr:prune` prunes also workspaces cleanly
mhsdesign Oct 26, 2024
a2b3c58
BUGFIX: ContentStream pruner mark `RootWorkspaceWasCreated` as in use
mhsdesign Oct 28, 2024
41d2941
TASK: Always remove all dangling content streams
mhsdesign Oct 28, 2024
baf56ff
BUGFIX: Mark workspace outdated if its source content stream was removed
mhsdesign Oct 28, 2024
da89494
TASK: `Workspace` to use static `create` like `Node` and other read m…
mhsdesign Oct 28, 2024
a7ee525
TASK: Introduce `contentstream:status`
mhsdesign Oct 29, 2024
40f41fd
TASK: Only remove dangling streams `contentstream:removeDangling` tha…
mhsdesign Oct 29, 2024
27b72d4
TASK: Remove obsolete explicit pruning (I prune unused content stream…
mhsdesign Oct 29, 2024
d1d56bb
TASK: Adjust documentation and add force flag
mhsdesign Oct 29, 2024
3d808c0
TASK: Remove obsolete `extractContentStreamId`
mhsdesign Oct 29, 2024
52c54a2
TASK: Minor cleanup in docs and introduce `isDangling`
mhsdesign Oct 29, 2024
e665aba
TASK: Test content stream pruner status output
mhsdesign Oct 29, 2024
00f6af7
TASK: Introduce simple `flow migrateevents:backup`
mhsdesign Oct 30, 2024
aa6a6b5
TASK: Adjust variables and namings
mhsdesign Oct 30, 2024
d6e7306
TASK: Validate `--remove-temporary-before`
mhsdesign Oct 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ public function findContentStreamById(ContentStreamId $contentStreamId): ?Conten
{
$contentStreamByIdStatement = <<<SQL
SELECT
id, sourceContentStreamId, status, version, removed
id, sourceContentStreamId, status, version
FROM
{$this->tableNames->contentStream()}
WHERE
Expand All @@ -119,7 +119,7 @@ public function findContentStreams(): ContentStreams
{
$contentStreamsStatement = <<<SQL
SELECT
id, sourceContentStreamId, status, version, removed
id, sourceContentStreamId, status, version
FROM
{$this->tableNames->contentStream()}
SQL;
Expand Down Expand Up @@ -153,8 +153,7 @@ private static function contentStreamFromDatabaseRow(array $row): ContentStream
ContentStreamId::fromString($row['id']),
isset($row['sourceContentStreamId']) ? ContentStreamId::fromString($row['sourceContentStreamId']) : null,
ContentStreamStatus::from($row['status']),
Version::fromInteger((int)$row['version']),
(bool)$row['removed']
Version::fromInteger((int)$row['version'])
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,6 @@ private function createContentStreamTable(): Table
DbalSchemaFactory::columnForContentStreamId('sourceContentStreamId')->setNotnull(false),
// Should become a DB ENUM (unclear how to configure with DBAL) or int (latter needs adaption to code)
(new Column('status', Type::getType(Types::BINARY)))->setLength(20)->setNotnull(true),
(new Column('removed', Type::getType(Types::BOOLEAN)))->setDefault(false)->setNotnull(false)
]);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,7 @@ private function updateContentStreamStatus(ContentStreamId $contentStreamId, Con

private function removeContentStream(ContentStreamId $contentStreamId): void
{
$this->dbal->update($this->tableNames->contentStream(), [
'removed' => true,
], [
$this->dbal->delete($this->tableNames->contentStream(), [
'id' => $contentStreamId->value
]);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
*/
final readonly class ContentStreamEventStreamName
{
private const EVENT_STREAM_NAME_PREFIX = 'ContentStream:';
public const EVENT_STREAM_NAME_PREFIX = 'ContentStream:';

private function __construct(
public string $value
Expand Down
50 changes: 34 additions & 16 deletions Neos.ContentRepository.Core/Classes/Service/ContentStreamPruner.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@
use Neos\ContentRepository\Core\Feature\ContentStreamRemoval\Command\RemoveContentStream;
use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStream;
use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId;
use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreams;
use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamStatus;
use Neos\EventStore\EventStoreInterface;
use Neos\EventStore\Model\Event\StreamName;
use Neos\EventStore\Model\EventStream\VirtualStreamName;

/**
* For implementation details of the content stream states and removed state, see {@see ContentStream}.
Expand Down Expand Up @@ -49,6 +50,7 @@ public function prune(bool $removeTemporary = false): iterable
if ($removeTemporary) {
$status[] = ContentStreamStatus::CREATED;
$status[] = ContentStreamStatus::FORKED;
$status[] = ContentStreamStatus::CLOSED;
}
$unusedContentStreams = $this->contentRepository->findContentStreams()->filter(
static fn (ContentStream $contentStream) => in_array($contentStream->status, $status, true),
Expand All @@ -73,28 +75,28 @@ public function prune(bool $removeTemporary = false): iterable
*
* - Otherwise, we cannot replay the other content streams correctly (if the base content streams are missing).
*
* @return ContentStreams the removed content streams
* @return list<StreamName> the removed content streams
*/
public function pruneRemovedFromEventStream(): ContentStreams
public function pruneRemovedFromEventStream(): array
mhsdesign marked this conversation as resolved.
Show resolved Hide resolved
{
$removedContentStreams = $this->findUnusedAndRemovedContentStreams();
foreach ($removedContentStreams as $removedContentStream) {
$streamName = ContentStreamEventStreamName::fromContentStreamId($removedContentStream->id)
->getEventStreamName();
$this->eventStore->deleteStream($streamName);
$removedContentStreams = $this->findUnusedAndRemovedContentStreamIds();
foreach ($removedContentStreams as $removedContentStreamName) {
$this->eventStore->deleteStream($removedContentStreamName);
mhsdesign marked this conversation as resolved.
Show resolved Hide resolved
}
return $removedContentStreams;
}

public function pruneAll(): void
{
foreach ($this->contentRepository->findContentStreams() as $contentStream) {
$streamName = ContentStreamEventStreamName::fromContentStreamId($contentStream->id)->getEventStreamName();
foreach ($this->findAllContentStreamEventNames() as $streamName) {
$this->eventStore->deleteStream($streamName);
}
}

private function findUnusedAndRemovedContentStreams(): ContentStreams
/**
* @return list<StreamName>
*/
private function findUnusedAndRemovedContentStreamIds(): array
mhsdesign marked this conversation as resolved.
Show resolved Hide resolved
{
$allContentStreams = $this->contentRepository->findContentStreams();

Expand All @@ -105,7 +107,7 @@ private function findUnusedAndRemovedContentStreams(): ContentStreams

// Step 1: Find all content streams currently in direct use by a workspace
foreach ($allContentStreams as $stream) {
if ($stream->status === ContentStreamStatus::IN_USE_BY_WORKSPACE && !$stream->removed) {
if ($stream->status === ContentStreamStatus::IN_USE_BY_WORKSPACE) {
$contentStreamIdsStack[] = $stream->id;
}
}
Expand All @@ -129,13 +131,29 @@ private function findUnusedAndRemovedContentStreams(): ContentStreams
}

// Step 3: Check for removed content streams which we do not need anymore transitively
$allContentStreamEventStreamNames = $this->findAllContentStreamEventNames();

$removedContentStreams = [];
foreach ($allContentStreams as $contentStream) {
if ($contentStream->removed && !array_key_exists($contentStream->id->value, $transitiveUsedStreams)) {
$removedContentStreams[] = $contentStream;
foreach ($allContentStreamEventStreamNames as $streamName) {
$removedContentStream = substr($streamName->value, strlen(ContentStreamEventStreamName::EVENT_STREAM_NAME_PREFIX));
if (!array_key_exists($removedContentStream, $transitiveUsedStreams)) {
$removedContentStreams[] = $streamName;
}
}

return ContentStreams::fromArray($removedContentStreams);
return $removedContentStreams;
}

/**
* @return list<StreamName>
*/
private function findAllContentStreamEventNames(): array
{
$events = $this->eventStore->load(VirtualStreamName::forCategory(ContentStreamEventStreamName::EVENT_STREAM_NAME_PREFIX));
$allContentStreamEventStreamNames = [];
foreach ($events as $eventEnvelope) {
$allContentStreamEventStreamNames[$eventEnvelope->streamName->value] = true;
}
return array_map(StreamName::fromString(...), array_keys($allContentStreamEventStreamNames));
mhsdesign marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ public function __construct(
public ?ContentStreamId $sourceContentStreamId,
public ContentStreamStatus $status,
public Version $version,
public bool $removed
) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,7 @@ protected function readPayloadTable(TableNode $payloadTable): array
public function iExpectTheContentStreamToNotExist(string $rawContentStreamId): void
{
$contentStream = $this->currentContentRepository->findContentStreamById(ContentStreamId::fromString($rawContentStreamId));
$contentStreamExists = $contentStream !== null && !$contentStream->removed;
Assert::assertFalse($contentStreamExists, sprintf('Content stream "%s" was not expected to exist, but it does', $rawContentStreamId));
Assert::assertNull($contentStream, sprintf('Content stream "%s" was not expected to exist, but it does', $rawContentStreamId));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,10 @@ public function pruneRemovedFromEventStreamCommand(string $contentRepository = '
$contentRepositoryId = ContentRepositoryId::fromString($contentRepository);
$contentStreamPruner = $this->contentRepositoryRegistry->buildService($contentRepositoryId, new ContentStreamPrunerFactory());

$unusedContentStreams = $contentStreamPruner->pruneRemovedFromEventStream();
$unusedContentStreamNames = $contentStreamPruner->pruneRemovedFromEventStream();
$unusedContentStreamsPresent = false;
foreach ($unusedContentStreams as $contentStreamId) {
$this->outputFormatted('Removed events for %s', [$contentStreamId->id->value]);
foreach ($unusedContentStreamNames as $contentStreamName) {
$this->outputFormatted('Removed events for %s', [$contentStreamName->value]);
$unusedContentStreamsPresent = true;
}
if (!$unusedContentStreamsPresent) {
Expand Down
Loading