diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 74d6e20291e..2fe858e44e8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -180,11 +180,9 @@ jobs: # We enable the race condition tracker presets: 'default': - projections: - 'Neos.ContentRepository:ContentGraph': - catchUpHooks: - 'Neos.ContentRepository.BehavioralTests:RaceConditionTracker': - factoryObjectName: Neos\ContentRepository\BehavioralTests\ProjectionRaceConditionTester\RaceTrackerCatchUpHookFactory + hooks: + 'Neos.ContentRepository.BehavioralTests:RaceConditionTracker': + factoryObjectName: Neos\ContentRepository\BehavioralTests\ProjectionRaceConditionTester\RaceTrackerContentRepositoryHookFactory ContentRepository: BehavioralTests: raceConditionTracker: diff --git a/Neos.ContentRepository.BehavioralTests/Classes/Command/PerformanceMeasurementCommandController.php b/Neos.ContentRepository.BehavioralTests/Classes/Command/PerformanceMeasurementCommandController.php index a34583a39e7..be8a194d3db 100644 --- a/Neos.ContentRepository.BehavioralTests/Classes/Command/PerformanceMeasurementCommandController.php +++ b/Neos.ContentRepository.BehavioralTests/Classes/Command/PerformanceMeasurementCommandController.php @@ -17,7 +17,7 @@ use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Cli\CommandController; -use Neos\Neos\Fusion\Cache\GraphProjectorCatchUpHookForCacheFlushing; +use Neos\Neos\Fusion\Cache\ContentRepositoryHookForCacheFlushing; final class PerformanceMeasurementCommandController extends CommandController { @@ -46,7 +46,7 @@ public function preparePerformanceTestCommand(int $nodesPerLevel, int $levels): { $this->performanceMeasurementService->removeEverything(); $this->outputLine("All removed. Starting to fill."); - GraphProjectorCatchUpHookForCacheFlushing::disabled( + ContentRepositoryHookForCacheFlushing::disabled( fn() => $this->performanceMeasurementService->createNodesForPerformanceTest($nodesPerLevel, $levels) ); } diff --git a/Neos.ContentRepository.BehavioralTests/Classes/Command/PerformanceMeasurementService.php b/Neos.ContentRepository.BehavioralTests/Classes/Command/PerformanceMeasurementService.php index 5e8caf7fb2e..1528641fe01 100644 --- a/Neos.ContentRepository.BehavioralTests/Classes/Command/PerformanceMeasurementService.php +++ b/Neos.ContentRepository.BehavioralTests/Classes/Command/PerformanceMeasurementService.php @@ -32,6 +32,7 @@ use Neos\ContentRepository\Core\Feature\RootNodeCreation\Event\RootNodeAggregateWithNodeWasCreated; use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateRootWorkspace; use Neos\ContentRepository\Core\NodeType\NodeTypeName; +use Neos\ContentRepository\Core\Projection\Projections; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateClassification; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; @@ -53,7 +54,8 @@ public function __construct( private readonly EventPersister $eventPersister, private readonly ContentRepository $contentRepository, private readonly Connection $dbal, - private readonly ContentRepositoryId $contentRepositoryId + private readonly ContentRepositoryId $contentRepositoryId, + private readonly Projections $projections, ) { $this->contentStreamId = contentStreamId::fromString('cs-identifier'); $this->workspaceName = WorkspaceName::fromString('some-workspace'); @@ -74,7 +76,7 @@ public function removeEverything(): void { $eventTableName = DoctrineEventStoreFactory::databaseTableName($this->contentRepositoryId); $this->dbal->executeStatement('TRUNCATE ' . $this->dbal->quoteIdentifier($eventTableName)); - $this->contentRepository->resetProjectionStates(); + $this->projections->resetAll(); } public function createNodesForPerformanceTest(int $nodesPerLevel, int $levels): void diff --git a/Neos.ContentRepository.BehavioralTests/Classes/Command/PerformanceMeasurementServiceFactory.php b/Neos.ContentRepository.BehavioralTests/Classes/Command/PerformanceMeasurementServiceFactory.php index a4a9e11a16a..56660584a5a 100644 --- a/Neos.ContentRepository.BehavioralTests/Classes/Command/PerformanceMeasurementServiceFactory.php +++ b/Neos.ContentRepository.BehavioralTests/Classes/Command/PerformanceMeasurementServiceFactory.php @@ -36,7 +36,8 @@ public function build( $serviceFactoryDependencies->eventPersister, $serviceFactoryDependencies->contentRepository, $this->dbal, - $serviceFactoryDependencies->contentRepositoryId + $serviceFactoryDependencies->contentRepositoryId, + $serviceFactoryDependencies->projections, ); } } diff --git a/Neos.ContentRepository.BehavioralTests/Classes/ProjectionRaceConditionTester/Command/RaceConditionTrackerCommandController.php b/Neos.ContentRepository.BehavioralTests/Classes/ProjectionRaceConditionTester/Command/RaceConditionTrackerCommandController.php index f78a816b18b..b12dba10ece 100644 --- a/Neos.ContentRepository.BehavioralTests/Classes/ProjectionRaceConditionTester/Command/RaceConditionTrackerCommandController.php +++ b/Neos.ContentRepository.BehavioralTests/Classes/ProjectionRaceConditionTester/Command/RaceConditionTrackerCommandController.php @@ -23,7 +23,7 @@ use Symfony\Component\Console\Helper\Table; /** - * For full docs and context, see {@see RaceTrackerCatchUpHook} + * For full docs and context, see {@see RaceTrackerContentRepositoryHook} * * @internal */ diff --git a/Neos.ContentRepository.BehavioralTests/Classes/ProjectionRaceConditionTester/Dto/TraceEntries.php b/Neos.ContentRepository.BehavioralTests/Classes/ProjectionRaceConditionTester/Dto/TraceEntries.php index dec33e98921..3c053035c51 100644 --- a/Neos.ContentRepository.BehavioralTests/Classes/ProjectionRaceConditionTester/Dto/TraceEntries.php +++ b/Neos.ContentRepository.BehavioralTests/Classes/ProjectionRaceConditionTester/Dto/TraceEntries.php @@ -19,7 +19,7 @@ /** * Value object for a list of {@see TraceEntry} objects, as stored in-order in the Redis stream * - * For full docs and context, see {@see RaceTrackerCatchUpHook} + * For full docs and context, see {@see RaceTrackerContentRepositoryHook} * * @internal * @implements \ArrayAccess diff --git a/Neos.ContentRepository.BehavioralTests/Classes/ProjectionRaceConditionTester/Dto/TraceEntry.php b/Neos.ContentRepository.BehavioralTests/Classes/ProjectionRaceConditionTester/Dto/TraceEntry.php index 46f86db202c..55b3b0ccab5 100644 --- a/Neos.ContentRepository.BehavioralTests/Classes/ProjectionRaceConditionTester/Dto/TraceEntry.php +++ b/Neos.ContentRepository.BehavioralTests/Classes/ProjectionRaceConditionTester/Dto/TraceEntry.php @@ -16,14 +16,14 @@ namespace Neos\ContentRepository\BehavioralTests\ProjectionRaceConditionTester\Dto; -use Neos\ContentRepository\BehavioralTests\ProjectionRaceConditionTester\RaceTrackerCatchUpHook; +use Neos\ContentRepository\BehavioralTests\ProjectionRaceConditionTester\RaceTrackerContentRepositoryHook; use Symfony\Component\Console\Helper\TableCell; use Symfony\Component\Console\Helper\TableCellStyle; /** * Value object for a single trace entry, as stored in Redis. * - * For full docs and context, see {@see RaceTrackerCatchUpHook} + * For full docs and context, see {@see RaceTrackerContentRepositoryHook} * * @internal */ diff --git a/Neos.ContentRepository.BehavioralTests/Classes/ProjectionRaceConditionTester/Dto/TraceEntryType.php b/Neos.ContentRepository.BehavioralTests/Classes/ProjectionRaceConditionTester/Dto/TraceEntryType.php index 8257b24ddd4..94cb92425c3 100644 --- a/Neos.ContentRepository.BehavioralTests/Classes/ProjectionRaceConditionTester/Dto/TraceEntryType.php +++ b/Neos.ContentRepository.BehavioralTests/Classes/ProjectionRaceConditionTester/Dto/TraceEntryType.php @@ -17,7 +17,7 @@ namespace Neos\ContentRepository\BehavioralTests\ProjectionRaceConditionTester\Dto; /** - * For full docs and context, see {@see RaceTrackerCatchUpHook} + * For full docs and context, see {@see RaceTrackerContentRepositoryHook} * * @internal */ diff --git a/Neos.ContentRepository.BehavioralTests/Classes/ProjectionRaceConditionTester/RaceTrackerCatchUpHook.php b/Neos.ContentRepository.BehavioralTests/Classes/ProjectionRaceConditionTester/RaceTrackerContentRepositoryHook.php similarity index 89% rename from Neos.ContentRepository.BehavioralTests/Classes/ProjectionRaceConditionTester/RaceTrackerCatchUpHook.php rename to Neos.ContentRepository.BehavioralTests/Classes/ProjectionRaceConditionTester/RaceTrackerContentRepositoryHook.php index 97ce6721535..6e1dfff081a 100644 --- a/Neos.ContentRepository.BehavioralTests/Classes/ProjectionRaceConditionTester/RaceTrackerCatchUpHook.php +++ b/Neos.ContentRepository.BehavioralTests/Classes/ProjectionRaceConditionTester/RaceTrackerContentRepositoryHook.php @@ -17,7 +17,7 @@ use Neos\ContentRepository\BehavioralTests\ProjectionRaceConditionTester\Dto\TraceEntries; use Neos\ContentRepository\BehavioralTests\ProjectionRaceConditionTester\Dto\TraceEntryType; use Neos\ContentRepository\Core\EventStore\EventInterface; -use Neos\ContentRepository\Core\Projection\CatchUpHookInterface; +use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryHookInterface; use Neos\EventStore\Model\EventEnvelope; use Neos\Flow\Annotations as Flow; @@ -68,7 +68,7 @@ * * ## Implementation Idea: Race Detector with Redis * - * We implement a custom CatchUpHook (this class {@see RaceTrackerCatchUpHook}) which is notified during + * We implement a custom ContentRepositoryHook (this class {@see RaceTrackerContentRepositoryHook}) which is notified during * the projection run. * * When {@see onBeforeEvent} is called, we know that we are inside applyEvent() in the diagram above, @@ -98,7 +98,7 @@ * * @internal */ -final class RaceTrackerCatchUpHook implements CatchUpHookInterface +final class RaceTrackerContentRepositoryHook implements ContentRepositoryHookInterface { /** * @Flow\InjectConfiguration("raceConditionTracker") @@ -107,12 +107,12 @@ final class RaceTrackerCatchUpHook implements CatchUpHookInterface protected $configuration; private bool $inCriticalSection = false; - public function onBeforeCatchUp(): void + public function onBeforeEvents(): void { RedisInterleavingLogger::connect($this->configuration['redis']['host'], $this->configuration['redis']['port']); } - public function onBeforeEvent(EventInterface $eventInstance, EventEnvelope $eventEnvelope): void + public function onBeforeEvent(EventInterface $event, EventEnvelope $eventEnvelope): void { $this->inCriticalSection = true; RedisInterleavingLogger::trace(TraceEntryType::InCriticalSection, [ @@ -122,11 +122,11 @@ public function onBeforeEvent(EventInterface $eventInstance, EventEnvelope $even ]); } - public function onAfterEvent(EventInterface $eventInstance, EventEnvelope $eventEnvelope): void + public function onAfterEvent(EventInterface $event, EventEnvelope $eventEnvelope): void { } - public function onBeforeBatchCompleted(): void + public function onAfterEvents(): void { // we only want to track relevant lock release calls (i.e. if we were in the event processing loop before) if ($this->inCriticalSection) { @@ -134,8 +134,4 @@ public function onBeforeBatchCompleted(): void RedisInterleavingLogger::trace(TraceEntryType::LockWillBeReleasedIfItWasAcquiredBefore); } } - - public function onAfterCatchUp(): void - { - } } diff --git a/Neos.ContentRepository.BehavioralTests/Classes/ProjectionRaceConditionTester/RaceTrackerCatchUpHookFactory.php b/Neos.ContentRepository.BehavioralTests/Classes/ProjectionRaceConditionTester/RaceTrackerContentRepositoryHookFactory.php similarity index 53% rename from Neos.ContentRepository.BehavioralTests/Classes/ProjectionRaceConditionTester/RaceTrackerCatchUpHookFactory.php rename to Neos.ContentRepository.BehavioralTests/Classes/ProjectionRaceConditionTester/RaceTrackerContentRepositoryHookFactory.php index 30467e9dd48..3a513f5ae3b 100644 --- a/Neos.ContentRepository.BehavioralTests/Classes/ProjectionRaceConditionTester/RaceTrackerCatchUpHookFactory.php +++ b/Neos.ContentRepository.BehavioralTests/Classes/ProjectionRaceConditionTester/RaceTrackerContentRepositoryHookFactory.php @@ -15,18 +15,17 @@ namespace Neos\ContentRepository\BehavioralTests\ProjectionRaceConditionTester; use Neos\ContentRepository\Core\ContentRepository; -use Neos\ContentRepository\Core\Projection\CatchUpHookFactoryInterface; -use Neos\ContentRepository\Core\Projection\CatchUpHookInterface; +use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryHookFactoryInterface; /** - * For full docs and context, see {@see RaceTrackerCatchUpHook} + * For full docs and context, see {@see RaceTrackerContentRepositoryHook} * * @internal */ -final class RaceTrackerCatchUpHookFactory implements CatchUpHookFactoryInterface +final class RaceTrackerContentRepositoryHookFactory implements ContentRepositoryHookFactoryInterface { - public function build(ContentRepository $contentRepository): CatchUpHookInterface + public function build(ContentRepository $contentRepository, array $options): RaceTrackerContentRepositoryHook { - return new RaceTrackerCatchUpHook(); + return new RaceTrackerContentRepositoryHook(); } } diff --git a/Neos.ContentRepository.BehavioralTests/Classes/ProjectionRaceConditionTester/RedisInterleavingLogger.php b/Neos.ContentRepository.BehavioralTests/Classes/ProjectionRaceConditionTester/RedisInterleavingLogger.php index 1d10901a255..8e4d2b60818 100644 --- a/Neos.ContentRepository.BehavioralTests/Classes/ProjectionRaceConditionTester/RedisInterleavingLogger.php +++ b/Neos.ContentRepository.BehavioralTests/Classes/ProjectionRaceConditionTester/RedisInterleavingLogger.php @@ -21,7 +21,7 @@ use Neos\ContentRepository\BehavioralTests\ProjectionRaceConditionTester\Dto\TraceEntryType; /** - * For full docs and context, see {@see RaceTrackerCatchUpHook} + * For full docs and context, see {@see RaceTrackerContentRepositoryHook} * * @internal */ diff --git a/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/Behavior/CRBehavioralTestsSubjectProvider.php b/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/Behavior/CRBehavioralTestsSubjectProvider.php index 19eb183ff16..85772d33a28 100644 --- a/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/Behavior/CRBehavioralTestsSubjectProvider.php +++ b/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/Behavior/CRBehavioralTestsSubjectProvider.php @@ -18,6 +18,8 @@ use Behat\Gherkin\Node\TableNode; use Doctrine\DBAL\Connection; use Neos\ContentRepository\Core\ContentRepository; +use Neos\ContentRepository\Core\Service\ProjectionService; +use Neos\ContentRepository\Core\Service\ProjectionServiceFactory; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Helpers\GherkinTableNodeBasedContentDimensionSource; use Neos\EventStore\EventStoreInterface; @@ -163,7 +165,7 @@ protected function setUpContentRepository(ContentRepositoryId $contentRepository * * This was an actual bug which bit us and made our tests unstable :D :D * - * How did we find this? By the virtue of our Race Tracker (Docs: see {@see RaceTrackerCatchUpHook}), which + * How did we find this? By the virtue of our Race Tracker (Docs: see {@see RaceTrackerContentRepositoryHook}), which * checks for events being applied multiple times to a projection. * ... and additionally by using {@see logToRaceConditionTracker()} to find the interleavings between the * Catch Up process and the testcase reset. @@ -179,7 +181,9 @@ protected function setUpContentRepository(ContentRepositoryId $contentRepository $databaseConnection = (new \ReflectionClass($eventStore))->getProperty('connection')->getValue($eventStore); $eventTableName = sprintf('cr_%s_events', $contentRepositoryId->value); $databaseConnection->executeStatement('TRUNCATE ' . $eventTableName); - $contentRepository->resetProjectionStates(); + /** @var ProjectionService $projectionService */ + $projectionService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->getObject(ProjectionServiceFactory::class)); + $projectionService->resetAllProjections(); return $contentRepository; } diff --git a/Neos.ContentRepository.BehavioralTests/Configuration/Settings.yaml b/Neos.ContentRepository.BehavioralTests/Configuration/Settings.yaml index f5877126acc..918405b0a3e 100644 --- a/Neos.ContentRepository.BehavioralTests/Configuration/Settings.yaml +++ b/Neos.ContentRepository.BehavioralTests/Configuration/Settings.yaml @@ -5,11 +5,9 @@ Neos: #ContentRepositoryRegistry: # presets: # 'default': - # projections: - # 'Neos.ContentRepository:ContentGraph': - # catchUpHooks: - # 'Neos.ContentRepository.BehavioralTests:RaceConditionTracker': - # factoryObjectName: Neos\ContentRepository\BehavioralTests\ProjectionRaceConditionTester\RaceTrackerCatchUpHookFactory + # hooks: + # 'Neos.ContentRepository.BehavioralTests:RaceConditionTracker': + # factoryObjectName: Neos\ContentRepository\BehavioralTests\ProjectionRaceConditionTester\RaceTrackerContentRepositoryHookFactory ContentRepository: BehavioralTests: diff --git a/Neos.ContentRepository.BehavioralTests/deployment/neos-in-docker/Configuration/Testing/Behat/Settings.yaml b/Neos.ContentRepository.BehavioralTests/deployment/neos-in-docker/Configuration/Testing/Behat/Settings.yaml index 32051f54db4..32c1b5f370b 100644 --- a/Neos.ContentRepository.BehavioralTests/deployment/neos-in-docker/Configuration/Testing/Behat/Settings.yaml +++ b/Neos.ContentRepository.BehavioralTests/deployment/neos-in-docker/Configuration/Testing/Behat/Settings.yaml @@ -25,14 +25,12 @@ Neos: password: 'db' charset: 'UTF8' - # We enable the race condition tracker. For details on how this works, see RaceTrackerCatchUpHook.php + # We enable the race condition tracker. For details on how this works, see RaceTrackerContentRepositoryHook.php presets: 'default': - projections: - 'Neos.ContentRepository:ContentGraph': - catchUpHooks: - 'Neos.ContentRepository.BehavioralTests:RaceConditionTracker': - factoryObjectName: Neos\ContentRepository\BehavioralTests\ProjectionRaceConditionTester\RaceTrackerCatchUpHookFactory + hooks: + 'Neos.ContentRepository.BehavioralTests:RaceConditionTracker': + factoryObjectName: Neos\ContentRepository\BehavioralTests\ProjectionRaceConditionTester\RaceTrackerContentRepositoryHookFactory ContentRepository: BehavioralTests: raceConditionTracker: diff --git a/Neos.ContentRepository.BehavioralTests/docker-compose-full.yml b/Neos.ContentRepository.BehavioralTests/docker-compose-full.yml index c22bd28f30f..39fb3663ae4 100644 --- a/Neos.ContentRepository.BehavioralTests/docker-compose-full.yml +++ b/Neos.ContentRepository.BehavioralTests/docker-compose-full.yml @@ -13,7 +13,7 @@ # FLOW_CONTEXT=Testing/Behat ../../../../../flow raceConditionTracker:analyzeTrace # docker compose --project-directory . --file Packages/Neos/Neos.ContentRepository.BehavioralTests/docker-compose-full.yml down -v # -# For details on how the race condition tracker works, see RaceTrackerCatchUpHook.php +# For details on how the race condition tracker works, see RaceTrackerContentRepositoryHook.php # # Note: the context of this dockerfile is the ROOT directory of the Flow Distribution (FLOW_PATH_ROOT) version: '3.5' @@ -66,7 +66,7 @@ services: - 15432:5432 # for running race-condition tests, we use Redis as a single-threaded time store. - # For details on how this works, see RaceTrackerCatchUpHook.php + # For details on how this works, see RaceTrackerContentRepositoryHook.php redis: image: redis:7.0.4 ports: diff --git a/Neos.ContentRepository.Core/Classes/ContentRepository.php b/Neos.ContentRepository.Core/Classes/ContentRepository.php index 40773e4629d..585bcd8b0b1 100644 --- a/Neos.ContentRepository.Core/Classes/ContentRepository.php +++ b/Neos.ContentRepository.Core/Classes/ContentRepository.php @@ -26,14 +26,13 @@ use Neos\ContentRepository\Core\EventStore\EventsToPublish; use Neos\ContentRepository\Core\Factory\ContentRepositoryFactory; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; -use Neos\ContentRepository\Core\Projection\CatchUpHookInterface; -use Neos\ContentRepository\Core\Projection\CatchUpOptions; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphInterface; use Neos\ContentRepository\Core\Projection\ContentStream\ContentStreamFinder; -use Neos\ContentRepository\Core\Projection\ProjectionInterface; -use Neos\ContentRepository\Core\Projection\ProjectionsAndCatchUpHooks; +use Neos\ContentRepository\Core\Projection\Projections; use Neos\ContentRepository\Core\Projection\ProjectionStateInterface; +use Neos\ContentRepository\Core\Projection\ProjectionStatus; use Neos\ContentRepository\Core\Projection\ProjectionStatuses; +use Neos\ContentRepository\Core\Projection\ProjectionStatusType; use Neos\ContentRepository\Core\Projection\Workspace\WorkspaceFinder; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryStatus; @@ -43,11 +42,8 @@ use Neos\EventStore\EventStoreInterface; use Neos\EventStore\Model\Event\EventMetadata; use Neos\EventStore\Model\Event\SequenceNumber; -use Neos\EventStore\Model\EventEnvelope; use Neos\EventStore\Model\EventStream\VirtualStreamName; use Psr\Clock\ClockInterface; -use Symfony\Component\Lock\LockFactory; -use Symfony\Component\Lock\Store\SemaphoreStore; /** * Main Entry Point to the system. Encapsulates the full event-sourced Content Repository. @@ -56,7 +52,6 @@ * - set up the necessary database tables and contents via {@see ContentRepository::setUp()} * - send commands to the system (to mutate state) via {@see ContentRepository::handle()} * - access projection state (to read state) via {@see ContentRepository::projectionState()} - * - catch up projections via {@see ContentRepository::catchUpProjection()} * * @api */ @@ -77,8 +72,7 @@ public function __construct( public readonly ContentRepositoryId $id, private readonly CommandBus $commandBus, private readonly EventStoreInterface $eventStore, - private readonly ProjectionsAndCatchUpHooks $projectionsAndCatchUpHooks, - private readonly EventNormalizer $eventNormalizer, + private readonly Projections $projections, private readonly EventPersister $eventPersister, private readonly NodeTypeManager $nodeTypeManager, private readonly InterDimensionalVariationGraph $variationGraph, @@ -154,7 +148,7 @@ public function projectionState(string $projectionStateClassName): ProjectionSta $projectionState = $this->projectionStateCache[$projectionStateClassName]; return $projectionState; } - foreach ($this->projectionsAndCatchUpHooks->projections as $projection) { + foreach ($this->projections as $projection) { $projectionState = $projection->getState(); if ($projectionState instanceof $projectionStateClassName) { $this->projectionStateCache[$projectionStateClassName] = $projectionState; @@ -164,101 +158,10 @@ public function projectionState(string $projectionStateClassName): ProjectionSta throw new \InvalidArgumentException(sprintf('A projection state of type "%s" is not registered in this content repository instance.', $projectionStateClassName), 1662033650); } - public function catchUpProjections(): void - { - $store = new SemaphoreStore(); - $factory = new LockFactory($store); - $lock = $factory->createLock('catchup'); - $lock->acquire(true); - #print_r(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)); - $lowestAppliedSequenceNumber = null; - - /** @var array, sequenceNumber: SequenceNumber, catchUpHook: CatchUpHookInterface|null}> $projectionsAndCatchUpHooks */ - $projectionsAndCatchUpHooks = []; - foreach ($this->projectionsAndCatchUpHooks->projections as $projectionClassName => $projection) { - $projectionSequenceNumber = $projection->getCheckpoint(); - #\Neos\Flow\var_dump('ACQUIRE LOCK FOR ' . $projectionClassName . ': ' . $projectionSequenceNumber->value); - if ($lowestAppliedSequenceNumber === null || $projectionSequenceNumber->value < $lowestAppliedSequenceNumber->value) { - $lowestAppliedSequenceNumber = $projectionSequenceNumber; - } - $catchUpHookFactory = $this->projectionsAndCatchUpHooks->getCatchUpHookFactoryForProjection($projection); - $projectionsAndCatchUpHooks[$projectionClassName] = [ - 'projection' => $projection, - 'sequenceNumber' => $projectionSequenceNumber, - 'catchUpHook' => $catchUpHookFactory?->build($this), - ]; - $projectionsAndCatchUpHooks[$projectionClassName]['catchUpHook']?->onBeforeCatchUp(); - } - #\Neos\Flow\var_dump('CATCHUP from ' . $lowestAppliedSequenceNumber->value); - assert($lowestAppliedSequenceNumber instanceof SequenceNumber); - $eventStream = $this->eventStore->load(VirtualStreamName::all())->withMinimumSequenceNumber($lowestAppliedSequenceNumber->next()); - $eventEnvelope = null; - foreach ($eventStream as $eventEnvelope) { - #\Neos\Flow\var_dump('EVENT ' . $eventEnvelope->event->type->value); - $event = $this->eventNormalizer->denormalize($eventEnvelope->event); - /** @var array{projection: ProjectionInterface, sequenceNumber: SequenceNumber, catchUpHook: CatchUpHookInterface|null} $projectionAndCatchUpHook */ - foreach ($projectionsAndCatchUpHooks as $projectionClassName => $projectionAndCatchUpHook) { - if ($projectionAndCatchUpHook['sequenceNumber']->value >= $eventEnvelope->sequenceNumber->value) { - continue; - } - #$projectionAndCatchUpHook['catchUpHook']?->onBeforeEvent($event, $eventEnvelope); - #\Neos\Flow\var_dump('APPLY ' . $eventEnvelope->event->type->value . ' (' . $eventEnvelope->sequenceNumber->value . ') TO ' . $projectionClassName); - #$futures[] = function () => $projectionAndCatchUpHook['projection']->apply($event, $eventEnvelope); - #$projectionAndCatchUpHook['catchUpHook']?->onAfterEvent($event, $eventEnvelope); - $projectionAndCatchUpHook['catchUpHook']?->onBeforeEvent($event, $eventEnvelope); - #\Neos\Flow\var_dump('APPLY ' . $eventEnvelope->event->type->value . ' (' . $eventEnvelope->sequenceNumber->value . ') TO ' . $projectionClassName); - $projectionAndCatchUpHook['projection']->apply($event, $eventEnvelope); - $projectionAndCatchUpHook['catchUpHook']?->onAfterEvent($event, $eventEnvelope); - #$projectionAndCatchUpHook['projection']->getCheckpointStorage()->updateAndReleaseLock($eventEnvelope->sequenceNumber); - #\Neos\Flow\var_dump('UPDATE CHECKPOINT for ' . $projectionClassName . ' to ' . $eventEnvelope->sequenceNumber->value); - } - } - assert($eventEnvelope instanceof EventEnvelope); - /** @var array{projection: ProjectionInterface, sequenceNumber: SequenceNumber, catchUpHook: CatchUpHookInterface|null} $projectionAndCatchUpHook */ - foreach ($projectionsAndCatchUpHooks as $projectionClassName => $projectionAndCatchUpHook) { - $projectionAndCatchUpHook['catchUpHook']?->onBeforeBatchCompleted(); - #$projectionAndCatchUpHook['projection']->getCheckpointStorage()->updateAndReleaseLock($eventEnvelope->sequenceNumber); - #\Neos\Flow\var_dump('UPDATE SQN FOR ' . $projectionClassName . ': ' . $eventEnvelope->sequenceNumber->value); - #\Neos\Flow\var_dump('UPDATE CHECKPOINT for ' . $projectionClassName . ' to ' . $eventEnvelope->sequenceNumber->value); - $projectionAndCatchUpHook['catchUpHook']?->onAfterCatchUp(); - } - $lock->release(); - } - - /** - * @param class-string> $projectionClassName - */ - public function catchUpProjection(string $projectionClassName, CatchUpOptions $options): void - { - $projection = $this->projectionsAndCatchUpHooks->projections->get($projectionClassName); - - $catchUpHookFactory = $this->projectionsAndCatchUpHooks->getCatchUpHookFactoryForProjection($projection); - $catchUpHook = $catchUpHookFactory?->build($this); - - $streamName = VirtualStreamName::all(); - $eventStream = $this->eventStore->load($streamName); - if ($options->maximumSequenceNumber !== null) { - $eventStream = $eventStream->withMaximumSequenceNumber($options->maximumSequenceNumber); - } - foreach ($eventStream as $eventEnvelope) { - $event = $this->eventNormalizer->denormalize($eventEnvelope->event); - if ($options->progressCallback !== null) { - ($options->progressCallback)($event, $eventEnvelope); - } - $catchUpHook?->onBeforeEvent($event, $eventEnvelope); - $projection->apply($event, $eventEnvelope); - // TODO this should happen in the inner transaction - $catchUpHook?->onBeforeBatchCompleted(); - $catchUpHook?->onAfterEvent($event, $eventEnvelope); - } - $catchUpHook?->onAfterCatchUp(); - } - - public function setUp(): void { $this->eventStore->setup(); - foreach ($this->projectionsAndCatchUpHooks->projections as $projection) { + foreach ($this->projections as $projection) { $projection->setUp(); } } @@ -266,8 +169,21 @@ public function setUp(): void public function status(): ContentRepositoryStatus { $projectionStatuses = ProjectionStatuses::create(); - foreach ($this->projectionsAndCatchUpHooks->projections as $projectionClassName => $projection) { - $projectionStatuses = $projectionStatuses->with($projectionClassName, $projection->status()); + $expectedCheckpoint = SequenceNumber::none(); + /** @noinspection LoopWhichDoesNotLoopInspection */ + foreach ($this->eventStore->load(VirtualStreamName::all())->backwards()->limit(1) as $eventEnvelope) { + $expectedCheckpoint = $eventEnvelope->sequenceNumber; + break; + } + foreach ($this->projections as $projectionClassName => $projection) { + $projectionStatus = $projection->status(); + if ($projectionStatus->type === ProjectionStatusType::OK) { + $projectionCheckpoint = $projection->getCheckpoint(); + if (!$projectionCheckpoint->equals($expectedCheckpoint)) { + $projectionStatus = ProjectionStatus::catchupRequired(sprintf('projection is at checkpoint %d of %d', $projectionCheckpoint->value, $expectedCheckpoint->value)); + } + } + $projectionStatuses = $projectionStatuses->with($projectionClassName, $projectionStatus); } return new ContentRepositoryStatus( $this->eventStore->status(), @@ -275,22 +191,6 @@ public function status(): ContentRepositoryStatus ); } - public function resetProjectionStates(): void - { - foreach ($this->projectionsAndCatchUpHooks->projections as $projection) { - $projection->reset(); - } - } - - /** - * @param class-string> $projectionClassName - */ - public function resetProjectionState(string $projectionClassName): void - { - $projection = $this->projectionsAndCatchUpHooks->projections->get($projectionClassName); - $projection->reset(); - } - public function getNodeTypeManager(): NodeTypeManager { return $this->nodeTypeManager; diff --git a/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php b/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php index 84b80d2f870..e4ab7834423 100644 --- a/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php +++ b/Neos.ContentRepository.Core/Classes/EventStore/EventPersister.php @@ -5,11 +5,14 @@ namespace Neos\ContentRepository\Core\EventStore; use Neos\ContentRepository\Core\ContentRepository; +use Neos\ContentRepository\Core\Factory\ContentRepositoryHooksFactory; use Neos\ContentRepository\Core\Projection\Projections; use Neos\ContentRepository\Core\Projection\WithMarkStaleInterface; use Neos\EventStore\EventStoreInterface; use Neos\EventStore\Exception\ConcurrencyException; +use Neos\EventStore\Model\Event\SequenceNumber; use Neos\EventStore\Model\Events; +use Symfony\Component\Lock\LockFactory; /** * Internal service to persist {@see EventInterface} with the proper normalization, and triggering the @@ -17,12 +20,14 @@ * * @internal */ -final class EventPersister +final readonly class EventPersister { public function __construct( - private readonly EventStoreInterface $eventStore, - private readonly EventNormalizer $eventNormalizer, - private readonly Projections $projections, + private EventStoreInterface $eventStore, + private EventNormalizer $eventNormalizer, + private Projections $projections, + private ContentRepositoryHooksFactory $hooksFactory, + private LockFactory $lockFactory, ) { } @@ -40,16 +45,40 @@ public function publishEvents(ContentRepository $contentRepository, EventsToPubl $normalizedEvents = Events::fromArray( $eventsToPublish->events->map($this->eventNormalizer->normalize(...)) ); - $this->eventStore->commit( + $commitResult = $this->eventStore->commit( $eventsToPublish->streamName, $normalizedEvents, $eventsToPublish->expectedVersion ); + $hooks = $this->hooksFactory->build($contentRepository); + $lock = $this->lockFactory->createLock($contentRepository->id->value . '_catchup'); + $lock->acquire(true); + + $expectedCheckpoint = SequenceNumber::fromInteger($commitResult->highestCommittedSequenceNumber->value - $eventsToPublish->events->count()); + + $projectionsToUpdate = []; foreach ($this->projections as $projection) { if ($projection instanceof WithMarkStaleInterface) { $projection->markStale(); } + if (!$projection->getCheckpoint()->equals($expectedCheckpoint)) { + //throw new \RuntimeException(sprintf('Projection %s is at checkpoint %d, but was expected to be at %d', $projection::class, $projection->getCheckpoint()->value, $expectedCheckpoint->value), 1714062281); + continue; + } + $projectionsToUpdate[] = $projection; + } + + $hooks->dispatchBeforeCatchUp(); + foreach ($this->eventStore->load($eventsToPublish->streamName)->withMinimumSequenceNumber($expectedCheckpoint->next()) as $eventEnvelope) { + $event = $this->eventNormalizer->denormalize($eventEnvelope->event); + $hooks->dispatchBeforeEvent($event, $eventEnvelope); + foreach ($projectionsToUpdate as $projection) { + $projection->apply($event, $eventEnvelope); + } + $hooks->dispatchAfterEvent($event, $eventEnvelope); } - $contentRepository->catchUpProjections(); + $hooks->dispatchAfterCatchup(); + $lock->release(); + //$contentRepository->catchUpProjections(); } } diff --git a/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php b/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php index 100298b988b..3fd5b3c7273 100644 --- a/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php +++ b/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryFactory.php @@ -28,11 +28,13 @@ use Neos\ContentRepository\Core\Feature\WorkspaceCommandHandler; use Neos\ContentRepository\Core\Infrastructure\Property\PropertyConverter; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; -use Neos\ContentRepository\Core\Projection\ProjectionsAndCatchUpHooks; +use Neos\ContentRepository\Core\Projection\Projections; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\User\UserIdProviderInterface; use Neos\EventStore\EventStoreInterface; use Psr\Clock\ClockInterface; +use Symfony\Component\Lock\LockFactory; +use Symfony\Component\Lock\Store\SemaphoreStore; use Symfony\Component\Serializer\Serializer; /** @@ -43,7 +45,12 @@ final class ContentRepositoryFactory { private ProjectionFactoryDependencies $projectionFactoryDependencies; - private ProjectionsAndCatchUpHooks $projectionsAndCatchUpHooks; + private Projections $projections; + + // The following properties store "singleton" references of objects for this content repository + private ?ContentRepository $contentRepository = null; + private ?CommandBus $commandBus = null; + private ?EventPersister $eventPersister = null; public function __construct( private readonly ContentRepositoryId $contentRepositoryId, @@ -51,7 +58,8 @@ public function __construct( NodeTypeManager $nodeTypeManager, ContentDimensionSourceInterface $contentDimensionSource, Serializer $propertySerializer, - ProjectionsAndCatchUpHooksFactory $projectionsAndCatchUpHooksFactory, + ProjectionsFactory $projectionsFactory, + private readonly ContentRepositoryHooksFactory $hooksFactory, private readonly UserIdProviderInterface $userIdProvider, private readonly ClockInterface $clock, ) { @@ -70,14 +78,9 @@ public function __construct( $interDimensionalVariationGraph, new PropertyConverter($propertySerializer) ); - $this->projectionsAndCatchUpHooks = $projectionsAndCatchUpHooksFactory->build($this->projectionFactoryDependencies); + $this->projections = $projectionsFactory->build($this->projectionFactoryDependencies); } - // The following properties store "singleton" references of objects for this content repository - private ?ContentRepository $contentRepository = null; - private ?CommandBus $commandBus = null; - private ?EventPersister $eventPersister = null; - /** * Builds and returns the content repository. If it is already built, returns the same instance. * @@ -91,8 +94,7 @@ public function getOrBuild(): ContentRepository $this->contentRepositoryId, $this->buildCommandBus(), $this->projectionFactoryDependencies->eventStore, - $this->projectionsAndCatchUpHooks, - $this->projectionFactoryDependencies->eventNormalizer, + $this->projections, $this->buildEventPersister(), $this->projectionFactoryDependencies->nodeTypeManager, $this->projectionFactoryDependencies->interDimensionalVariationGraph, @@ -123,7 +125,7 @@ public function buildService( $this->projectionFactoryDependencies, $this->getOrBuild(), $this->buildEventPersister(), - $this->projectionsAndCatchUpHooks->projections, + $this->projections, ); return $serviceFactory->build($serviceFactoryDependencies); } @@ -165,7 +167,9 @@ private function buildEventPersister(): EventPersister $this->eventPersister = new EventPersister( $this->projectionFactoryDependencies->eventStore, $this->projectionFactoryDependencies->eventNormalizer, - $this->projectionsAndCatchUpHooks->projections, + $this->projections, + $this->hooksFactory, + new LockFactory(new SemaphoreStore()), // TODO make configurable ); } return $this->eventPersister; diff --git a/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryHooksFactory.php b/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryHooksFactory.php new file mode 100644 index 00000000000..5bb0b279dbf --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/Factory/ContentRepositoryHooksFactory.php @@ -0,0 +1,44 @@ +}> + */ + private array $factories = []; + + /** + * @param array $options + * @api + */ + public function registerFactory(ContentRepositoryHookFactoryInterface $factory, array $options): void + { + $this->factories[] = [ + 'factory' => $factory, + 'options' => $options, + ]; + } + + /** + * @internal this method is only called by the {@see ContentRepository}, and not by anybody in userland + */ + public function build(ContentRepository $contentRepository): ContentRepositoryHooks + { + $hooks = []; + foreach ($this->factories as $factoryDefinition) { + $factory = $factoryDefinition['factory']; + $options = $factoryDefinition['options']; + $hooks[] = $factory->build($contentRepository, $options); + } + return ContentRepositoryHooks::fromArray($hooks); + } +} diff --git a/Neos.ContentRepository.Core/Classes/Factory/ProjectionsAndCatchUpHooksFactory.php b/Neos.ContentRepository.Core/Classes/Factory/ProjectionsAndCatchUpHooksFactory.php deleted file mode 100644 index 28e7fd6f27d..00000000000 --- a/Neos.ContentRepository.Core/Classes/Factory/ProjectionsAndCatchUpHooksFactory.php +++ /dev/null @@ -1,77 +0,0 @@ ->, options: array, catchUpHooksFactories: array}> - */ - private array $factories = []; - - /** - * @param ProjectionFactoryInterface> $factory - * @param array $options - * @return void - * @api - */ - public function registerFactory(ProjectionFactoryInterface $factory, array $options): void - { - $this->factories[get_class($factory)] = [ - 'factory' => $factory, - 'options' => $options, - 'catchUpHooksFactories' => [] - ]; - } - - /** - * @param ProjectionFactoryInterface> $factory - * @param CatchUpHookFactoryInterface $catchUpHookFactory - * @return void - * @api - */ - public function registerCatchUpHookFactory(ProjectionFactoryInterface $factory, CatchUpHookFactoryInterface $catchUpHookFactory): void - { - $this->factories[get_class($factory)]['catchUpHooksFactories'][] = $catchUpHookFactory; - } - - /** - * @internal this method is only called by the {@see ContentRepositoryFactory}, and not by anybody in userland - */ - public function build(ProjectionFactoryDependencies $projectionFactoryDependencies): ProjectionsAndCatchUpHooks - { - $projectionsArray = []; - $catchUpHookFactoriesByProjectionClassName = []; - foreach ($this->factories as $factoryDefinition) { - $factory = $factoryDefinition['factory']; - $options = $factoryDefinition['options']; - assert($factory instanceof ProjectionFactoryInterface); - - $catchUpHookFactories = CatchUpHookFactories::create(); - foreach ($factoryDefinition['catchUpHooksFactories'] as $catchUpHookFactory) { - assert($catchUpHookFactory instanceof CatchUpHookFactoryInterface); - $catchUpHookFactories = $catchUpHookFactories->with($catchUpHookFactory); - } - - $projection = $factory->build( - $projectionFactoryDependencies, - $options, - ); - $catchUpHookFactoriesByProjectionClassName[$projection::class] = $catchUpHookFactories; - $projectionsArray[] = $projection; - } - - return new ProjectionsAndCatchUpHooks(Projections::fromArray($projectionsArray), $catchUpHookFactoriesByProjectionClassName); - } -} diff --git a/Neos.ContentRepository.Core/Classes/Factory/ProjectionsFactory.php b/Neos.ContentRepository.Core/Classes/Factory/ProjectionsFactory.php new file mode 100644 index 00000000000..15bfce0c15b --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/Factory/ProjectionsFactory.php @@ -0,0 +1,52 @@ +>, options: array}> + */ + private array $factories = []; + + /** + * @param ProjectionFactoryInterface> $factory + * @param array $options + * @return void + * @api + */ + public function registerFactory(ProjectionFactoryInterface $factory, array $options): void + { + $this->factories[] = [ + 'factory' => $factory, + 'options' => $options, + ]; + } + + /** + * @internal this method is only called by the {@see ContentRepositoryBuilder}, and not by anybody in userland + */ + public function build(ProjectionFactoryDependencies $projectionFactoryDependencies): Projections + { + $projectionsArray = []; + foreach ($this->factories as $factoryDefinition) { + $factory = $factoryDefinition['factory']; + $options = $factoryDefinition['options']; + assert($factory instanceof ProjectionFactoryInterface); + $projection = $factory->build( + $projectionFactoryDependencies, + $options, + ); + $projectionsArray[] = $projection; + } + return Projections::fromArray($projectionsArray); + } +} diff --git a/Neos.ContentRepository.Core/Classes/Projection/CatchUpHookFactories.php b/Neos.ContentRepository.Core/Classes/Projection/CatchUpHookFactories.php deleted file mode 100644 index deabf53477b..00000000000 --- a/Neos.ContentRepository.Core/Classes/Projection/CatchUpHookFactories.php +++ /dev/null @@ -1,52 +0,0 @@ - - */ - private array $catchUpHookFactories; - - private function __construct(CatchUpHookFactoryInterface ...$catchUpHookFactories) - { - $this->catchUpHookFactories = $catchUpHookFactories; - } - - public static function create(): self - { - return new self(); - } - - public function with(CatchUpHookFactoryInterface $catchUpHookFactory): self - { - if ($this->has($catchUpHookFactory::class)) { - throw new \InvalidArgumentException( - sprintf('a CatchUpHookFactory of type "%s" already exists in this set', $catchUpHookFactory::class), - 1650121280 - ); - } - $catchUpHookFactories = $this->catchUpHookFactories; - $catchUpHookFactories[$catchUpHookFactory::class] = $catchUpHookFactory; - return new self(...$catchUpHookFactories); - } - - private function has(string $catchUpHookFactoryClassName): bool - { - return array_key_exists($catchUpHookFactoryClassName, $this->catchUpHookFactories); - } - - public function build(ContentRepository $contentRepository): CatchUpHookInterface - { - $catchUpHooks = array_map(static fn(CatchUpHookFactoryInterface $catchUpHookFactory) => $catchUpHookFactory->build($contentRepository), $this->catchUpHookFactories); - return new DelegatingCatchUpHook(...$catchUpHooks); - } -} diff --git a/Neos.ContentRepository.Core/Classes/Projection/CatchUpHookFactoryInterface.php b/Neos.ContentRepository.Core/Classes/Projection/CatchUpHookFactoryInterface.php deleted file mode 100644 index ec84d096c16..00000000000 --- a/Neos.ContentRepository.Core/Classes/Projection/CatchUpHookFactoryInterface.php +++ /dev/null @@ -1,15 +0,0 @@ -catchUpHooks = $catchUpHooks; - } - - public function onBeforeCatchUp(): void - { - foreach ($this->catchUpHooks as $catchUpHook) { - $catchUpHook->onBeforeCatchUp(); - } - } - - public function onBeforeEvent(EventInterface $eventInstance, EventEnvelope $eventEnvelope): void - { - foreach ($this->catchUpHooks as $catchUpHook) { - $catchUpHook->onBeforeEvent($eventInstance, $eventEnvelope); - } - } - - public function onAfterEvent(EventInterface $eventInstance, EventEnvelope $eventEnvelope): void - { - foreach ($this->catchUpHooks as $catchUpHook) { - $catchUpHook->onAfterEvent($eventInstance, $eventEnvelope); - } - } - - public function onBeforeBatchCompleted(): void - { - foreach ($this->catchUpHooks as $catchUpHook) { - $catchUpHook->onBeforeBatchCompleted(); - } - } - - public function onAfterCatchUp(): void - { - foreach ($this->catchUpHooks as $catchUpHook) { - $catchUpHook->onAfterCatchUp(); - } - } -} diff --git a/Neos.ContentRepository.Core/Classes/Projection/ProjectionStatus.php b/Neos.ContentRepository.Core/Classes/Projection/ProjectionStatus.php index bcf9399ead0..c1b99b9389d 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ProjectionStatus.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ProjectionStatus.php @@ -42,6 +42,14 @@ public static function replayRequired(string $details): self return new self(ProjectionStatusType::REPLAY_REQUIRED, $details); } + /** + * @param non-empty-string $details + */ + public static function catchupRequired(string $details): self + { + return new self(ProjectionStatusType::CATCHUP_REQUIRED, $details); + } + /** * @param non-empty-string $details */ diff --git a/Neos.ContentRepository.Core/Classes/Projection/ProjectionStatusType.php b/Neos.ContentRepository.Core/Classes/Projection/ProjectionStatusType.php index 33c7e79c9cc..555518b65c6 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ProjectionStatusType.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ProjectionStatusType.php @@ -11,4 +11,5 @@ enum ProjectionStatusType case ERROR; case SETUP_REQUIRED; case REPLAY_REQUIRED; + case CATCHUP_REQUIRED; } diff --git a/Neos.ContentRepository.Core/Classes/Projection/Projections.php b/Neos.ContentRepository.Core/Classes/Projection/Projections.php index 0bc7dffec20..b835a78b2b7 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/Projections.php +++ b/Neos.ContentRepository.Core/Classes/Projection/Projections.php @@ -4,11 +4,13 @@ namespace Neos\ContentRepository\Core\Projection; +use Neos\ContentRepository\Core\Service\ProjectionService; + /** * An immutable set of Content Repository projections ({@see ProjectionInterface} * * @implements \IteratorAggregate - * @internal + * @internal only used by framework code or services such as {@see ProjectionService} */ final class Projections implements \IteratorAggregate, \Countable { @@ -86,6 +88,13 @@ public function getClassNames(): array return array_keys($this->projections); } + public function resetAll(): void + { + foreach ($this->projections as $projection) { + $projection->reset(); + } + } + /** * @return \Traversable> */ diff --git a/Neos.ContentRepository.Core/Classes/Projection/ProjectionsAndCatchUpHooks.php b/Neos.ContentRepository.Core/Classes/Projection/ProjectionsAndCatchUpHooks.php deleted file mode 100644 index 86eebadae4a..00000000000 --- a/Neos.ContentRepository.Core/Classes/Projection/ProjectionsAndCatchUpHooks.php +++ /dev/null @@ -1,28 +0,0 @@ ->, CatchUpHookFactories> $catchUpHookFactoriesByProjectionClassName - */ - public function __construct( - public Projections $projections, - private array $catchUpHookFactoriesByProjectionClassName, - ) { - } - - /** - * @param ProjectionInterface $projection - */ - public function getCatchUpHookFactoryForProjection(ProjectionInterface $projection): ?CatchUpHookFactoryInterface - { - return $this->catchUpHookFactoriesByProjectionClassName[$projection::class] ?? null; - } -} diff --git a/Neos.ContentRepository.Core/Classes/Projection/WithMarkStaleInterface.php b/Neos.ContentRepository.Core/Classes/Projection/WithMarkStaleInterface.php index 124b97ec6b5..5902bb0b419 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/WithMarkStaleInterface.php +++ b/Neos.ContentRepository.Core/Classes/Projection/WithMarkStaleInterface.php @@ -18,8 +18,7 @@ interface WithMarkStaleInterface { /** - * Triggered directly before {@see ContentRepository::catchUpProjections()} is called; - * by the {@see EventPersister::publishEvents()} method. + * Triggered by {@see EventPersister::publishEvents()} before events are applied; * * Can be f.e. used to disable caches inside the Projection State. * diff --git a/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayService.php b/Neos.ContentRepository.Core/Classes/Service/ProjectionService.php similarity index 55% rename from Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayService.php rename to Neos.ContentRepository.Core/Classes/Service/ProjectionService.php index 00a946be01a..b7f71db9348 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayService.php +++ b/Neos.ContentRepository.Core/Classes/Service/ProjectionService.php @@ -1,38 +1,58 @@ resolveProjection($projectionAliasOrClassName); + $this->catchUpProjectionInternal($projection, $options); + } + + public function catchUpAllProjections(CatchUpOptions $options, ?\Closure $progressCallback = null): void + { + foreach ($this->projectionClassNamesAndAliases() as $classNamesAndAlias) { + if ($progressCallback) { + $progressCallback($classNamesAndAlias['alias']); + } + $projection = $this->projections->get($classNamesAndAlias['className']); + $this->catchUpProjectionInternal($projection, $options); + } + } + public function replayProjection(string $projectionAliasOrClassName, CatchUpOptions $options): void { - $projectionClassName = $this->resolveProjectionClassName($projectionAliasOrClassName); - $this->contentRepository->resetProjectionState($projectionClassName); - $this->contentRepository->catchUpProjection($projectionClassName, $options); + $projection = $this->resolveProjection($projectionAliasOrClassName); + $projection->reset(); + $this->catchUpProjectionInternal($projection, $options); } public function replayAllProjections(CatchUpOptions $options, ?\Closure $progressCallback = null): void @@ -41,20 +61,20 @@ public function replayAllProjections(CatchUpOptions $options, ?\Closure $progres if ($progressCallback) { $progressCallback($classNamesAndAlias['alias']); } - $this->contentRepository->resetProjectionState($classNamesAndAlias['className']); - $this->contentRepository->catchUpProjection($classNamesAndAlias['className'], $options); + $projection = $this->projections->get($classNamesAndAlias['className']); + $projection->reset(); + $this->catchUpProjectionInternal($projection, $options); } } public function resetAllProjections(): void { - foreach ($this->projectionClassNamesAndAliases() as $classNamesAndAlias) { - $this->contentRepository->resetProjectionState($classNamesAndAlias['className']); - } + $this->projections->resetAll(); } public function highestSequenceNumber(): SequenceNumber { + /** @noinspection LoopWhichDoesNotLoopInspection */ foreach ($this->eventStore->load(VirtualStreamName::all())->backwards()->limit(1) as $eventEnvelope) { return $eventEnvelope->sequenceNumber; } @@ -67,15 +87,15 @@ public function numberOfProjections(): int } /** - * @return class-string> + * @return ProjectionInterface */ - private function resolveProjectionClassName(string $projectionAliasOrClassName): string + private function resolveProjection(string $projectionAliasOrClassName): ProjectionInterface { $lowerCaseProjectionName = strtolower($projectionAliasOrClassName); $projectionClassNamesAndAliases = $this->projectionClassNamesAndAliases(); foreach ($projectionClassNamesAndAliases as $classNamesAndAlias) { if (strtolower($classNamesAndAlias['className']) === $lowerCaseProjectionName || strtolower($classNamesAndAlias['alias']) === $lowerCaseProjectionName) { - return $classNamesAndAlias['className']; + return $this->projections->get($classNamesAndAlias['className']); } } throw new \InvalidArgumentException(sprintf( @@ -99,6 +119,27 @@ private function projectionClassNamesAndAliases(): array ); } + /** + * NOTE: This will NOT trigger hooks! + * + * @param ProjectionInterface $projection + */ + private function catchUpProjectionInternal(ProjectionInterface $projection, CatchUpOptions $options): void + { + $streamName = VirtualStreamName::all(); + $eventStream = $this->eventStore->load($streamName); + if ($options->maximumSequenceNumber !== null) { + $eventStream = $eventStream->withMaximumSequenceNumber($options->maximumSequenceNumber); + } + foreach ($eventStream as $eventEnvelope) { + $event = $this->eventNormalizer->denormalize($eventEnvelope->event); + if ($options->progressCallback !== null) { + ($options->progressCallback)($event, $eventEnvelope); + } + $projection->apply($event, $eventEnvelope); + } + } + private static function projectionAlias(string $className): string { $alias = lcfirst(substr(strrchr($className, '\\') ?: '\\' . $className, 1)); diff --git a/Neos.ContentRepository.Core/Classes/Service/ProjectionServiceFactory.php b/Neos.ContentRepository.Core/Classes/Service/ProjectionServiceFactory.php new file mode 100644 index 00000000000..51e22fe4b4a --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/Service/ProjectionServiceFactory.php @@ -0,0 +1,24 @@ + + * @api + */ +class ProjectionServiceFactory implements ContentRepositoryServiceFactoryInterface +{ + public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): ProjectionService + { + return new ProjectionService( + $serviceFactoryDependencies->projections, + $serviceFactoryDependencies->eventStore, + $serviceFactoryDependencies->eventNormalizer, + ); + } +} diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/ContentRepository/ContentRepositoryHookFactoryInterface.php b/Neos.ContentRepository.Core/Classes/SharedModel/ContentRepository/ContentRepositoryHookFactoryInterface.php new file mode 100644 index 00000000000..dfa2402e2bd --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/SharedModel/ContentRepository/ContentRepositoryHookFactoryInterface.php @@ -0,0 +1,18 @@ + $options + */ + public function build(ContentRepository $contentRepository, array $options): ContentRepositoryHookInterface; +} diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/ContentRepository/ContentRepositoryHookInterface.php b/Neos.ContentRepository.Core/Classes/SharedModel/ContentRepository/ContentRepositoryHookInterface.php new file mode 100644 index 00000000000..950706c937a --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/SharedModel/ContentRepository/ContentRepositoryHookInterface.php @@ -0,0 +1,52 @@ + + * @internal + */ +final class ContentRepositoryHooks implements \IteratorAggregate +{ + /** + * @var array + */ + private array $hooks; + + private function __construct(ContentRepositoryHookInterface ...$hooks) + { + $this->hooks = $hooks; + } + + public static function empty(): self + { + return new self(); + } + + /** + * @param array $hooks + * @return self + */ + public static function fromArray(array $hooks): self + { + return new self(...array_values($hooks)); + } + + public function dispatchBeforeCatchUp(): void + { + foreach ($this->hooks as $hook) { + $hook->onBeforeEvents(); + } + } + + public function dispatchBeforeEvent(EventInterface $event, EventEnvelope $eventEnvelope): void + { + foreach ($this->hooks as $hook) { + $hook->onBeforeEvent($event, $eventEnvelope); + } + } + + public function dispatchAfterEvent(EventInterface $event, EventEnvelope $eventEnvelope): void + { + foreach ($this->hooks as $hook) { + $hook->onAfterEvent($event, $eventEnvelope); + } + } + + public function dispatchAfterCatchup(): void + { + foreach ($this->hooks as $hook) { + $hook->onAfterEvents(); + } + } + + /** + * @return \Traversable + */ + public function getIterator(): \Traversable + { + yield from $this->hooks; + } +} diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/CrCommandController.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/CrCommandController.php index cff112dd638..8659e916500 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/CrCommandController.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/Command/CrCommandController.php @@ -19,12 +19,12 @@ use Doctrine\DBAL\Exception as DbalException; use Doctrine\DBAL\Exception\ConnectionException; use Neos\ContentRepository\Core\Projection\CatchUpOptions; +use Neos\ContentRepository\Core\Service\ProjectionServiceFactory; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\LegacyNodeMigration\LegacyMigrationService; use Neos\ContentRepository\LegacyNodeMigration\LegacyMigrationServiceFactory; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\ContentRepositoryRegistry\Factory\EventStore\DoctrineEventStoreFactory; -use Neos\ContentRepositoryRegistry\Service\ProjectionReplayServiceFactory; use Neos\Flow\Cli\CommandController; use Neos\Flow\Persistence\PersistenceManagerInterface; use Neos\Flow\Property\PropertyMapper; @@ -47,7 +47,7 @@ public function __construct( private readonly PropertyMapper $propertyMapper, private readonly ContentRepositoryRegistry $contentRepositoryRegistry, private readonly SiteRepository $siteRepository, - private readonly ProjectionReplayServiceFactory $projectionReplayServiceFactory, + private readonly ProjectionServiceFactory $projectionServiceFactory, ) { parent::__construct(); } @@ -120,7 +120,7 @@ public function migrateLegacyDataCommand(bool $verbose = false, string $config = } $this->connection->executeStatement('TRUNCATE ' . $connection->quoteIdentifier($eventTableName)); // we also need to reset the projections; in order to ensure the system runs deterministically - $projectionService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->projectionReplayServiceFactory); + $projectionService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->projectionServiceFactory); $projectionService->resetAllProjections(); $this->outputLine('Truncated events'); diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php index 057edaa23b6..3e3c206d208 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php @@ -28,6 +28,8 @@ use Neos\ContentRepository\Core\Projection\Workspace\Workspace; use Neos\ContentRepository\Core\Service\ContentStreamPruner; use Neos\ContentRepository\Core\Service\ContentStreamPrunerFactory; +use Neos\ContentRepository\Core\Service\ProjectionService; +use Neos\ContentRepository\Core\Service\ProjectionServiceFactory; use Neos\ContentRepository\Core\SharedModel\Exception\RootNodeAggregateDoesNotExist; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; @@ -303,8 +305,9 @@ abstract protected function getContentRepositoryService( */ public function iReplayTheProjection(string $projectionName): void { - $this->currentContentRepository->resetProjectionState($projectionName); - $this->currentContentRepository->catchUpProjection($projectionName, CatchUpOptions::create()); + /** @var ProjectionService $projectionService */ + $projectionService = $this->contentRepositoryRegistry->buildService($this->currentContentRepository->id, $this->getObject(ProjectionServiceFactory::class)); + $projectionService->replayProjection($projectionName, CatchUpOptions::create()); } protected function deserializeProperties(array $properties): PropertyValuesToWrite diff --git a/Neos.ContentRepositoryRegistry.TestSuite/Classes/Behavior/CRRegistrySubjectProvider.php b/Neos.ContentRepositoryRegistry.TestSuite/Classes/Behavior/CRRegistrySubjectProvider.php index c026f755119..611319cf0eb 100644 --- a/Neos.ContentRepositoryRegistry.TestSuite/Classes/Behavior/CRRegistrySubjectProvider.php +++ b/Neos.ContentRepositoryRegistry.TestSuite/Classes/Behavior/CRRegistrySubjectProvider.php @@ -17,6 +17,8 @@ use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryInterface; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; +use Neos\ContentRepository\Core\Service\ProjectionService; +use Neos\ContentRepository\Core\Service\ProjectionServiceFactory; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\ContentRepositoryRegistry\Exception\ContentRepositoryNotFoundException; @@ -66,7 +68,9 @@ public function iInitializeContentRepository(string $contentRepositoryId): void $contentRepository->setUp(); self::$alreadySetUpContentRepositories[] = $contentRepository->id; } - $contentRepository->resetProjectionStates(); + /** @var ProjectionService $projectionService */ + $projectionService = $this->contentRepositoryRegistry->buildService($contentRepository->id, $this->getObject(ProjectionServiceFactory::class)); + $projectionService->resetAllProjections(); } /** diff --git a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php index b5d98b29e8e..6ba6e8531ae 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php +++ b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php @@ -6,10 +6,10 @@ use Neos\ContentRepository\Core\Projection\CatchUpOptions; use Neos\ContentRepository\Core\Projection\ProjectionStatusType; use Neos\ContentRepository\Core\Service\ContentStreamPrunerFactory; +use Neos\ContentRepository\Core\Service\ProjectionServiceFactory; use Neos\ContentRepository\Core\Service\WorkspaceMaintenanceServiceFactory; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; -use Neos\ContentRepositoryRegistry\Service\ProjectionReplayServiceFactory; use Neos\EventStore\Model\Event\SequenceNumber; use Neos\EventStore\Model\EventStore\StatusType; use Neos\Flow\Cli\CommandController; @@ -22,7 +22,7 @@ final class CrCommandController extends CommandController public function __construct( private readonly ContentRepositoryRegistry $contentRepositoryRegistry, - private readonly ProjectionReplayServiceFactory $projectionServiceFactory, + private readonly ProjectionServiceFactory $projectionServiceFactory, ) { parent::__construct(); } @@ -92,6 +92,7 @@ public function statusCommand(string $contentRepository = 'default', bool $verbo ProjectionStatusType::OK => 'OK', ProjectionStatusType::SETUP_REQUIRED => 'Setup required!', ProjectionStatusType::REPLAY_REQUIRED => 'Replay required!', + ProjectionStatusType::CATCHUP_REQUIRED => 'Catchup required!', ProjectionStatusType::ERROR => 'ERROR', }); if ($verbose && ($projectionStatus->type !== ProjectionStatusType::OK || $projectionStatus->details)) { @@ -107,6 +108,74 @@ public function statusCommand(string $contentRepository = 'default', bool $verbo } } + /** + * Applies events to specified projection of a Content Repository that it hasn't processed yet + * + * @param string $projection Full Qualified Class Name or alias of the projection to replay (e.g. "contentStream") + * @param string $contentRepository Identifier of the Content Repository instance to operate on + * @param int $until Until which sequence number should projections be caught up to? useful for debugging + */ + public function projectionCatchUpCommand(string $projection, string $contentRepository = 'default', int $until = 0): void + { + $progressBar = new ProgressBar($this->output->getOutput()); + $progressBar->setFormat(' %current%/%max% [%bar%] %percent:3s%% %elapsed:16s%/%estimated:-16s% %memory:6s%'); + + $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); + $projectionService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->projectionServiceFactory); + + $options = CatchUpOptions::create(); + $progressBar->start(max($until > 0 ? $until : $projectionService->highestSequenceNumber()->value, 1)); + $options->with(progressCallback: fn () => $progressBar->advance()); + if ($until > 0) { + $options = $options->with(maximumSequenceNumber: SequenceNumber::fromInteger($until)); + } + $projectionService->catchUpProjection($projection, $options); + $progressBar->finish(); + $this->outputLine(); + $this->outputLine('Done.'); + } + + /** + * Applies un-applied events to all projections of the specified Content Repository by performing a catchup + * + * @param string $contentRepository Identifier of the Content Repository instance to operate on + * @param int $until Until which sequence number should projections be caught up to? useful for debugging + */ + public function projectionCatchUpAllCommand(string $contentRepository = 'default', int $until = 0): void + { + $mainSection = ($this->output->getOutput() instanceof ConsoleOutput) ? $this->output->getOutput()->section() : $this->output->getOutput(); + $mainProgressBar = new ProgressBar($mainSection); + $mainProgressBar->setBarCharacter(''); + $mainProgressBar->setEmptyBarCharacter('░'); + $mainProgressBar->setProgressCharacter(''); + $mainProgressBar->setFormat('debug'); + + $subSection = ($this->output->getOutput() instanceof ConsoleOutput) ? $this->output->getOutput()->section() : $this->output->getOutput(); + $progressBar = new ProgressBar($subSection); + $progressBar->setFormat(' %message% - %current%/%max% [%bar%] %percent:3s%% %elapsed:16s%/%estimated:-16s% %memory:6s%'); + + $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); + $projectionService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->projectionServiceFactory); + $this->outputLine('Replaying events for all projections of Content Repository "%s" ...', [$contentRepositoryId->value]); + $options = CatchUpOptions::create(); + $options = $options->with(progressCallback: fn () => $progressBar->advance()); + if ($until > 0) { + $options = $options->with(maximumSequenceNumber: SequenceNumber::fromInteger($until)); + } + $highestSequenceNumber = max($until > 0 ? $until : $projectionService->highestSequenceNumber()->value, 1); + $mainProgressBar->start($projectionService->numberOfProjections()); + $mainProgressCallback = static function (string $projectionAlias) use ($mainProgressBar, $progressBar, $highestSequenceNumber) { + $mainProgressBar->advance(); + $progressBar->setMessage($projectionAlias); + $progressBar->start($highestSequenceNumber); + $progressBar->setProgress(0); + }; + $projectionService->replayAllProjections($options, $mainProgressCallback); + $mainProgressBar->finish(); + $progressBar->finish(); + $this->outputLine('Done.'); + } + /** * Replays the specified projection of a Content Repository by resetting its state and performing a full catchup. * diff --git a/Neos.ContentRepositoryRegistry/Classes/ContentRepositoryRegistry.php b/Neos.ContentRepositoryRegistry/Classes/ContentRepositoryRegistry.php index 8fde1f030be..d34d0916b46 100644 --- a/Neos.ContentRepositoryRegistry/Classes/ContentRepositoryRegistry.php +++ b/Neos.ContentRepositoryRegistry/Classes/ContentRepositoryRegistry.php @@ -7,14 +7,16 @@ use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\Dimension\ContentDimensionSourceInterface; use Neos\ContentRepository\Core\Factory\ContentRepositoryFactory; +use Neos\ContentRepository\Core\Factory\ContentRepositoryHooksFactory; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryInterface; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; -use Neos\ContentRepository\Core\Factory\ProjectionsAndCatchUpHooksFactory; +use Neos\ContentRepository\Core\Factory\ProjectionsFactory; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; -use Neos\ContentRepository\Core\Projection\CatchUpHookFactoryInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Projection\ProjectionFactoryInterface; +use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryHookFactoryInterface; +use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryHooks; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\User\UserIdProviderInterface; use Neos\ContentRepositoryRegistry\Exception\ContentRepositoryNotFoundException; @@ -128,7 +130,8 @@ private function getFactory( /** * @throws ContentRepositoryNotFoundException | InvalidConfigurationException */ - private function buildFactory(ContentRepositoryId $contentRepositoryId): ContentRepositoryFactory { + private function buildFactory(ContentRepositoryId $contentRepositoryId): ContentRepositoryFactory + { if (!is_array($this->settings['contentRepositories'] ?? null)) { throw InvalidConfigurationException::fromMessage('No Content Repositories are configured'); } @@ -154,6 +157,7 @@ private function buildFactory(ContentRepositoryId $contentRepositoryId): Content $this->buildContentDimensionSource($contentRepositoryId, $contentRepositorySettings), $this->buildPropertySerializer($contentRepositoryId, $contentRepositorySettings), $this->buildProjectionsFactory($contentRepositoryId, $contentRepositorySettings), + $this->buildHooksFactory($contentRepositoryId, $contentRepositorySettings), $this->buildUserIdProvider($contentRepositoryId, $contentRepositorySettings), $clock ); @@ -220,10 +224,10 @@ private function buildPropertySerializer(ContentRepositoryId $contentRepositoryI } /** @param array $contentRepositorySettings */ - private function buildProjectionsFactory(ContentRepositoryId $contentRepositoryId, array $contentRepositorySettings): ProjectionsAndCatchUpHooksFactory + private function buildProjectionsFactory(ContentRepositoryId $contentRepositoryId, array $contentRepositorySettings): ProjectionsFactory { (isset($contentRepositorySettings['projections']) && is_array($contentRepositorySettings['projections'])) || throw InvalidConfigurationException::fromMessage('Content repository "%s" does not have projections configured, or the value is no array.', $contentRepositoryId->value); - $projectionsFactory = new ProjectionsAndCatchUpHooksFactory(); + $projectionsFactory = new ProjectionsFactory(); foreach ($contentRepositorySettings['projections'] as $projectionName => $projectionOptions) { if ($projectionOptions === null) { continue; @@ -233,20 +237,26 @@ private function buildProjectionsFactory(ContentRepositoryId $contentRepositoryI throw InvalidConfigurationException::fromMessage('Projection factory object name for projection "%s" (content repository "%s") is not an instance of %s but %s.', $projectionName, $contentRepositoryId->value, ProjectionFactoryInterface::class, get_debug_type($projectionFactory)); } $projectionsFactory->registerFactory($projectionFactory, $projectionOptions['options'] ?? []); - foreach (($projectionOptions['catchUpHooks'] ?? []) as $catchUpHookOptions) { - if ($catchUpHookOptions === null) { - continue; - } - $catchUpHookFactory = $this->objectManager->get($catchUpHookOptions['factoryObjectName']); - if (!$catchUpHookFactory instanceof CatchUpHookFactoryInterface) { - throw InvalidConfigurationException::fromMessage('CatchUpHook factory object name for projection "%s" (content repository "%s") is not an instance of %s but %s', $projectionName, $contentRepositoryId->value, CatchUpHookFactoryInterface::class, get_debug_type($catchUpHookFactory)); - } - $projectionsFactory->registerCatchUpHookFactory($projectionFactory, $catchUpHookFactory); - } } return $projectionsFactory; } + /** @param array $contentRepositorySettings */ + private function buildHooksFactory(ContentRepositoryId $contentRepositoryId, array $contentRepositorySettings): ContentRepositoryHooksFactory + { + (isset($contentRepositorySettings['hooks']) && is_array($contentRepositorySettings['hooks'])) || throw InvalidConfigurationException::fromMessage('Content repository "%s" does not have hooks configured, or the value is no array.', $contentRepositoryId->value); + $hooksFactory = new ContentRepositoryHooksFactory(); + foreach (($contentRepositorySettings['hooks']) as $hookOptions) { + if ($hookOptions === null) { + continue; + } + $hookFactory = $this->objectManager->get($hookOptions['factoryObjectName']); + $hookFactory instanceof ContentRepositoryHookFactoryInterface || throw InvalidConfigurationException::fromMessage('ContentRepositoryHook factory object name for content repository "%s" is not an instance of %s but %s', $contentRepositoryId->value, ContentRepositoryHookFactoryInterface::class, get_debug_type($hookFactory)); + $hooksFactory->registerFactory($hookFactory, $hookOptions['options'] ?? []); + } + return $hooksFactory; + } + /** @param array $contentRepositorySettings */ private function buildUserIdProvider(ContentRepositoryId $contentRepositoryId, array $contentRepositorySettings): UserIdProviderInterface { diff --git a/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayServiceFactory.php b/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayServiceFactory.php deleted file mode 100644 index 337297d9bb6..00000000000 --- a/Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayServiceFactory.php +++ /dev/null @@ -1,30 +0,0 @@ - - * @internal this is currently only used by the {@see CrCommandController} - */ -#[Flow\Scope("singleton")] -final class ProjectionReplayServiceFactory implements ContentRepositoryServiceFactoryInterface -{ - - public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): ContentRepositoryServiceInterface - { - return new ProjectionReplayService( - $serviceFactoryDependencies->projections, - $serviceFactoryDependencies->contentRepository, - $serviceFactoryDependencies->eventStore, - ); - } -} diff --git a/Neos.ContentRepositoryRegistry/Configuration/Objects.yaml b/Neos.ContentRepositoryRegistry/Configuration/Objects.yaml index 113f828b6d6..5ef73b729f4 100644 --- a/Neos.ContentRepositoryRegistry/Configuration/Objects.yaml +++ b/Neos.ContentRepositoryRegistry/Configuration/Objects.yaml @@ -31,3 +31,6 @@ Neos\ContentGraph\DoctrineDbalAdapter\DoctrineDbalContentGraphProjectionFactory: value: 'Neos\ContentGraph\DoctrineDbalAdapter\DoctrineDbalContentGraphProjectionFactory' 2: object: 'Doctrine\DBAL\Connection' + +Neos\ContentRepository\Core\Service\ProjectionServiceFactory: + scope: singleton diff --git a/Neos.Neos/Classes/Command/CrCommandController.php b/Neos.Neos/Classes/Command/CrCommandController.php index af2719e83e4..5c9775043de 100644 --- a/Neos.Neos/Classes/Command/CrCommandController.php +++ b/Neos.Neos/Classes/Command/CrCommandController.php @@ -7,6 +7,7 @@ use League\Flysystem\Filesystem; use League\Flysystem\Local\LocalFilesystemAdapter; use Neos\ContentRepository\Core\Projection\CatchUpOptions; +use Neos\ContentRepository\Core\Service\ProjectionServiceFactory; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Export\ExportService; @@ -14,7 +15,6 @@ use Neos\ContentRepository\Export\ImportService; use Neos\ContentRepository\Export\ImportServiceFactory; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; -use Neos\ContentRepositoryRegistry\Service\ProjectionReplayServiceFactory; use Neos\Flow\Annotations as Flow; use Neos\Flow\Cli\CommandController; use Neos\Flow\Persistence\PersistenceManagerInterface; @@ -38,7 +38,7 @@ public function __construct( private readonly ResourceManager $resourceManager, private readonly PersistenceManagerInterface $persistenceManager, private readonly ContentRepositoryRegistry $contentRepositoryRegistry, - private readonly ProjectionReplayServiceFactory $projectionReplayServiceFactory, + private readonly ProjectionServiceFactory $projectionServiceFactory, ) { parent::__construct(); } @@ -110,7 +110,7 @@ public function importCommand(string $path, string $contentRepository = 'default $this->outputLine('Replaying projections'); - $projectionService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->projectionReplayServiceFactory); + $projectionService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, $this->projectionServiceFactory); $projectionService->replayAllProjections(CatchUpOptions::create()); $this->outputLine('Done'); diff --git a/Neos.Neos/Classes/FrontendRouting/CatchUpHook/RouterCacheHookFactory.php b/Neos.Neos/Classes/FrontendRouting/CatchUpHook/RouterCacheHookFactory.php deleted file mode 100644 index dda9b9eead6..00000000000 --- a/Neos.Neos/Classes/FrontendRouting/CatchUpHook/RouterCacheHookFactory.php +++ /dev/null @@ -1,25 +0,0 @@ -routerCachingService - ); - } -} diff --git a/Neos.Neos/Classes/FrontendRouting/CatchUpHook/RouterCacheHook.php b/Neos.Neos/Classes/FrontendRouting/ContentRepositoryHook/RouterCacheHook.php similarity index 89% rename from Neos.Neos/Classes/FrontendRouting/CatchUpHook/RouterCacheHook.php rename to Neos.Neos/Classes/FrontendRouting/ContentRepositoryHook/RouterCacheHook.php index 3c57bc02c14..331aba9903c 100644 --- a/Neos.Neos/Classes/FrontendRouting/CatchUpHook/RouterCacheHook.php +++ b/Neos.Neos/Classes/FrontendRouting/ContentRepositoryHook/RouterCacheHook.php @@ -1,6 +1,6 @@ $this->onBeforeNodeAggregateWasRemoved($eventInstance), - NodePropertiesWereSet::class => $this->onBeforeNodePropertiesWereSet($eventInstance), - NodeAggregateWasMoved::class => $this->onBeforeNodeAggregateWasMoved($eventInstance), - SubtreeWasTagged::class => $this->onBeforeSubtreeWasTagged($eventInstance), + match ($event::class) { + NodeAggregateWasRemoved::class => $this->onBeforeNodeAggregateWasRemoved($event), + NodePropertiesWereSet::class => $this->onBeforeNodePropertiesWereSet($event), + NodeAggregateWasMoved::class => $this->onBeforeNodeAggregateWasMoved($event), + SubtreeWasTagged::class => $this->onBeforeSubtreeWasTagged($event), default => null }; } - public function onAfterEvent(EventInterface $eventInstance, EventEnvelope $eventEnvelope): void + public function onAfterEvent(EventInterface $event, EventEnvelope $eventEnvelope): void { - match ($eventInstance::class) { + match ($event::class) { NodeAggregateWasRemoved::class => $this->flushAllCollectedTags(), NodePropertiesWereSet::class => $this->flushAllCollectedTags(), NodeAggregateWasMoved::class => $this->flushAllCollectedTags(), @@ -58,12 +58,7 @@ public function onAfterEvent(EventInterface $eventInstance, EventEnvelope $event }; } - public function onBeforeBatchCompleted(): void - { - // Nothing to do here - } - - public function onAfterCatchUp(): void + public function onAfterEvents(): void { // Nothing to do here } @@ -166,7 +161,6 @@ private function flushAllCollectedTags(): void if ($this->tagsToFlush === []) { return; } - $this->routerCachingService->flushCachesByTags($this->tagsToFlush); $this->tagsToFlush = []; } diff --git a/Neos.Neos/Classes/FrontendRouting/ContentRepositoryHook/RouterCacheHookFactory.php b/Neos.Neos/Classes/FrontendRouting/ContentRepositoryHook/RouterCacheHookFactory.php new file mode 100644 index 00000000000..0ecada66851 --- /dev/null +++ b/Neos.Neos/Classes/FrontendRouting/ContentRepositoryHook/RouterCacheHookFactory.php @@ -0,0 +1,23 @@ +routerCachingService + ); + } +} diff --git a/Neos.Neos/Classes/Fusion/Cache/ContentCacheFlusher.php b/Neos.Neos/Classes/Fusion/Cache/ContentCacheFlusher.php index daea26cfeec..2d7624022ff 100644 --- a/Neos.Neos/Classes/Fusion/Cache/ContentCacheFlusher.php +++ b/Neos.Neos/Classes/Fusion/Cache/ContentCacheFlusher.php @@ -36,7 +36,7 @@ * This service flushes Fusion content caches triggered by node changes. * * It is called when the projection changes: In this case, it is triggered by - * {@see GraphProjectorCatchUpHookForCacheFlushing} which calls this method.. + * {@see ContentRepositoryHookForCacheFlushing} which calls this method.. * This is the relevant case if publishing a workspace * - where we f.e. need to flush the cache for Live. * diff --git a/Neos.Neos/Classes/Fusion/Cache/ContentRepositoryHookForCacheFlushing.php b/Neos.Neos/Classes/Fusion/Cache/ContentRepositoryHookForCacheFlushing.php new file mode 100644 index 00000000000..9bab48be236 --- /dev/null +++ b/Neos.Neos/Classes/Fusion/Cache/ContentRepositoryHookForCacheFlushing.php @@ -0,0 +1,166 @@ + INVARIANT_1 violated. + * => this case needs a cache flush at onAfterCatchUp; to ensure the system converges. + * + * CASE B (cache flushed on onAfterCatchUp only): + * - Let's assume the blocking has finished, and caches have not been flushed yet. + * - Then, during re-rendering, the old content is shown because the cache is still full + * + * => INVARIANT_2 violated. + * => this case needs a cache flush at onBeforeBatchCompleted. + * + * SUMMARY: we need to flush the cache at BOTH places. + * + * @internal + */ +class ContentRepositoryHookForCacheFlushing implements ContentRepositoryHookInterface +{ + private static bool $enabled = true; + + /** + * @var array> + */ + private array $cacheFlushesOnAfterCatchUp = []; + + + public function __construct( + private readonly ContentRepository $contentRepository, + private readonly ContentCacheFlusher $contentCacheFlusher + ) { + } + + public static function disabled(\Closure $fn): void + { + $previousValue = self::$enabled; + self::$enabled = false; + try { + $fn(); + } finally { + self::$enabled = $previousValue; + } + } + + + public function onBeforeEvents(): void + { + } + + public function onBeforeEvent(EventInterface $event, EventEnvelope $eventEnvelope): void + { + if (!self::$enabled) { + return; + } + + if ( + !$event instanceof NodeAggregateWasRemoved + // NOTE: when moving a node, we need to clear the cache not just after the move was completed, + // but also on the original location. Otherwise, we have the problem that the cache is not + // cleared, leading to presumably duplicate nodes in the UI. + && !$event instanceof NodeAggregateWasMoved + ) { + return; + } + $nodeAggregate = $this->contentRepository->getContentGraph()->findNodeAggregateById($event->getContentStreamId(), $event->getNodeAggregateId()); + if ($nodeAggregate === null) { + return; + } + $parentNodeAggregates = $this->contentRepository->getContentGraph()->findParentNodeAggregates($nodeAggregate->contentStreamId, $nodeAggregate->nodeAggregateId); + foreach ($parentNodeAggregates as $parentNodeAggregate) { + assert($parentNodeAggregate instanceof NodeAggregate); + $this->scheduleCacheFlushJobForNodeAggregate($this->contentRepository, $parentNodeAggregate->contentStreamId, $parentNodeAggregate->nodeAggregateId); + } + } + + public function onAfterEvent(EventInterface $event, EventEnvelope $eventEnvelope): void + { + if (!self::$enabled) { + return; + } + if ( + $event instanceof NodeAggregateWasRemoved + || !$event instanceof EmbedsContentStreamAndNodeAggregateId + ) { + return; + } + $nodeAggregate = $this->contentRepository->getContentGraph()->findNodeAggregateById($event->getContentStreamId(), $event->getNodeAggregateId()); + if ($nodeAggregate !== null) { + $this->scheduleCacheFlushJobForNodeAggregate($this->contentRepository, $nodeAggregate->contentStreamId, $nodeAggregate->nodeAggregateId); + } + } + + public function onAfterEvents(): void + { + foreach ($this->cacheFlushesOnAfterCatchUp as $entry) { + $this->contentCacheFlusher->flushNodeAggregate($entry['cr'], $entry['csi'], $entry['nai']); + } + $this->cacheFlushesOnAfterCatchUp = []; + } + + private function scheduleCacheFlushJobForNodeAggregate(ContentRepository $contentRepository, ContentStreamId $contentStreamId, NodeAggregateId $nodeAggregateId): void + { + // we store this in an associative array deduplicate. + $this->cacheFlushesOnAfterCatchUp[$contentStreamId->value . '__' . $nodeAggregateId->value] = [ + 'cr' => $contentRepository, + 'csi' => $contentStreamId, + 'nai' => $nodeAggregateId + ]; + } +} diff --git a/Neos.Neos/Classes/Fusion/Cache/GraphProjectorCatchUpHookForCacheFlushingFactory.php b/Neos.Neos/Classes/Fusion/Cache/ContentRepositoryHookForCacheFlushingFactory.php similarity index 60% rename from Neos.Neos/Classes/Fusion/Cache/GraphProjectorCatchUpHookForCacheFlushingFactory.php rename to Neos.Neos/Classes/Fusion/Cache/ContentRepositoryHookForCacheFlushingFactory.php index 7dfa6022cd6..028fabe2401 100644 --- a/Neos.Neos/Classes/Fusion/Cache/GraphProjectorCatchUpHookForCacheFlushingFactory.php +++ b/Neos.Neos/Classes/Fusion/Cache/ContentRepositoryHookForCacheFlushingFactory.php @@ -13,18 +13,18 @@ */ use Neos\ContentRepository\Core\ContentRepository; -use Neos\ContentRepository\Core\Projection\CatchUpHookFactoryInterface; +use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryHookFactoryInterface; -class GraphProjectorCatchUpHookForCacheFlushingFactory implements CatchUpHookFactoryInterface +class ContentRepositoryHookForCacheFlushingFactory implements ContentRepositoryHookFactoryInterface { public function __construct( private readonly ContentCacheFlusher $contentCacheFlusher ) { } - public function build(ContentRepository $contentRepository): GraphProjectorCatchUpHookForCacheFlushing + public function build(ContentRepository $contentRepository, array $options): ContentRepositoryHookForCacheFlushing { - return new GraphProjectorCatchUpHookForCacheFlushing( + return new ContentRepositoryHookForCacheFlushing( $contentRepository, $this->contentCacheFlusher ); diff --git a/Neos.Neos/Classes/Fusion/Cache/GraphProjectorCatchUpHookForCacheFlushing.monopic b/Neos.Neos/Classes/Fusion/Cache/GraphProjectorCatchUpHookForCacheFlushing.monopic deleted file mode 100644 index c7fc9e98bb5..00000000000 Binary files a/Neos.Neos/Classes/Fusion/Cache/GraphProjectorCatchUpHookForCacheFlushing.monopic and /dev/null differ diff --git a/Neos.Neos/Classes/Fusion/Cache/GraphProjectorCatchUpHookForCacheFlushing.php b/Neos.Neos/Classes/Fusion/Cache/GraphProjectorCatchUpHookForCacheFlushing.php deleted file mode 100644 index 2efd26a30ed..00000000000 --- a/Neos.Neos/Classes/Fusion/Cache/GraphProjectorCatchUpHookForCacheFlushing.php +++ /dev/null @@ -1,236 +0,0 @@ - new) │ │ TX commit │ - * │ │ (end of batch) │ - * update sequence │ │ - * number │ │ - * │ │ - * onAfterCatchUp - * onBefore (B) - * BatchCompleted - * (A) - * - * There are two natural places where the Fusion cache can be flushed: - * - A: onBeforeBatchCompleted (before ending the transaction in the projection) - * - B: onAfterCatchUp (after ending the transaction). - * - * We need to ensure that the system is eventually consistent, so the following invariants must hold: - * - After a change in the projection, some time later, the cache must have been flushed - * - at a re-rendering after the cache flush, the new content must be shown - * - when block() returns, ANY re-render (even if happening immediately) must return the new content. - * - (Eventual Consistency): Processes can be blocked arbitrarily long at any point in time indefinitely. - * - * The scenarios which are NOT allowed to happen are: - * - INVARIANT_1: after a change, the old content is still visible when all processes have ended. - * - INVARIANT_2: after a change, when rendering happens directly after block(), the old content - * is shown (e.g. because cache is not yet flushed). - * - * CASE A (cache flushed at onBeforeBatchCompleted only): - * - Let's assume the cache is flushed really quickly. - * - and AFTER the cache is flushed but BEFORE the transaction is committed, - * - another request hits the system - marked above with !1! - * - * THEN: the request will still load the old data, render the page based on the old data, and add - * the old data to the cache. The cache will not be flushed again because it has already been flushed. - * - * => INVARIANT_1 violated. - * => this case needs a cache flush at onAfterCatchUp; to ensure the system converges. - * - * CASE B (cache flushed on onAfterCatchUp only): - * - Let's assume the blocking has finished, and caches have not been flushed yet. - * - Then, during re-rendering, the old content is shown because the cache is still full - * - * => INVARIANT_2 violated. - * => this case needs a cache flush at onBeforeBatchCompleted. - * - * SUMMARY: we need to flush the cache at BOTH places. - * - * @internal - */ -class GraphProjectorCatchUpHookForCacheFlushing implements CatchUpHookInterface -{ - private static bool $enabled = true; - - - public static function disabled(\Closure $fn): void - { - $previousValue = self::$enabled; - self::$enabled = false; - try { - $fn(); - } finally { - self::$enabled = $previousValue; - } - } - - - public function __construct( - private readonly ContentRepository $contentRepository, - private readonly ContentCacheFlusher $contentCacheFlusher - ) { - } - - - public function onBeforeCatchUp(): void - { - } - - public function onBeforeEvent(EventInterface $eventInstance, EventEnvelope $eventEnvelope): void - { - if (!self::$enabled) { - // performance optimization: on full replay, we assume all caches to be flushed anyways - // - so we do not need to do it individually here. - return; - } - - if ( - $eventInstance instanceof NodeAggregateWasRemoved - // NOTE: when moving a node, we need to clear the cache not just after the move was completed, - // but also on the original location. Otherwise, we have the problem that the cache is not - // cleared, leading to presumably duplicate nodes in the UI. - || $eventInstance instanceof NodeAggregateWasMoved - ) { - $workspace = $this->contentRepository->getWorkspaceFinder()->findOneByCurrentContentStreamId($eventInstance->getContentStreamId()); - if ($workspace === null) { - return; - } - // FIXME: EventInterface->workspaceName - $contentGraph = $this->contentRepository->getContentGraph($workspace->workspaceName); - $nodeAggregate = $contentGraph->findNodeAggregateById( - $eventInstance->getNodeAggregateId() - ); - if ($nodeAggregate) { - $parentNodeAggregates = $contentGraph->findParentNodeAggregates( - $nodeAggregate->nodeAggregateId - ); - foreach ($parentNodeAggregates as $parentNodeAggregate) { - assert($parentNodeAggregate instanceof NodeAggregate); - $this->scheduleCacheFlushJobForNodeAggregate( - $this->contentRepository, - $workspace->workspaceName, - $parentNodeAggregate->nodeAggregateId - ); - } - } - } - } - - public function onAfterEvent(EventInterface $eventInstance, EventEnvelope $eventEnvelope): void - { - if (!self::$enabled) { - // performance optimization: on full replay, we assume all caches to be flushed anyways - // - so we do not need to do it individually here. - return; - } - - if ( - !($eventInstance instanceof NodeAggregateWasRemoved) - && $eventInstance instanceof EmbedsContentStreamAndNodeAggregateId - ) { - $workspace = $this->contentRepository->getWorkspaceFinder()->findOneByCurrentContentStreamId($eventInstance->getContentStreamId()); - if ($workspace === null) { - return; - } - // FIXME: EventInterface->workspaceName - $nodeAggregate = $this->contentRepository->getContentGraph($workspace->workspaceName)->findNodeAggregateById( - $eventInstance->getNodeAggregateId() - ); - - if ($nodeAggregate) { - $this->scheduleCacheFlushJobForNodeAggregate( - $this->contentRepository, - $workspace->workspaceName, - $nodeAggregate->nodeAggregateId - ); - } - } - } - - /** - * @var array> - */ - private array $cacheFlushesOnBeforeBatchCompleted = []; - /** - * @var array> - */ - private array $cacheFlushesOnAfterCatchUp = []; - - protected function scheduleCacheFlushJobForNodeAggregate( - ContentRepository $contentRepository, - WorkspaceName $workspaceName, - NodeAggregateId $nodeAggregateId - ): void { - // we store this in an associative array deduplicate. - $this->cacheFlushesOnBeforeBatchCompleted[$workspaceName->value . '__' . $nodeAggregateId->value] = [ - 'cr' => $contentRepository, - 'wsn' => $workspaceName, - 'nai' => $nodeAggregateId - ]; - } - - public function onBeforeBatchCompleted(): void - { - foreach ($this->cacheFlushesOnBeforeBatchCompleted as $k => $entry) { - $this->contentCacheFlusher->flushNodeAggregate($entry['cr'], $entry['wsn'], $entry['nai']); - $this->cacheFlushesOnAfterCatchUp[$k] = $entry; - } - $this->cacheFlushesOnBeforeBatchCompleted = []; - } - - - public function onAfterCatchUp(): void - { - foreach ($this->cacheFlushesOnAfterCatchUp as $entry) { - $this->contentCacheFlusher->flushNodeAggregate($entry['cr'], $entry['wsn'], $entry['nai']); - } - $this->cacheFlushesOnAfterCatchUp = []; - } -} diff --git a/Neos.Neos/Configuration/Settings.ContentRepositoryRegistry.yaml b/Neos.Neos/Configuration/Settings.ContentRepositoryRegistry.yaml index fe26202594d..a02b78e4ae2 100644 --- a/Neos.Neos/Configuration/Settings.ContentRepositoryRegistry.yaml +++ b/Neos.Neos/Configuration/Settings.ContentRepositoryRegistry.yaml @@ -9,15 +9,13 @@ Neos: projections: 'Neos.Neos:DocumentUriPathProjection': factoryObjectName: Neos\Neos\FrontendRouting\Projection\DocumentUriPathProjectionFactory - catchUpHooks: - 'Neos.Neos:FlushRouteCache': - factoryObjectName: Neos\Neos\FrontendRouting\CatchUpHook\RouterCacheHookFactory - 'Neos.Neos:PendingChangesProjection': factoryObjectName: Neos\Neos\PendingChangesProjection\ChangeProjectionFactory - 'Neos.ContentRepository:ContentGraph': - catchUpHooks: - 'Neos.Neos:FlushContentCache': - factoryObjectName: Neos\Neos\Fusion\Cache\GraphProjectorCatchUpHookForCacheFlushingFactory 'Neos.Neos:AssetUsage': factoryObjectName: Neos\Neos\AssetUsage\Projection\AssetUsageProjectionFactory + + hooks: + 'Neos.Neos:FlushRouteCache': + factoryObjectName: Neos\Neos\FrontendRouting\ContentRepositoryHook\RouterCacheHookFactory + 'Neos.Neos:FlushContentCache': + factoryObjectName: Neos\Neos\Fusion\Cache\ContentRepositoryHookForCacheFlushingFactory diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 158d86aa599..4f8f2ba1560 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,9 +1,5 @@ parameters: ignoreErrors: - - - message: "#^The internal method \"Neos\\\\ContentRepository\\\\Core\\\\Projection\\\\Projections\\:\\:getClassNames\" is called\\.$#" - count: 1 - path: Neos.ContentRepositoryRegistry/Classes/Service/ProjectionReplayService.php - message: "#^Method Neos\\\\Neos\\\\Controller\\\\Backend\\\\MenuHelper\\:\\:buildModuleList\\(\\) return type has no value type specified in iterable type array\\.$#"