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 (performance): compress SetSerializedNodeProperties together; allow experiments in codebase #4263

Draft
wants to merge 6 commits into
base: 9.0
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -102,7 +102,7 @@ protected function getEnvironment()
return $this->objectManager->get(Environment::class);
}

protected function initCleanContentRepository(array $adapterKeys): void
protected function initCleanContentRepository(array $adapterKeys = null): void
{
$this->logToRaceConditionTracker(['msg' => 'initCleanContentRepository']);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ class FeatureContext implements \Behat\Behat\Context\Context
protected $behatTestHelperObjectName = BehatTestHelper::class;

protected ?ContentRepositoryRegistry $contentRepositoryRegistry = null;
private array $contentRepositoryExperiments = [];
private ?array $adapterKeys = null;

public function __construct()
{
Expand All @@ -107,6 +109,24 @@ public function __construct()
$this->setUpInterleavingLogger();
}

/**
* @BeforeScenario @contentrepository
*/
public function resetExperiments()
{
$this->contentRepositoryExperiments = [];
}

/**
* @When /^the content repository experiment "([^"]*)" has value "([^"]*)"$/
*/
public function theContentRepositoryExperimentHasValue($experimentName, $experimentValue)
{
$this->contentRepositoryExperiments[$experimentName] = $experimentValue;
$this->initCleanContentRepository();
}


private function setUpInterleavingLogger(): void
{
// prepare race tracking for debugging into the race log
Expand Down Expand Up @@ -134,8 +154,14 @@ private function setUpInterleavingLogger(): void
* @param array<string> $adapterKeys "DoctrineDBAL" if
* @return void
*/
protected function initCleanContentRepository(array $adapterKeys): void
protected function initCleanContentRepository(array $adapterKeys = null): void
{
if ($adapterKeys !== null) {
$this->adapterKeys = $adapterKeys;
} else {
// re-use old adapter keys if not explicitly given
$adapterKeys = $this->adapterKeys;
}
$this->logToRaceConditionTracker(['msg' => 'initCleanContentRepository']);

$configurationManager = $this->getObjectManager()->get(ConfigurationManager::class);
Expand All @@ -155,6 +181,7 @@ protected function initCleanContentRepository(array $adapterKeys): void
}
$registrySettings['presets'][$this->contentRepositoryId->value]['userIdProvider']['factoryObjectName'] = FakeUserIdProviderFactory::class;
$registrySettings['presets'][$this->contentRepositoryId->value]['clock']['factoryObjectName'] = FakeClockFactory::class;
$registrySettings['presets'][$this->contentRepositoryId->value]['experiments'] = $this->contentRepositoryExperiments;

$this->contentRepositoryRegistry = new ContentRepositoryRegistry(
$registrySettings,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
@contentrepository @adapters=DoctrineDBAL
Feature: Workspace publishing with experimental command compression

If people write texts, the Neos UI will send many SetSerializedNodeProperties commands touching the same node.
This change compacts this to a single SetSerializedNodeProperties command, so that projection rebase is sped up.

Background:
Given I have no content dimensions
And I have the following NodeTypes configuration:
"""
'Neos.ContentRepository:Root': {}
'Neos.ContentRepository.Testing:Content':
properties:
text:
type: string
"""
And the command CreateRootWorkspace is executed with payload:
| Key | Value |
| workspaceName | "live" |
| newContentStreamId | "cs-identifier" |
And the graph projection is fully up to date
And the command CreateRootNodeAggregateWithNode is executed with payload:
| Key | Value |
| contentStreamId | "cs-identifier" |
| nodeAggregateId | "lady-eleonode-rootford" |
| nodeTypeName | "Neos.ContentRepository:Root" |

And the event NodeAggregateWithNodeWasCreated was published with payload:
| Key | Value |
| contentStreamId | "cs-identifier" |
| nodeAggregateId | "nody-mc-nodeface" |
| nodeTypeName | "Neos.ContentRepository.Testing:Content" |
| originDimensionSpacePoint | {} |
| coveredDimensionSpacePoints | [{}] |
| parentNodeAggregateId | "lady-eleonode-rootford" |
| nodeName | "child" |
| nodeAggregateClassification | "regular" |
And the event NodeAggregateWithNodeWasCreated was published with payload:
| Key | Value |
| contentStreamId | "cs-identifier" |
| nodeAggregateId | "hamish-mc-stonehill" |
| nodeTypeName | "Neos.ContentRepository.Testing:Content" |
| originDimensionSpacePoint | {} |
| coveredDimensionSpacePoints | [{}] |
| parentNodeAggregateId | "lady-eleonode-rootford" |
| nodeName | "child" |
| nodeAggregateClassification | "regular" |

And the graph projection is fully up to date

And the command SetNodeProperties is executed with payload:
| Key | Value |
| contentStreamId | "cs-identifier" |
| nodeAggregateId | "nody-mc-nodeface" |
| originDimensionSpacePoint | {} |
| propertyValues | {"text": "content 1"} |
And the command SetNodeProperties is executed with payload:
| Key | Value |
| contentStreamId | "cs-identifier" |
| nodeAggregateId | "hamish-mc-stonehill" |
| originDimensionSpacePoint | {} |
| propertyValues | {"text": "content stonehill"} |
# we need to ensure that the projections are up to date now; otherwise a content stream is forked with an out-
# of-date base version. This means the content stream can never be merged back, but must always be rebased.
And the graph projection is fully up to date
And the command CreateWorkspace is executed with payload:
| Key | Value |
| workspaceName | "user-test" |
| baseWorkspaceName | "live" |
| newContentStreamId | "user-cs-identifier" |
| workspaceOwner | "owner-identifier" |
And the graph projection is fully up to date
# we need to trigger a rebase for compaction to happen; so we need one more event on the original content stream
And the command SetNodeProperties is executed with payload:
| Key | Value |
| contentStreamId | "cs-identifier" |
| nodeAggregateId | "nody-mc-nodeface" |
| originDimensionSpacePoint | {} |
| propertyValues | {"text": "c-orig"} |

And the command SetNodeProperties is executed with payload:
| Key | Value |
| contentStreamId | "user-cs-identifier" |
| nodeAggregateId | "nody-mc-nodeface" |
| originDimensionSpacePoint | {} |
| propertyValues | {"text": "content 2"} |
And the command SetNodeProperties is executed with payload:
| Key | Value |
| contentStreamId | "user-cs-identifier" |
| nodeAggregateId | "nody-mc-nodeface" |
| originDimensionSpacePoint | {} |
| propertyValues | {"text": "content 3"} |
And the graph projection is fully up to date

Scenario: publishing without compression
When the command RebaseWorkspace is executed with payload:
| Key | Value |
| workspaceName | "user-test" |
And the graph projection is fully up to date
When the command PublishWorkspace is executed with payload:
| Key | Value |
| workspaceName | "user-test" |
And the graph projection is fully up to date

When I am in the active content stream of workspace "live" and dimension space point {}
Then I expect node aggregate identifier "nody-mc-nodeface" to lead to node cs-identifier;nody-mc-nodeface;{}
And I expect this node to have the following properties:
| Key | Value |
| text | "content 3" |

Then I expect exactly 9 events to be published on stream "ContentStream:cs-identifier"


Scenario: publishing with simple compression
# compaction of the two write-events into one -> we expect one less event than above.
When the content repository experiment "compactCommands" has value "compress-simple"
When the command RebaseWorkspace is executed with payload:
| Key | Value |
| workspaceName | "user-test" |
And the graph projection is fully up to date
When the command PublishWorkspace is executed with payload:
| Key | Value |
| workspaceName | "user-test" |
And the graph projection is fully up to date

When I am in the active content stream of workspace "live" and dimension space point {}
Then I expect node aggregate identifier "nody-mc-nodeface" to lead to node cs-identifier;nody-mc-nodeface;{}
And I expect this node to have the following properties:
| Key | Value |
| text | "content 3" |

# THIS IS THE MODIFICATION
Then I expect exactly 8 events to be published on stream "ContentStream:cs-identifier"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clever way to test this and great job with the comments!



Scenario: simple compression only happens up to a modification of ANOTHER node
# node1 <-\
# node1 <-- COMPRESSED
# node2
# node1 <-\
# node1 <-- COMPRESSED
And the command SetNodeProperties is executed with payload:
| Key | Value |
| contentStreamId | "user-cs-identifier" |
| nodeAggregateId | "hamish-mc-stonehill" |
| originDimensionSpacePoint | {} |
| propertyValues | {"text": "other"} |

And the command SetNodeProperties is executed with payload:
| Key | Value |
| contentStreamId | "user-cs-identifier" |
| nodeAggregateId | "nody-mc-nodeface" |
| originDimensionSpacePoint | {} |
| propertyValues | {"text": "content 2"} |
And the command SetNodeProperties is executed with payload:
| Key | Value |
| contentStreamId | "user-cs-identifier" |
| nodeAggregateId | "nody-mc-nodeface" |
| originDimensionSpacePoint | {} |
| propertyValues | {"text": "content 3"} |
And the command SetNodeProperties is executed with payload:
| Key | Value |
| contentStreamId | "user-cs-identifier" |
| nodeAggregateId | "nody-mc-nodeface" |
| originDimensionSpacePoint | {} |
| propertyValues | {"text": "content 4"} |

When the content repository experiment "compactCommands" has value "compress-simple"
When the command RebaseWorkspace is executed with payload:
| Key | Value |
| workspaceName | "user-test" |
And the graph projection is fully up to date
When the command PublishWorkspace is executed with payload:
| Key | Value |
| workspaceName | "user-test" |
And the graph projection is fully up to date

When I am in the active content stream of workspace "live" and dimension space point {}
Then I expect node aggregate identifier "nody-mc-nodeface" to lead to node cs-identifier;nody-mc-nodeface;{}
And I expect this node to have the following properties:
| Key | Value |
| text | "content 4" |

# 10 instead of 13 events
Then I expect exactly 10 events to be published on stream "ContentStream:cs-identifier"

Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
use Neos\ContentRepository\Core\NodeType\NodeTypeManager;
use Neos\ContentRepository\Core\Projection\ProjectionCatchUpTriggerInterface;
use Neos\ContentRepository\Core\Projection\Projections;
use Neos\ContentRepository\Core\SharedModel\Experimental\ContentRepositoryExperiments;
use Neos\ContentRepository\Core\SharedModel\User\UserIdProviderInterface;
use Neos\EventStore\EventStoreInterface;
use Psr\Clock\ClockInterface;
Expand All @@ -55,6 +56,7 @@ public function __construct(
private readonly ProjectionCatchUpTriggerInterface $projectionCatchUpTrigger,
private readonly UserIdProviderInterface $userIdProvider,
private readonly ClockInterface $clock,
private readonly ContentRepositoryExperiments $experiments,
) {
$contentDimensionZookeeper = new ContentDimensionZookeeper($contentDimensionSource);
$interDimensionalVariationGraph = new InterDimensionalVariationGraph(
Expand Down Expand Up @@ -138,7 +140,8 @@ private function buildCommandBus(): CommandBus
new WorkspaceCommandHandler(
$this->buildEventPersister(),
$this->projectionFactoryDependencies->eventStore,
$this->projectionFactoryDependencies->eventNormalizer
$this->projectionFactoryDependencies->eventNormalizer,
$this->experiments
),
new NodeAggregateCommandHandler(
$this->projectionFactoryDependencies->nodeTypeManager,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,33 @@ public function matchesNodeId(NodeIdToPublishOrDiscard $nodeIdToPublish): bool
&& $this->nodeAggregateId->equals($nodeIdToPublish->nodeAggregateId)
);
}

/**
* @internal
*/
public function appliesToSameNode(SetSerializedNodeProperties $command): bool
{
return (
$this->contentStreamId === $command->contentStreamId
&& $this->originDimensionSpacePoint->equals($command->originDimensionSpacePoint)
&& $this->nodeAggregateId->equals($command->nodeAggregateId)
);
}

/**
* @internal
*/
public function mergeWith(SetSerializedNodeProperties $command): self
{
if (!$this->appliesToSameNode($command)) {
throw new \InvalidArgumentException('command applies to different node, this is not supported');
}

return new self(
$this->contentStreamId,
$this->nodeAggregateId,
$this->originDimensionSpacePoint,
$this->propertyValues->merge($command->propertyValues)
);
}
}
Loading