-
-
Notifications
You must be signed in to change notification settings - Fork 222
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: Subscription Engine #5321
base: 9.0
Are you sure you want to change the base?
Conversation
# Conflicts: # Neos.ContentRepository.Core/Classes/ContentRepository.php
Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php
Outdated
Show resolved
Hide resolved
Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php
Outdated
Show resolved
Hide resolved
I had a quick look and id love to help with the wiring and sticking it all together :) We should probably really get a simple draft running of the catchups just getting their projection state first :) As that will require some work already |
…n they are registered for A catchup doesn't have access to the full content repository, as it would allow full recursion via handle and accessing other projections state is not safe as the other projection might not be behind - the order is undefined. This will make it possible to catchup projections from outside of the cr instance as proposed here: neos#5321
…n they are registered for A catchup doesn't have access to the full content repository, as it would allow full recursion via handle and accessing other projections state is not safe as the other projection might not be behind - the order is undefined. This will make it possible to catchup projections from outside of the cr instance as proposed here: neos#5321
Okay i thought further about our little content graph projection vs projection states vs event handlers dilemma and i think the solution is not necessary exposing just the in my eyes this object, being build by the content repository registry in some way, must reflect
that could look like: final readonly class ContentRepositoryGraphProjectionAndSubscribers
{
public function __construct(
public ContentGraphProjectionInterface $contentGraphProjection,
public Subscribers $subscribers, // must contain a subscriber for the $contentGraphProjection
public ProjectionStates $additionalProjectionStates, // must not contain the $contentGraphProjection state
) {
}
} or maybe a little more explicit so the factories dont have to deal with all the logic and we have control over the subscription ids: final readonly class ProjectionsAndCatchupHooksBetterVersionZwo
{
public function __construct(
public ContentGraphProjectionInterface $contentGraphProjection,
private Projections $additionalProjections,
private Subscribers $additionalSubscriber,
private array $catchUpHooksByProjectionClass
) {
}
public function getSubscribers(): Subscribers
{
$subscribers = iterator_to_array($this->additionalSubscriber);
$subscribers[] = new Subscriber(
SubscriptionId::fromString('contentGraphProjection'),
SubscriptionGroup::fromString('default'),
RunMode::FROM_BEGINNING,
new ProjectionEventHandler(
$this->contentGraphProjection,
$this->getCatchUpHooksForProjectionClass(ContentGraphProjectionInterface::class),
),
);
foreach ($this->additionalProjections as $projection) {
$subscribers[] = new Subscriber(
SubscriptionId::fromString(substr(strrchr($projection::class, '\\'), 1)),
SubscriptionGroup::fromString('default'),
RunMode::FROM_BEGINNING,
new ProjectionEventHandler(
$projection,
$this->getCatchUpHooksForProjectionClass($projection::class),
),
);
}
return Subscribers::fromArray($subscribers);
}
public function getAdditionalProjectionStates(): ProjectionStates
{
return ProjectionStates::fromArray(array_map(
fn ($projection) => $projection->getState(),
iterator_to_array($this->additionalProjections)
));
}
private function getCatchUpHooksForProjectionClass(string $projectionClass): ?CatchUpHookInterface
{
return $this->catchUpHooksByProjectionClass[$projectionClass] ?? null;
}
}
|
@mhsdesign thanks for your input! That's my current draft of the public function __construct(
private readonly ContentRepositoryId $contentRepositoryId,
EventStoreInterface $eventStore,
NodeTypeManager $nodeTypeManager,
ContentDimensionSourceInterface $contentDimensionSource,
Serializer $propertySerializer,
private readonly UserIdProviderInterface $userIdProvider,
private readonly ClockInterface $clock,
SubscriptionStoreInterface $subscriptionStore,
ContentGraphProjectionFactoryInterface $contentGraphProjectionFactory,
CatchUpHookFactoryInterface $contentGraphCatchUpHookFactory,
private readonly ContentRepositorySubscribersFactoryInterface $additionalSubscribersFactory,
) {
// ...
} |
As discussed that looks good ❤️ my idea had a flaw because i assumed the projection instance could be build by the registry which it CANNOT because we need factory dependencies.... and the thing with iterating over the event handlers to fetch their state via |
…n they are registered for A catchup doesn't have access to the full content repository, as it would allow full recursion via handle and accessing other projections state is not safe as the other projection might not be behind - the order is undefined. This will make it possible to catchup projections from outside of the cr instance as proposed here: neos/neos-development-collection#5321
…n they are registered for A catchup doesn't have access to the full content repository, as it would allow full recursion via handle and accessing other projections state is not safe as the other projection might not be behind - the order is undefined. This will make it possible to catchup projections from outside of the cr instance as proposed here: neos/neos-development-collection#5321
…n they are registered for A catchup doesn't have access to the full content repository, as it would allow full recursion via handle and accessing other projections state is not safe as the other projection might not be behind - the order is undefined. This will make it possible to catchup projections from outside of the cr instance as proposed here: neos/neos-development-collection#5321
Neos.ContentRepository.Core/Classes/Projection/CatchUpHook/CatchUpHookFactoryDependencies.php
Outdated
Show resolved
Hide resolved
# Conflicts: # Neos.ContentRepository.Core/Classes/ContentRepository.php # Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php # Neos.ContentRepositoryRegistry/Classes/ContentRepositoryRegistry.php # Neos.ContentRepositoryRegistry/Classes/Service/ProjectionService.php # Neos.ContentRepositoryRegistry/Classes/Service/ProjectionServiceFactory.php # phpstan-baseline.neon
No retry is simpler at first and its unlikely that a projection will fix itself.
#5321 (comment) > Anyways, I think with the removed retry strategy we should just get rid of the automatic retry altogether right now. It's quite unlikely that a retry suddenly works without other changes. So I'd be fully OK if it was only possible to manually retry failed subscriptions for 9.0
> there's a mismatch between variable name "additionalProjectionsFactory" and type "ContentRepositorySubsciberFactory" > the distinction between projection and subscription makes sense even if the only supported type of subscription target projections (for now) #5375 (review)
which lead to it being invoked on aktive > Subscriber "Vendor.Package:FakeProjection" could not invoke onBeforeCatchUp: Subscriber with the subscription id "Vendor.Package:FakeProjection" not found.
…cription-pr TASK: Radical cleanup for subscription pr to simplify and get into 9.0
Also allows the `Vendor.Package:FakeCatchupHook` to be picked up for testing
…eric `ProjectionStates`
Readded assertions as retry was removed for now ... and should probably NOT do anything if setup is called!
…rationIsRemoved > InvalidArgumentException: Subscriber with the subscription id "Vendor.Package:FakeProjection" not found. The subscriber is detached, so the state is not calculate-able at this point!
…terCatchUp failes as we consider it a critical developer error For `onBeforeCatchUp` we could probably wrap a savepoint and roll it back and skip also the projection, but errors in `onAfterCatchUp` would then analog also need to rollback only the one projection where it was registered. This is not possible and too complex. see \Neos\ContentRepository\BehavioralTests\Tests\Functional\Subscription\CatchUpHookErrorTest::error_onBeforeCatchUp_abortsCatchup
…ailure https://neos-project.slack.com/archives/C04PYL8H3/p1732318989845619 We dont want to rollback the main transaction, as other projections still need to be processed, the previously working events need to be applied, and we want to set the ERROR state of the projection
…the tables are dropped
To reduce additional sql query and lock, and do it in the main transaction
We do not expect any changes during runtime. Setup and status should handle this case.
a little update from my part: i spend a lot of time first getting a little bit of testcoverage before i trusted myself to do any core changes :D
bugs i discovered and fixed for the tests:
Deliberate behaviour changes / simplifications:
introduction of savepointsThe previous checkpoint implementation guaranteed an exactly once delivery. (implemented by using a transaction for each event for each projection in a for update lock) We still need this rollback behaviour: If a catchup fails on the content graph projection, it means the content graph projection is in error state but has the event applied and would get it after repairing the catchup again, that will not work for the projection. Currently the subscription engine uses a big transaction for all projections and to update the subscriptions at the end. We cannot roll that back. So we want the following things:
i implemented this with savepoints which would look like:
the exactly once delivery is well tested with my above tests. and looks like this on error:
Regarding onBeforeCatchUp and onAfterCatchUp, we cannot wrap these into a savepoint but will just throw hard and consider this a not forgive-able developer error. The whole transaction will be rolled back.: TASK: Throw CatchUpFailed exception in case onBeforeCatchUp or onAfterCatchUp failes followup: inconsistency of projection transaction vs subscription transaction.In this commit TASK: Use save points to rollback projections during transaction on failure i added methods on the subscription store to set the savepoints. This is not entirely correct as there is no rule that a projection and its store have to share the same dbal instance or even connection nor database. In the current state of 9.0 this is solved by using the pattern of a checkpoint table per projection, which would live in the same database and transaction. Instead for now it might be sensible to add a public function transactional(\Closure $fn): mixed
{
if ($this->dbal->isTransactionActive() === false) {
throw new \RuntimeException(sprintf('Cannot set savepoint for projection, because no transaction is active. SubscriptionEngine and Projection must use the same database connection.'));
}
$this->dbal->createSavepoint('PROJECTION');
try {
$result = $fn();
} catch (\Throwable $e) {
$this->dbal->rollbackSavepoint('PROJECTION');
throw $e;
}
$this->dbal->releaseSavepoint('PROJECTION');
return $result;
} mutable SubscriptionManagerregarding the mutable vs immutable subscription updating, i have a wip local which does simplify this.
as far as i can see its only important to reduce sql statements, but for the result it should not matter. In my case, marking the projection as failure happens directly, while the other active and position updates happen at the end. |
and failing test for setupIsInvokedForPreviouslyActiveSubscribers
…ark detached ones as detached.
@kitsunet and me agreed on the following: In order to get the basis of the change in we should keep the locking behaviour as it is now (meaning one transaction for each event for each projection) as this will allow discussing behaviour changes in a separate pull request and also if a simplification to reduce transactions brings really any performance. So this pr would only introduce the central registration (which allows to fetch the lowest position in one query) and leave us with the performance gain of only having to load the events one and not n times on replay. That means each projection will receive a function which either uses save points to emulate nested transactions, or if its another connection or database uses an own transaction: public function transactional(\Closure $fn): mixed
{
if ($this->dbal->isTransactionActive() === false) {
return $this->dbal->transactional($fn);
}
$this->dbal->createSavepoint('PROJECTION');
try {
$result = $fn();
} catch (\Throwable $e) {
$this->dbal->rollbackSavepoint('PROJECTION');
throw $e;
}
$this->dbal->releaseSavepoint('PROJECTION');
return $result;
} Our reasons for still replying so hard on exactly once delivery are that our projections simply are not build to ignore events twice. Until we fully agree that this is a good idea, and also find solutions to do the same for catchup hooks, and have test that prove that at least once delivery is okay in most cases, it would be not a good idea to introduce a change here before done the other. future followups
|
Related: #4746
Fixes:
ParallelWritingInWorkspaces
) was added which fails in 9.0. >Failed to acquire checkpoint lock for subscriber "Neos\ContentGraph\DoctrineDbalAdapter\DoctrineDbalContentGraphProjection" because it is acquired already
TODO
SubscriptionEngine::reset()
(i.e. projection replay)Neos\ContentRepository\Core\Subscription
enough?)