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

BUGFIX: Fix and test tethered node structure adjustment #4969

Open
wants to merge 7 commits into
base: 9.0
Choose a base branch
from

Conversation

mhsdesign
Copy link
Member

@mhsdesign mhsdesign commented Mar 30, 2024

I found out that this line

is totally untested. Thus i wanted to write a test.

This pr also adds causation ids and meta data for changes made by structure adjustments to easier debug them. See also #3887

Related: #4966
Related: #4832 (comment)

Upgrade instructions

Review instructions

Checklist

  • Code follows the PSR-2 coding style
  • Tests have been created, run and adjusted as needed
  • The PR is created against the lowest maintained branch
  • Reviewer - PR Title is brief but complete and starts with FEATURE|TASK|BUGFIX
  • Reviewer - The first section explains the change briefly for change-logs
  • Reviewer - Breaking Changes are marked with !!! and have upgrade-instructions

@mhsdesign
Copy link
Member Author

Currently fails with

RuntimeException: Failed to update and commit highest applied sequence number for subscriber "Neos\ContentGraph\DoctrineDbalAdapter\DoctrineDbalContentGraphProjection". Please run Neos\ContentRepository\Core\Infrastructure\DbalCheckpointStorage::setUp() in /Users/marchenryschultz/Code/core/neos-manufacture-90/Packages/Neos/Neos.ContentRepository.Core/Classes/Infrastructure/DbalCheckpointStorage.php:125

this is due to the blocking in $this->eventPersister->publishEvents. If we dont catchup everything is fine. So a projection problem?

break;
}
/** @var Node $childNodeSource Node aggregates are never empty */
$occupiedDimensionSpacePoints = $childNodeAggregate->occupiedDimensionSpacePoints->getPoints();
Copy link
Member Author

Choose a reason for hiding this comment

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

idk if we should guard here with this check ... the added tests will pass either way ;)

if (count($occupiedDimensionSpacePoints) !== 1) {
    throw new \RuntimeException('TODO: The ChildNodeAggregate occupies multiple origins.', 1711897662);
}

Copy link
Member

Choose a reason for hiding this comment

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

I don't see an issue with the child node aggregate occupying multiple DSPs, that's totally legit

@mhsdesign mhsdesign requested a review from nezaniel April 2, 2024 13:29
@mhsdesign mhsdesign changed the title TASK: Write test for tethered node structure adjustment BUGFIX: Fix and test tethered node structure adjustment Apr 3, 2024
@github-actions github-actions bot added the Bug label Apr 3, 2024
Copy link
Member

@nezaniel nezaniel left a comment

Choose a reason for hiding this comment

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

Needs to be synced with 9.0, looks otherwise fine so far

break;
}
/** @var Node $childNodeSource Node aggregates are never empty */
$occupiedDimensionSpacePoints = $childNodeAggregate->occupiedDimensionSpacePoints->getPoints();
Copy link
Member

Choose a reason for hiding this comment

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

I don't see an issue with the child node aggregate occupying multiple DSPs, that's totally legit

@mhsdesign
Copy link
Member Author

@mhsdesign mhsdesign requested a review from nezaniel April 27, 2024 08:56
@@ -33,4 +35,31 @@ public static function empty(): self
ExpectedVersion::ANY()
);
}

public function withCausationOfFirstEventAndAdditionalMetaData(EventMetadata $metadata): self
Copy link
Member Author

Choose a reason for hiding this comment

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

that might be something were i guess only @bwaidelich can say if this is a good idea.

I use this utility to tie all events together and add a description to the first event as to the why.

Copy link
Member

Choose a reason for hiding this comment

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

It's a good idea in general (thus #3887) but we have to get this right:
The first event is not really the cause for the other events but the command. Our commands don't have an id yet. We could actually change that since we persist the commands.
But instead we should IMO focus on the correlation id (so that all events of one transaction are inter-connected).
The causation id does not have to be a UUID – we could e.g. add some speaking prefix, for example the command type (see #3887 (comment))

Copy link
Member Author

Choose a reason for hiding this comment

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

okay thanks so i understand all should share one common correlationId (CorrelationId::fromString(UuidFactory::create()).

But additionally i want to add somewhere to the (first?) event (as part of metadata?) which structure adjustment caused the creation of this event.

So that the events read like:

Name correlation id metadata
NodeWasCreated 123 {structureAdjustment: 'Content Stream: %s; Dimension Space Point: %s, Node Aggregate: %s --- The tethered child node "bar" is missing.'}
SomethingLol 123

at first i tried to insert the rendered structure adjustment message in there but that wont work because its to long and special chars.

Copy link
Member

Choose a reason for hiding this comment

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

I see, that makes sense.
What about turning this method into

/**
 * @param \Closure(EventInterface|DecoratedEvent): array<mixed> $callback
 **/
public function mapEvents(\Closure $callback): array
{
    return new self(
        $this->streamName,
        $this->events->map($callback),
        $this->expectedVersion
    );
}

and then in the calling side below do sth. along the lines of

            $isFirstEvent = true;
            $enrichedEventsToPublish = $eventsToPublish->mapEvents(function (EventInterface|DecoratedEvent) use (&$isFirstEvent, $correlationId) {
                $metadata = null;
                if ($isFirstEvent) {
                    $metadata = $event instanceof DecoratedEvent && $event->metadata !== null ? $event->metadata->value : [];
                    $metadata['structureAdjustment'] =  mb_strimwidth($adjustment->render() , 0, 250, '…');
                    $isFirstEvent = false;
                }
                return DecoratedEvent::create(
                    event: $event,
                    correlationId: $correlationId,
                    metadata: $metadata,
                )
            );

ok, this is is not much cleaner. But it avoids turning iterators to arrays vice versa and is a bit less dependent to inner workings.

Copy link
Member

Choose a reason for hiding this comment

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

btw: you can probably omit the correlation id since we should add that within ContentRepository::handle() with #3887 nevermind, in this case that's not possible

Copy link
Member Author

Choose a reason for hiding this comment

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

&$isFirstEvent :D no okay fine with me ;)

Copy link
Member

Choose a reason for hiding this comment

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

"along the lines of" – there's probably more bugs in the dummy code above.

BTW: DecoratedEvent and EventMetadata is not easy to be used.. We could extend that (in the neos/eventstore package) a bit so that we could do

$metadata->with('newKey', $newValue);

so that we could simplify the above to sth like:

$decoratedEvent = DecoratedEvent::create(
    event: $event,
    correlationId: $correlationId,
);
if ($isFirstEvent) {
    $decoratedEvent = $decoratedEvent->withAddedMetadata('structureAdjustment', mb_strimwidth($adjustment->render() , 0, 250, '…'));
    $isFirstEvent = false;
}
return $decoratedEvent;

Copy link
Member

@bwaidelich bwaidelich left a comment

Choose a reason for hiding this comment

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

Just some initial thoughts on correlation and causation ids, happy to discuss this further!

@@ -33,4 +35,31 @@ public static function empty(): self
ExpectedVersion::ANY()
);
}

public function withCausationOfFirstEventAndAdditionalMetaData(EventMetadata $metadata): self
Copy link
Member

Choose a reason for hiding this comment

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

It's a good idea in general (thus #3887) but we have to get this right:
The first event is not really the cause for the other events but the command. Our commands don't have an id yet. We could actually change that since we persist the commands.
But instead we should IMO focus on the correlation id (so that all events of one transaction are inter-connected).
The causation id does not have to be a UUID – we could e.g. add some speaking prefix, for example the command type (see #3887 (comment))

Comment on lines +102 to +106
$enrichedEventsToPublish = $eventsToPublish->withCausationOfFirstEventAndAdditionalMetaData(
EventMetadata::fromArray([
'structureAdjustment' => mb_strimwidth($adjustment->render() , 0, 250, '…')
])
);
Copy link
Member

Choose a reason for hiding this comment

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

IMO this should look something like:

Suggested change
$enrichedEventsToPublish = $eventsToPublish->withCausationOfFirstEventAndAdditionalMetaData(
EventMetadata::fromArray([
'structureAdjustment' => mb_strimwidth($adjustment->render() , 0, 250, '…')
])
);
$enrichedEventsToPublish = $eventsToPublish->withCorrelationId(CorrelationId::fromString(UuidFactory::create());

Comment on lines +41 to +63
/** @var list<EventInterface|DecoratedEvent> $restEvents */
$restEvents = iterator_to_array($this->events);
if (empty($restEvents)) {
return $this;
}
$firstEvent = array_shift($restEvents);

if ($firstEvent instanceof DecoratedEvent && $firstEvent->eventMetadata) {
$metadata = EventMetadata::fromArray(array_merge($firstEvent->eventMetadata->value, $metadata->value));
}

$decoratedFirstEvent = DecoratedEvent::create($firstEvent, eventId: Event\EventId::create(), metadata: $metadata);

$decoratedRestEvents = [];
foreach ($restEvents as $event) {
$decoratedRestEvents[] = DecoratedEvent::create($event, causationId: $decoratedFirstEvent->eventId);
}

return new EventsToPublish(
$this->streamName,
Events::fromArray([$decoratedFirstEvent, ...$decoratedRestEvents]),
$this->expectedVersion
);
Copy link
Member

Choose a reason for hiding this comment

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

That's quite a lot of complexity. I don't really think that we need to handle the first event differently from the others in this case (see suggestion below) so maybe we can simplify this to a one-liner like:

return new self(
    $this->streamName,
    $this->events->map(fn (Event $event) => DecoratedEvent::create(
        event: $event,
        correlationId: $correlationId,
    ),
    $this->expectedVersion,
);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants