From f46077ec892a3aa0869d91a50ba4423a44983d99 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 24 Nov 2024 17:17:36 +0100 Subject: [PATCH 01/24] FEATURE: Introduce `ContentRepositoryMaintainer` and restore cr:projection commands --- .../CRBehavioralTestsSubjectProvider.php | 20 +- .../AbstractSubscriptionEngineTestCase.php | 8 +- .../Subscription/CatchUpHookErrorTest.php | 24 +- .../Subscription/CatchUpHookTest.php | 6 +- .../Subscription/ProjectionErrorTest.php | 58 ++-- .../SubscriptionActiveStatusTest.php | 24 +- .../SubscriptionBootingStatusTest.php | 10 +- .../SubscriptionDetachedStatusTest.php | 12 +- .../SubscriptionGetStatusTest.php | 4 +- .../SubscriptionNewStatusTest.php | 6 +- .../Subscription/SubscriptionResetTest.php | 4 +- .../Subscription/SubscriptionSetupTest.php | 32 +- .../Parallel/AbstractParallelTestCase.php | 21 +- .../Service/ContentRepositoryMaintainer.php | 146 +++++++++ ...=> ContentRepositoryMaintainerFactory.php} | 13 +- .../Classes/Service/SubscriptionService.php | 32 -- .../Engine/SubscriptionEngine.php | 4 +- .../SubscriptionAndProjectionStatuses.php | 13 + .../Features/Bootstrap/CRTestSuiteTrait.php | 7 +- .../Behavior/CRRegistrySubjectProvider.php | 29 +- .../Classes/Command/CrCommandController.php | 306 ++++++++++-------- ...ssor.php => ProjectionReplayProcessor.php} | 8 +- .../Processors/ProjectionResetProcessor.php | 24 -- .../ContentRepositoryPruningProcessor.php | 11 +- .../Domain/Service/SiteImportService.php | 26 +- .../Domain/Service/SitePruningService.php | 14 +- .../Features/FrontendRouting/Basic.feature | 2 +- 27 files changed, 497 insertions(+), 367 deletions(-) create mode 100644 Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php rename Neos.ContentRepository.Core/Classes/Service/{SubscriptionServiceFactory.php => ContentRepositoryMaintainerFactory.php} (50%) delete mode 100644 Neos.ContentRepository.Core/Classes/Service/SubscriptionService.php rename Neos.ContentRepositoryRegistry/Classes/Processors/{ProjectionCatchupProcessor.php => ProjectionReplayProcessor.php} (53%) delete mode 100644 Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionResetProcessor.php diff --git a/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/Behavior/CRBehavioralTestsSubjectProvider.php b/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/Behavior/CRBehavioralTestsSubjectProvider.php index 3fb475d02bd..2fb4fe8ac9b 100644 --- a/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/Behavior/CRBehavioralTestsSubjectProvider.php +++ b/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/Behavior/CRBehavioralTestsSubjectProvider.php @@ -18,12 +18,14 @@ use Behat\Gherkin\Node\TableNode; use Doctrine\DBAL\Connection; use Neos\ContentRepository\Core\ContentRepository; -use Neos\ContentRepository\Core\Service\SubscriptionServiceFactory; +use Neos\ContentRepository\Core\Service\ContentRepositoryMaintainerFactory; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; +use Neos\ContentRepository\Core\Subscription\Engine\SubscriptionEngine; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Helpers\GherkinTableNodeBasedContentDimensionSource; use Neos\ContentRepository\TestSuite\Fakes\FakeContentDimensionSourceFactory; use Neos\ContentRepository\TestSuite\Fakes\FakeNodeTypeManagerFactory; use Neos\EventStore\EventStoreInterface; +use PHPUnit\Framework\Assert; use Symfony\Component\Yaml\Yaml; /** @@ -179,20 +181,26 @@ protected function setUpContentRepository(ContentRepositoryId $contentRepository * Catch Up process and the testcase reset. */ $contentRepository = $this->createContentRepository($contentRepositoryId); - $subscriptionService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, new SubscriptionServiceFactory()); + $contentRepositoryMaintainer = $this->contentRepositoryRegistry->buildService($contentRepositoryId, new ContentRepositoryMaintainerFactory()); if (!in_array($contentRepository->id, self::$alreadySetUpContentRepositories)) { - $subscriptionService->setupEventStore(); - $subscriptionService->subscriptionEngine->setup(); + $result = $contentRepositoryMaintainer->setUp(); + Assert::assertNull($result); self::$alreadySetUpContentRepositories[] = $contentRepository->id; } + // todo we TRUNCATE here and do not want to use $contentRepositoryMaintainer->prune(); here as it would not reset the autoincrement sequence number making some assertions impossible /** @var EventStoreInterface $eventStore */ $eventStore = (new \ReflectionClass($contentRepository))->getProperty('eventStore')->getValue($contentRepository); /** @var Connection $databaseConnection */ $databaseConnection = (new \ReflectionClass($eventStore))->getProperty('connection')->getValue($eventStore); $eventTableName = sprintf('cr_%s_events', $contentRepositoryId->value); $databaseConnection->executeStatement('TRUNCATE ' . $eventTableName); - $subscriptionService->subscriptionEngine->reset(); - $subscriptionService->subscriptionEngine->boot(); + + /** @var SubscriptionEngine $subscriptionEngine */ + $subscriptionEngine = (new \ReflectionClass($contentRepositoryMaintainer))->getProperty('subscriptionEngine')->getValue($contentRepositoryMaintainer); + $result = $subscriptionEngine->reset(); + Assert::assertNull($result->errors); + $result = $subscriptionEngine->boot(); + Assert::assertNull($result->errors); return $contentRepository; } diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/AbstractSubscriptionEngineTestCase.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/AbstractSubscriptionEngineTestCase.php index 1602b362781..c4e9506aee9 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/AbstractSubscriptionEngineTestCase.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/AbstractSubscriptionEngineTestCase.php @@ -15,8 +15,6 @@ use Neos\ContentRepository\Core\Projection\ProjectionInterface; use Neos\ContentRepository\Core\Projection\ProjectionStateInterface; use Neos\ContentRepository\Core\Projection\ProjectionStatus; -use Neos\ContentRepository\Core\Service\SubscriptionService; -use Neos\ContentRepository\Core\Service\SubscriptionServiceFactory; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\Subscription\Engine\SubscriptionEngine; @@ -41,8 +39,6 @@ abstract class AbstractSubscriptionEngineTestCase extends TestCase // we don't u { protected ContentRepository $contentRepository; - protected SubscriptionService $subscriptionService; - protected SubscriptionEngine $subscriptionEngine; protected EventStoreInterface $eventStore; @@ -102,8 +98,6 @@ final protected function setupContentRepositoryDependencies(ContentRepositoryId $contentRepositoryId ); - $this->subscriptionService = $this->getObject(ContentRepositoryRegistry::class)->buildService($contentRepositoryId, new SubscriptionServiceFactory()); - $subscriptionEngineAndEventStoreAccessor = new class implements ContentRepositoryServiceFactoryInterface { public EventStoreInterface|null $eventStore; public SubscriptionEngine|null $subscriptionEngine; @@ -142,7 +136,7 @@ final protected function resetDatabase(Connection $connection, ContentRepository final protected function subscriptionStatus(string $subscriptionId): ?SubscriptionAndProjectionStatus { - return $this->subscriptionService->subscriptionEngine->subscriptionStatuses(SubscriptionCriteria::create(ids: [SubscriptionId::fromString($subscriptionId)]))->first(); + return $this->subscriptionEngine->subscriptionStatuses(SubscriptionCriteria::create(ids: [SubscriptionId::fromString($subscriptionId)]))->first(); } final protected function commitExampleContentStreamEvent(): void diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/CatchUpHookErrorTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/CatchUpHookErrorTest.php index 63b1bbc6806..9948144bbad 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/CatchUpHookErrorTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/CatchUpHookErrorTest.php @@ -18,11 +18,11 @@ final class CatchUpHookErrorTest extends AbstractSubscriptionEngineTestCase /** @test */ public function error_onBeforeEvent_projectionIsNotRun() { - $this->subscriptionService->setupEventStore(); + $this->eventStore->setup(); $this->fakeProjection->expects(self::once())->method('setUp'); $this->fakeProjection->expects(self::once())->method('apply'); - $this->subscriptionService->subscriptionEngine->setup(); - $this->subscriptionService->subscriptionEngine->boot(); + $this->subscriptionEngine->setup(); + $this->subscriptionEngine->boot(); // commit an event $this->commitExampleContentStreamEvent(); @@ -66,11 +66,11 @@ public function error_onBeforeEvent_projectionIsNotRun() /** @test */ public function error_onAfterEvent_projectionIsRolledBack() { - $this->subscriptionService->setupEventStore(); + $this->eventStore->setup(); $this->fakeProjection->expects(self::once())->method('setUp'); $this->fakeProjection->expects(self::once())->method('apply'); - $this->subscriptionService->subscriptionEngine->setup(); - $this->subscriptionService->subscriptionEngine->boot(); + $this->subscriptionEngine->setup(); + $this->subscriptionEngine->boot(); // commit an event $this->commitExampleContentStreamEvent(); @@ -112,11 +112,11 @@ public function error_onAfterEvent_projectionIsRolledBack() /** @test */ public function error_onBeforeCatchUp_abortsCatchup() { - $this->subscriptionService->setupEventStore(); + $this->eventStore->setup(); $this->fakeProjection->expects(self::once())->method('setUp'); $this->fakeProjection->expects(self::never())->method('apply'); - $this->subscriptionService->subscriptionEngine->setup(); - $this->subscriptionService->subscriptionEngine->boot(); + $this->subscriptionEngine->setup(); + $this->subscriptionEngine->boot(); $this->expectOkayStatus('Vendor.Package:SecondFakeProjection', SubscriptionStatus::ACTIVE, SequenceNumber::none()); @@ -158,11 +158,11 @@ public function error_onBeforeCatchUp_abortsCatchup() /** @test */ public function error_onAfterCatchUp_abortsCatchupAndRollBack() { - $this->subscriptionService->setupEventStore(); + $this->eventStore->setup(); $this->fakeProjection->expects(self::once())->method('setUp'); $this->fakeProjection->expects(self::once())->method('apply'); - $this->subscriptionService->subscriptionEngine->setup(); - $this->subscriptionService->subscriptionEngine->boot(); + $this->subscriptionEngine->setup(); + $this->subscriptionEngine->boot(); $this->expectOkayStatus('Vendor.Package:SecondFakeProjection', SubscriptionStatus::ACTIVE, SequenceNumber::none()); diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/CatchUpHookTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/CatchUpHookTest.php index 71fb6e8ca7c..56b9d632597 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/CatchUpHookTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/CatchUpHookTest.php @@ -13,11 +13,11 @@ final class CatchUpHookTest extends AbstractSubscriptionEngineTestCase /** @test */ public function catchUpHooksAreExecutedAndCanAccessTheCorrectProjectionsState() { - $this->subscriptionService->setupEventStore(); + $this->eventStore->setup(); $this->fakeProjection->expects(self::once())->method('setUp'); $this->fakeProjection->expects(self::once())->method('apply'); - $this->subscriptionService->subscriptionEngine->setup(); - $this->subscriptionService->subscriptionEngine->boot(); + $this->subscriptionEngine->setup(); + $this->subscriptionEngine->boot(); // commit an event $this->commitExampleContentStreamEvent(); diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/ProjectionErrorTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/ProjectionErrorTest.php index 86550aae570..8f82f1ef9e9 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/ProjectionErrorTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/ProjectionErrorTest.php @@ -25,11 +25,11 @@ final class ProjectionErrorTest extends AbstractSubscriptionEngineTestCase /** @test */ public function projectionWithError() { - $this->subscriptionService->setupEventStore(); + $this->eventStore->setup(); $this->fakeProjection->expects(self::once())->method('setUp'); - $this->subscriptionService->subscriptionEngine->setup(); + $this->subscriptionEngine->setup(); $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); - $result = $this->subscriptionService->subscriptionEngine->boot(); + $result = $this->subscriptionEngine->boot(); self::assertEquals(ProcessedResult::success(0), $result); $this->expectOkayStatus('contentGraph', SubscriptionStatus::ACTIVE, SequenceNumber::none()); $this->expectOkayStatus('Vendor.Package:FakeProjection', SubscriptionStatus::ACTIVE, SequenceNumber::none()); @@ -49,7 +49,7 @@ public function projectionWithError() projectionStatus: ProjectionStatus::ok(), ); - $result = $this->subscriptionService->subscriptionEngine->catchUpActive(); + $result = $this->subscriptionEngine->catchUpActive(); self::assertEquals(ProcessedResult::failed(1, Errors::fromArray([Error::fromSubscriptionIdAndException(SubscriptionId::fromString('Vendor.Package:FakeProjection'), $exception)])), $result); self::assertEquals( @@ -60,13 +60,13 @@ public function projectionWithError() // todo test retry if reimplemented: https://github.com/patchlevel/event-sourcing/blob/6826d533fd4762220f0397bc7afc589abb8c901b/src/Subscription/RetryStrategy/RetryStrategy.php // // CatchUp 2 with retry - // $result = $this->subscriptionService->subscriptionEngine->catchUpActive(); + // $result = $this->subscriptionEngine->catchUpActive(); // self::assertTrue($result->hasFailed()); // self::assertEquals($result->errors->first()->message, 'Something really wrong.'); // self::assertEquals($this->subscriptionStatus('Vendor.Package:FakeProjection')->subscriptionError->errorMessage, 'Something really wrong.'); // no retry, nothing to do. - $result = $this->subscriptionService->subscriptionEngine->catchUpActive(); + $result = $this->subscriptionEngine->catchUpActive(); self::assertEquals(ProcessedResult::success(0), $result); self::assertEquals($this->subscriptionStatus('Vendor.Package:FakeProjection')->subscriptionError->errorMessage, 'This projection is kaputt.'); self::assertEquals( @@ -78,11 +78,11 @@ public function projectionWithError() /** @test */ public function fixFailedProjection() { - $this->subscriptionService->setupEventStore(); + $this->eventStore->setup(); $this->fakeProjection->expects(self::once())->method('setUp'); $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); - $this->subscriptionService->subscriptionEngine->setup(); - $this->subscriptionService->subscriptionEngine->boot(); + $this->subscriptionEngine->setup(); + $this->subscriptionEngine->boot(); // commit an event $this->commitExampleContentStreamEvent(); @@ -101,7 +101,7 @@ public function fixFailedProjection() projectionStatus: ProjectionStatus::ok(), ); - $result = $this->subscriptionService->subscriptionEngine->catchUpActive(); + $result = $this->subscriptionEngine->catchUpActive(); self::assertTrue($result->hasFailed()); self::assertEquals( @@ -110,10 +110,10 @@ public function fixFailedProjection() ); // catchup active does not change anything - $result = $this->subscriptionService->subscriptionEngine->catchUpActive(); + $result = $this->subscriptionEngine->catchUpActive(); self::assertEquals(ProcessedResult::success(0), $result); // boot neither - $result = $this->subscriptionService->subscriptionEngine->boot(); + $result = $this->subscriptionEngine->boot(); self::assertEquals(ProcessedResult::success(0), $result); // still the same state self::assertEquals( @@ -123,13 +123,13 @@ public function fixFailedProjection() $this->fakeProjection->expects(self::once())->method('resetState'); - $result = $this->subscriptionService->subscriptionEngine->reset(); + $result = $this->subscriptionEngine->reset(); self::assertNull($result->errors); // expect the subscriptionError to be reset to null $this->expectOkayStatus('Vendor.Package:FakeProjection', SubscriptionStatus::BOOTING, SequenceNumber::none()); - $result = $this->subscriptionService->subscriptionEngine->boot(); + $result = $this->subscriptionEngine->boot(); self::assertNull($result->errors); $this->expectOkayStatus('Vendor.Package:FakeProjection', SubscriptionStatus::ACTIVE, SequenceNumber::fromInteger(1)); @@ -138,12 +138,12 @@ public function fixFailedProjection() /** @test */ public function projectionIsRolledBackAfterError() { - $this->subscriptionService->setupEventStore(); + $this->eventStore->setup(); $this->fakeProjection->expects(self::exactly(2))->method('setUp'); $this->fakeProjection->expects(self::once())->method('apply'); - $result = $this->subscriptionService->subscriptionEngine->setup(); + $result = $this->subscriptionEngine->setup(); self::assertNull($result->errors); - $result = $this->subscriptionService->subscriptionEngine->boot(); + $result = $this->subscriptionEngine->boot(); self::assertNull($result->errors); // commit an event @@ -165,7 +165,7 @@ public function projectionIsRolledBackAfterError() $this->secondFakeProjection->getState()->findAppliedSequenceNumbers() ); - $result = $this->subscriptionService->subscriptionEngine->catchUpActive(); + $result = $this->subscriptionEngine->catchUpActive(); self::assertSame($result->errors?->first()->message, 'This projection is kaputt.'); self::assertEquals( @@ -184,14 +184,14 @@ public function projectionIsRolledBackAfterError() $this->secondFakeProjection->killSaboteur(); - $result = $this->subscriptionService->subscriptionEngine->setup(); + $result = $this->subscriptionEngine->setup(); self::assertNull($result->errors); // subscriptionError is reset $this->expectOkayStatus('Vendor.Package:SecondFakeProjection', SubscriptionStatus::BOOTING, SequenceNumber::none()); // catchup after fix - $result = $this->subscriptionService->subscriptionEngine->boot(); + $result = $this->subscriptionEngine->boot(); self::assertNull($result->errors); self::assertEquals( @@ -203,11 +203,11 @@ public function projectionIsRolledBackAfterError() /** @test */ public function projectionIsRolledBackAfterErrorButKeepsSuccessFullEvents() { - $this->subscriptionService->setupEventStore(); + $this->eventStore->setup(); $this->fakeProjection->expects(self::exactly(2))->method('setUp'); $this->fakeProjection->expects(self::exactly(2))->method('apply'); - $this->subscriptionService->subscriptionEngine->setup(); - $this->subscriptionService->subscriptionEngine->boot(); + $this->subscriptionEngine->setup(); + $this->subscriptionEngine->boot(); // commit two events $this->commitExampleContentStreamEvent(); @@ -227,7 +227,7 @@ public function projectionIsRolledBackAfterErrorButKeepsSuccessFullEvents() $this->secondFakeProjection->getState()->findAppliedSequenceNumbers() ); - $result = $this->subscriptionService->subscriptionEngine->catchUpActive(); + $result = $this->subscriptionEngine->catchUpActive(); self::assertTrue($result->hasFailed()); $expectedFailure = SubscriptionAndProjectionStatus::create( @@ -255,7 +255,7 @@ public function projectionIsRolledBackAfterErrorButKeepsSuccessFullEvents() $this->secondFakeProjection->killSaboteur(); - $result = $this->subscriptionService->subscriptionEngine->setup(); + $result = $this->subscriptionEngine->setup(); self::assertNull($result->errors); // subscriptionError is reset, but the position is preserved @@ -266,7 +266,7 @@ public function projectionIsRolledBackAfterErrorButKeepsSuccessFullEvents() ); // catchup after fix - $result = $this->subscriptionService->subscriptionEngine->boot(); + $result = $this->subscriptionEngine->boot(); self::assertNull($result->errors); self::assertEquals( @@ -278,11 +278,11 @@ public function projectionIsRolledBackAfterErrorButKeepsSuccessFullEvents() /** @test */ public function projectionErrorWithMultipleProjectionsInContentRepositoryHandle() { - $this->subscriptionService->setupEventStore(); + $this->eventStore->setup(); $this->fakeProjection->expects(self::once())->method('setUp'); $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); - $this->subscriptionService->subscriptionEngine->setup(); - $this->subscriptionService->subscriptionEngine->boot(); + $this->subscriptionEngine->setup(); + $this->subscriptionEngine->boot(); $this->fakeProjection->expects(self::once())->method('apply')->with(self::isInstanceOf(ContentStreamWasCreated::class))->willThrowException( $originalException = new \RuntimeException('This projection is kaputt.'), diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionActiveStatusTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionActiveStatusTest.php index 2585852b998..421d9a4c212 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionActiveStatusTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionActiveStatusTest.php @@ -17,12 +17,12 @@ final class SubscriptionActiveStatusTest extends AbstractSubscriptionEngineTestC /** @test */ public function setupProjectionsAndCatchup() { - $this->subscriptionService->setupEventStore(); + $this->eventStore->setup(); $this->fakeProjection->expects(self::once())->method('setUp'); - $this->subscriptionService->subscriptionEngine->setup(); + $this->subscriptionEngine->setup(); - $result = $this->subscriptionService->subscriptionEngine->boot(); + $result = $this->subscriptionEngine->boot(); self::assertEquals(ProcessedResult::success(0), $result); $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); $this->expectOkayStatus('contentGraph', SubscriptionStatus::ACTIVE, SequenceNumber::none()); @@ -33,7 +33,7 @@ public function setupProjectionsAndCatchup() $this->commitExampleContentStreamEvent(); // subsequent catchup setup'd does not change the position - $result = $this->subscriptionService->subscriptionEngine->boot(); + $result = $this->subscriptionEngine->boot(); self::assertEquals(ProcessedResult::success(0), $result); $this->expectOkayStatus('contentGraph', SubscriptionStatus::ACTIVE, SequenceNumber::none()); $this->expectOkayStatus('Vendor.Package:FakeProjection', SubscriptionStatus::ACTIVE, SequenceNumber::none()); @@ -41,7 +41,7 @@ public function setupProjectionsAndCatchup() // catchup active does apply the commited event $this->fakeProjection->expects(self::once())->method('apply')->with(self::isInstanceOf(ContentStreamWasCreated::class)); - $result = $this->subscriptionService->subscriptionEngine->catchUpActive(); + $result = $this->subscriptionEngine->catchUpActive(); self::assertEquals(ProcessedResult::success(1), $result); $this->expectOkayStatus('contentGraph', SubscriptionStatus::ACTIVE, SequenceNumber::fromInteger(1)); @@ -55,9 +55,9 @@ public function filteringCatchUpActive() $this->fakeProjection->expects(self::once())->method('setUp'); $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); - $this->subscriptionService->setupEventStore(); + $this->eventStore->setup(); - $result = $this->subscriptionService->subscriptionEngine->setup(); + $result = $this->subscriptionEngine->setup(); self::assertNull($result->errors); $result = $this->subscriptionEngine->boot(); self::assertNull($result->errors); @@ -85,13 +85,13 @@ public function filteringCatchUpActive() /** @test */ public function catchupWithNoEventsKeepsThePreviousPositionOfTheSubscribers() { - $this->subscriptionService->setupEventStore(); + $this->eventStore->setup(); $this->fakeProjection->expects(self::once())->method('setUp'); $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); - $this->subscriptionService->subscriptionEngine->setup(); + $this->subscriptionEngine->setup(); - $result = $this->subscriptionService->subscriptionEngine->boot(); + $result = $this->subscriptionEngine->boot(); self::assertEquals(ProcessedResult::success(0), $result); $this->expectOkayStatus('contentGraph', SubscriptionStatus::ACTIVE, SequenceNumber::none()); $this->expectOkayStatus('Vendor.Package:SecondFakeProjection', SubscriptionStatus::ACTIVE, SequenceNumber::none()); @@ -101,13 +101,13 @@ public function catchupWithNoEventsKeepsThePreviousPositionOfTheSubscribers() // catchup active does apply the commited event $this->fakeProjection->expects(self::once())->method('apply')->with(self::isInstanceOf(ContentStreamWasCreated::class)); - $result = $this->subscriptionService->subscriptionEngine->catchUpActive(); + $result = $this->subscriptionEngine->catchUpActive(); self::assertEquals(ProcessedResult::success(1), $result); $this->expectOkayStatus('contentGraph', SubscriptionStatus::ACTIVE, SequenceNumber::fromInteger(1)); // empty catchup must keep the sequence numbers of the projections okay - $result = $this->subscriptionService->subscriptionEngine->catchUpActive(); + $result = $this->subscriptionEngine->catchUpActive(); self::assertEquals(ProcessedResult::success(0), $result); $this->expectOkayStatus('contentGraph', SubscriptionStatus::ACTIVE, SequenceNumber::fromInteger(1)); diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionBootingStatusTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionBootingStatusTest.php index 01b15d9137c..a89370bd57e 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionBootingStatusTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionBootingStatusTest.php @@ -23,20 +23,20 @@ public function existingEventStoreEventsAreCaughtUpOnBoot() $this->commitExampleContentStreamEvent(); $this->fakeProjection->expects(self::once())->method('setUp'); - $this->subscriptionService->subscriptionEngine->setup(); + $this->subscriptionEngine->setup(); $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); $this->expectOkayStatus('contentGraph', SubscriptionStatus::BOOTING, SequenceNumber::none()); $this->expectOkayStatus('Vendor.Package:FakeProjection', SubscriptionStatus::BOOTING, SequenceNumber::none()); $this->fakeProjection->expects(self::once())->method('apply')->with(self::isInstanceOf(ContentStreamWasCreated::class)); - $this->subscriptionService->subscriptionEngine->boot(); + $this->subscriptionEngine->boot(); $this->expectOkayStatus('contentGraph', SubscriptionStatus::ACTIVE, SequenceNumber::fromInteger(1)); $this->expectOkayStatus('Vendor.Package:FakeProjection', SubscriptionStatus::ACTIVE, SequenceNumber::fromInteger(1)); // catchup is a noop because there are no unhandled events - $result = $this->subscriptionService->subscriptionEngine->catchUpActive(); + $result = $this->subscriptionEngine->catchUpActive(); self::assertEquals(ProcessedResult::success(0), $result); } @@ -46,9 +46,9 @@ public function filteringCatchUpBoot() $this->fakeProjection->expects(self::once())->method('setUp'); $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); - $this->subscriptionService->setupEventStore(); + $this->eventStore->setup(); - $result = $this->subscriptionService->subscriptionEngine->setup(); + $result = $this->subscriptionEngine->setup(); self::assertNull($result->errors); diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionDetachedStatusTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionDetachedStatusTest.php index 2a45ec60c4e..855b68d473b 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionDetachedStatusTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionDetachedStatusTest.php @@ -28,10 +28,10 @@ public function projectionIsDetachedOnCatchupActive() $this->fakeProjection->expects(self::once())->method('setUp'); $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); - $this->subscriptionService->setupEventStore(); - $this->subscriptionService->subscriptionEngine->setup(); + $this->eventStore->setup(); + $this->subscriptionEngine->setup(); - $result = $this->subscriptionService->subscriptionEngine->boot(); + $result = $this->subscriptionEngine->boot(); self::assertEquals(ProcessedResult::success(0), $result); $this->expectOkayStatus('Vendor.Package:FakeProjection', SubscriptionStatus::ACTIVE, SequenceNumber::none()); @@ -96,12 +96,12 @@ public function projectionIsDetachedOnSetupAndReattachedIfPossible() $this->fakeProjection->expects(self::once())->method('apply'); $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); - $this->subscriptionService->setupEventStore(); - $this->subscriptionService->subscriptionEngine->setup(); + $this->eventStore->setup(); + $this->subscriptionEngine->setup(); $this->commitExampleContentStreamEvent(); - $result = $this->subscriptionService->subscriptionEngine->boot(); + $result = $this->subscriptionEngine->boot(); self::assertEquals(ProcessedResult::success(1), $result); $this->expectOkayStatus('Vendor.Package:FakeProjection', SubscriptionStatus::ACTIVE, SequenceNumber::fromInteger(1)); diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionGetStatusTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionGetStatusTest.php index 594c7264e92..000944385f3 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionGetStatusTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionGetStatusTest.php @@ -25,7 +25,7 @@ public function statusOnEmptyDatabase() keepSchema: false ); - $actualStatuses = $this->subscriptionService->subscriptionEngine->subscriptionStatuses(); + $actualStatuses = $this->subscriptionEngine->subscriptionStatuses(); self::assertTrue($actualStatuses->isEmpty()); self::assertNull( @@ -48,7 +48,7 @@ public function statusOnEmptyDatabase() $this->fakeProjection->expects(self::once())->method('status')->willReturn(ProjectionStatus::setupRequired('fake needs setup.')); - $actualStatuses = $this->subscriptionService->subscriptionEngine->subscriptionStatuses(); + $actualStatuses = $this->subscriptionEngine->subscriptionStatuses(); $expected = SubscriptionAndProjectionStatuses::fromArray([ SubscriptionAndProjectionStatus::create( diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionNewStatusTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionNewStatusTest.php index fc8e61beddc..7cc7c2c1670 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionNewStatusTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionNewStatusTest.php @@ -32,10 +32,10 @@ public function newProjectionIsFoundWhenConfigurationIsAdded() $this->fakeProjection->expects(self::exactly(2))->method('setUp'); $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); - $this->subscriptionService->setupEventStore(); - $this->subscriptionService->subscriptionEngine->setup(); + $this->eventStore->setup(); + $this->subscriptionEngine->setup(); - $result = $this->subscriptionService->subscriptionEngine->boot(); + $result = $this->subscriptionEngine->boot(); self::assertEquals(ProcessedResult::success(0), $result); self::assertNull($this->subscriptionStatus('Vendor.Package:NewFakeProjection')); diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionResetTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionResetTest.php index ea1a741ad57..cbebb621ceb 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionResetTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionResetTest.php @@ -18,9 +18,9 @@ public function filteringReset() $this->fakeProjection->expects(self::once())->method('setUp'); $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); - $this->subscriptionService->setupEventStore(); + $this->eventStore->setup(); - $result = $this->subscriptionService->subscriptionEngine->setup(); + $result = $this->subscriptionEngine->setup(); self::assertNull($result->errors); self::assertEmpty( $this->secondFakeProjection->getState()->findAppliedSequenceNumbers() diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionSetupTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionSetupTest.php index 1483e90851f..d3b3e6d5ada 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionSetupTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionSetupTest.php @@ -18,13 +18,13 @@ final class SubscriptionSetupTest extends AbstractSubscriptionEngineTestCase /** @test */ public function setupOnEmptyDatabase() { - $this->subscriptionService->setupEventStore(); + $this->eventStore->setup(); $this->fakeProjection->expects(self::once())->method('setUp'); - $this->subscriptionService->subscriptionEngine->setup(); + $this->subscriptionEngine->setup(); $this->fakeProjection->expects(self::exactly(2))->method('status')->willReturn(ProjectionStatus::ok()); - $actualStatuses = $this->subscriptionService->subscriptionEngine->subscriptionStatuses(); + $actualStatuses = $this->subscriptionEngine->subscriptionStatuses(); $expected = SubscriptionAndProjectionStatuses::fromArray([ SubscriptionAndProjectionStatus::create( @@ -67,11 +67,11 @@ public function filteringSetup() $this->fakeProjection->expects(self::once())->method('setUp'); $this->fakeProjection->expects(self::once())->method('status')->willReturn(ProjectionStatus::ok()); - $this->subscriptionService->setupEventStore(); + $this->eventStore->setup(); $filter = SubscriptionEngineCriteria::create([SubscriptionId::fromString('Vendor.Package:FakeProjection')]); - $result = $this->subscriptionService->subscriptionEngine->setup($filter); + $result = $this->subscriptionEngine->setup($filter); self::assertNull($result->errors); $this->expectOkayStatus('Vendor.Package:FakeProjection', SubscriptionStatus::BOOTING, SequenceNumber::none()); @@ -97,11 +97,11 @@ public function setupIsInvokedForBootingSubscribers() // hard reset, so that the tests actually need sql migrations $this->secondFakeProjection->dropTables(); - $this->subscriptionService->setupEventStore(); + $this->eventStore->setup(); // initial setup for FakeProjection - $result = $this->subscriptionService->subscriptionEngine->setup(); + $result = $this->subscriptionEngine->setup(); self::assertNull($result->errors); $this->expectOkayStatus('Vendor.Package:SecondFakeProjection', SubscriptionStatus::BOOTING, SequenceNumber::none()); @@ -120,7 +120,7 @@ public function setupIsInvokedForBootingSubscribers() $this->subscriptionStatus('Vendor.Package:SecondFakeProjection') ); - $result = $this->subscriptionService->subscriptionEngine->setup(); + $result = $this->subscriptionEngine->setup(); self::assertNull($result->errors); $this->expectOkayStatus('Vendor.Package:SecondFakeProjection', SubscriptionStatus::BOOTING, SequenceNumber::none()); @@ -138,9 +138,9 @@ public function setupIsInvokedForPreviouslyActiveSubscribers() // hard reset, so that the tests actually need sql migrations $this->secondFakeProjection->dropTables(); - $this->subscriptionService->setupEventStore(); + $this->eventStore->setup(); // setup subscription tables - $result = $this->subscriptionService->subscriptionEngine->setup(SubscriptionEngineCriteria::create([SubscriptionId::fromString('contentGraph')])); + $result = $this->subscriptionEngine->setup(SubscriptionEngineCriteria::create([SubscriptionId::fromString('contentGraph')])); self::assertNull($result->errors); self::assertEquals( @@ -156,17 +156,17 @@ public function setupIsInvokedForPreviouslyActiveSubscribers() // initial setup for FakeProjection - $result = $this->subscriptionService->subscriptionEngine->setup(); + $result = $this->subscriptionEngine->setup(); self::assertNull($result->errors); $this->expectOkayStatus('Vendor.Package:SecondFakeProjection', SubscriptionStatus::BOOTING, SequenceNumber::none()); - $result = $this->subscriptionService->subscriptionEngine->boot(); + $result = $this->subscriptionEngine->boot(); self::assertNull($result->errors); $this->expectOkayStatus('Vendor.Package:SecondFakeProjection', SubscriptionStatus::ACTIVE, SequenceNumber::none()); // regular work $this->commitExampleContentStreamEvent(); - $result = $this->subscriptionService->subscriptionEngine->catchUpActive(); + $result = $this->subscriptionEngine->catchUpActive(); self::assertNull($result->errors); $this->expectOkayStatus('Vendor.Package:SecondFakeProjection', SubscriptionStatus::ACTIVE, SequenceNumber::fromInteger(1)); @@ -186,7 +186,7 @@ public function setupIsInvokedForPreviouslyActiveSubscribers() $this->subscriptionStatus('Vendor.Package:SecondFakeProjection') ); - $result = $this->subscriptionService->subscriptionEngine->setup(); + $result = $this->subscriptionEngine->setup(); self::assertNull($result->errors); $this->expectOkayStatus('Vendor.Package:SecondFakeProjection', SubscriptionStatus::ACTIVE, SequenceNumber::fromInteger(1)); @@ -200,9 +200,9 @@ public function failingSetupWillMarkProjectionAsErrored() ); $this->fakeProjection->expects(self::once())->method('status')->willReturn(ProjectionStatus::setupRequired('Needs setup')); - $this->subscriptionService->setupEventStore(); + $this->eventStore->setup(); - $result = $this->subscriptionService->subscriptionEngine->setup(); + $result = $this->subscriptionEngine->setup(); self::assertSame('Projection could not be setup', $result->errors?->first()->message); $expectedFailure = SubscriptionAndProjectionStatus::create( diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Parallel/AbstractParallelTestCase.php b/Neos.ContentRepository.BehavioralTests/Tests/Parallel/AbstractParallelTestCase.php index d4646e8165e..efa68008845 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Parallel/AbstractParallelTestCase.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Parallel/AbstractParallelTestCase.php @@ -14,10 +14,9 @@ namespace Neos\ContentRepository\BehavioralTests\Tests\Parallel; -use Doctrine\DBAL\Connection; use Neos\ContentRepository\Core\ContentRepository; -use Neos\ContentRepository\Core\Service\SubscriptionService; -use Neos\ContentRepository\Core\Service\SubscriptionServiceFactory; +use Neos\ContentRepository\Core\Service\ContentRepositoryMaintainer; +use Neos\ContentRepository\Core\Service\ContentRepositoryMaintainerFactory; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Core\Bootstrap; @@ -71,19 +70,11 @@ final protected function setUpContentRepository( ContentRepositoryId $contentRepositoryId ): ContentRepository { $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); - /** @var SubscriptionService $subscriptionService */ - $subscriptionService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, new SubscriptionServiceFactory()); - $subscriptionService->setupEventStore(); - $subscriptionService->subscriptionEngine->setup(); - - $connection = $this->objectManager->get(Connection::class); - + /** @var ContentRepositoryMaintainer $contentRepositoryMaintainer */ + $contentRepositoryMaintainer = $this->contentRepositoryRegistry->buildService($contentRepositoryId, new ContentRepositoryMaintainerFactory()); + $contentRepositoryMaintainer->setUp(); // reset events and projections - $eventTableName = sprintf('cr_%s_events', $contentRepositoryId->value); - $connection->executeStatement('TRUNCATE ' . $eventTableName); - - $subscriptionService->subscriptionEngine->reset(); - $subscriptionService->subscriptionEngine->boot(); + $contentRepositoryMaintainer->prune(); return $contentRepository; } diff --git a/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php b/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php new file mode 100644 index 00000000000..1db74979dae --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php @@ -0,0 +1,146 @@ +eventStore->setup(); + $eventStoreIsEmpty = iterator_count($this->eventStore->load(VirtualStreamName::all())->limit(1)) === 0; + $setupResult = $this->subscriptionEngine->setup(); + if ($setupResult->errors !== null) { + return self::createErrorForReason('setup', $setupResult->errors); + } + if ($eventStoreIsEmpty) { + // todo reintroduce skipBooting flag, and also notify if the flag is not set, e.g. because there are events + $bootResult = $this->subscriptionEngine->boot(); + if ($bootResult->errors !== null) { + return self::createErrorForReason('initial catchup', $bootResult->errors); + } + } + return null; + } + + public function eventStoreStatus(): EventStoreStatus + { + return $this->eventStore->status(); + } + + public function subscriptionStatuses(): SubscriptionAndProjectionStatuses + { + return $this->subscriptionEngine->subscriptionStatuses(); + } + + public function replayProjection(SubscriptionId $subscriptionId, \Closure|null $progressCallback = null): Error|null + { + $resetResult = $this->subscriptionEngine->reset(SubscriptionEngineCriteria::create([$subscriptionId])); + if ($resetResult->errors !== null) { + return self::createErrorForReason('reset', $resetResult->errors); + } + $bootResult = $this->subscriptionEngine->boot(SubscriptionEngineCriteria::create([$subscriptionId]), $progressCallback); + if ($bootResult->errors !== null) { + return self::createErrorForReason('catchup', $bootResult->errors); + } + return null; + } + + public function replayAllProjections(\Closure|null $progressCallback = null): Error|null + { + $resetResult = $this->subscriptionEngine->reset(); + if ($resetResult->errors !== null) { + return self::createErrorForReason('reset', $resetResult->errors); + } + $bootResult = $this->subscriptionEngine->boot(progressCallback: $progressCallback); + if ($bootResult->errors !== null) { + return self::createErrorForReason('catchup', $bootResult->errors); + } + return null; + } + + /** + * Catchup one specific projection. + * + * The explicit catchup is required for new projections in the booting state. + * + * We don't offer an API to catch up all projections catchupAllProjection as we would have to distinct between booting or catchup if its active already. + * + * This method is only needed in rare cases for debugging or after installing a new projection or fixing its errors. + */ + public function catchupProjection(SubscriptionId $subscriptionId, \Closure|null $progressCallback = null): Error|null + { + $bootResult = $this->subscriptionEngine->boot(SubscriptionEngineCriteria::create([$subscriptionId]), progressCallback: $progressCallback); + if ($bootResult->errors !== null) { + return self::createErrorForReason('catchup', $bootResult->errors); + } + if ($bootResult->numberOfProcessedEvents > 0) { + // the projection was bootet + return null; + } + // todo the projection was active, and we might still want to catch it up ... find reason for this? And combine boot and catchup? + $catchupResult = $this->subscriptionEngine->catchUpActive(SubscriptionEngineCriteria::create([$subscriptionId]), progressCallback: $progressCallback); + if ($catchupResult->errors !== null) { + return self::createErrorForReason('catchup', $catchupResult->errors); + } + return null; + } + + /** + * WARNING: Removes all events from the content repository and resets the projections + * This operation cannot be undone. + */ + public function prune(): Error|null + { + // todo move pruneAllWorkspacesAndContentStreamsFromEventStream here. + $this->contentStreamPruner->pruneAllWorkspacesAndContentStreamsFromEventStream(); + $resetResult = $this->subscriptionEngine->reset(); + if ($resetResult->errors !== null) { + return self::createErrorForReason('reset', $resetResult->errors); + } + // todo reintroduce skipBooting flag to reset + $bootResult = $this->subscriptionEngine->boot(); + if ($bootResult->errors !== null) { + return self::createErrorForReason('booting', $bootResult->errors); + } + return null; + } + + private static function createErrorForReason(string $method, Errors $errors): Error + { + // todo log throwable via flow???, but we are here in the CORE ... + $message = []; + $message[] = sprintf('%s produced the following error%s', $method, $errors->count() === 1 ? '' : 's'); + foreach ($errors as $error) { + $message[] = sprintf(' Subscription "%s": %s', $error->subscriptionId->value, $error->message); + } + return new Error(join("\n", $message)); + } +} diff --git a/Neos.ContentRepository.Core/Classes/Service/SubscriptionServiceFactory.php b/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainerFactory.php similarity index 50% rename from Neos.ContentRepository.Core/Classes/Service/SubscriptionServiceFactory.php rename to Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainerFactory.php index 3fe17594800..e4ddcf4e76d 100644 --- a/Neos.ContentRepository.Core/Classes/Service/SubscriptionServiceFactory.php +++ b/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainerFactory.php @@ -8,17 +8,22 @@ use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryInterface; /** - * @implements ContentRepositoryServiceFactoryInterface + * @implements ContentRepositoryServiceFactoryInterface * @api */ -class SubscriptionServiceFactory implements ContentRepositoryServiceFactoryInterface +class ContentRepositoryMaintainerFactory implements ContentRepositoryServiceFactoryInterface { public function build( ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies - ): SubscriptionService { - return new SubscriptionService( + ): ContentRepositoryMaintainer { + return new ContentRepositoryMaintainer( $serviceFactoryDependencies->eventStore, $serviceFactoryDependencies->subscriptionEngine, + new ContentStreamPruner( + $serviceFactoryDependencies->eventStore, + $serviceFactoryDependencies->eventNormalizer, + $serviceFactoryDependencies->subscriptionEngine, + ) ); } } diff --git a/Neos.ContentRepository.Core/Classes/Service/SubscriptionService.php b/Neos.ContentRepository.Core/Classes/Service/SubscriptionService.php deleted file mode 100644 index 321df1534ff..00000000000 --- a/Neos.ContentRepository.Core/Classes/Service/SubscriptionService.php +++ /dev/null @@ -1,32 +0,0 @@ -eventStore->setup(); - } - - public function eventStoreStatus(): Status - { - return $this->eventStore->status(); - } -} diff --git a/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionEngine.php b/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionEngine.php index 0ad1265502c..5d8bb9ac086 100644 --- a/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionEngine.php +++ b/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionEngine.php @@ -5,8 +5,10 @@ namespace Neos\ContentRepository\Core\Subscription\Engine; use Doctrine\DBAL\Exception\TableNotFoundException; +use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\EventStore\EventInterface; use Neos\ContentRepository\Core\EventStore\EventNormalizer; +use Neos\ContentRepository\Core\Service\ContentRepositoryMaintainer; use Neos\ContentRepository\Core\Subscription\Exception\CatchUpFailed; use Neos\ContentRepository\Core\Subscription\Exception\SubscriptionEngineAlreadyProcessingException; use Neos\ContentRepository\Core\Subscription\SubscriptionAndProjectionStatus; @@ -25,7 +27,7 @@ use Neos\ContentRepository\Core\Subscription\Subscriptions; /** - * @api + * @internal public API is the {@see ContentRepository::handle()} and the {@see ContentRepositoryMaintainer} */ final class SubscriptionEngine { diff --git a/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionAndProjectionStatuses.php b/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionAndProjectionStatuses.php index 47c237813b5..1c3351ed666 100644 --- a/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionAndProjectionStatuses.php +++ b/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionAndProjectionStatuses.php @@ -51,4 +51,17 @@ public function isEmpty(): bool { return $this->statuses === []; } + + public function isOk(): bool + { + foreach ($this->statuses as $status) { + if ($status->subscriptionStatus === SubscriptionStatus::ERROR) { + return false; + } + if ($status->projectionStatus?->type !== ProjectionStatusType::OK) { + return false; + } + } + return true; + } } diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php index f65d0de69fc..777d5a9c1f3 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php @@ -27,10 +27,12 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\NodeType\NodeTypeCriteria; use Neos\ContentRepository\Core\Projection\ContentGraph\Subtree; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; +use Neos\ContentRepository\Core\Service\ContentRepositoryMaintainerFactory; use Neos\ContentRepository\Core\Service\ContentStreamPrunerFactory; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; +use Neos\ContentRepository\Core\Subscription\SubscriptionId; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Features\ContentStreamClosing; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Features\NodeCreation; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Features\NodeModification; @@ -254,8 +256,9 @@ abstract protected function getContentRepositoryService( */ public function iReplayTheProjection(string $projectionName): void { - $this->currentContentRepository->resetProjectionState($projectionName); - $this->currentContentRepository->catchUpProjection($projectionName, CatchUpOptions::create()); + $contentRepositoryMaintainer = $this->getContentRepositoryService(new ContentRepositoryMaintainerFactory()); + $result = $contentRepositoryMaintainer->replayProjection(SubscriptionId::fromString($projectionName)); + Assert::assertNull($result); } 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 51216bbbd7a..e4eeaedd33b 100644 --- a/Neos.ContentRepositoryRegistry.TestSuite/Classes/Behavior/CRRegistrySubjectProvider.php +++ b/Neos.ContentRepositoryRegistry.TestSuite/Classes/Behavior/CRRegistrySubjectProvider.php @@ -13,15 +13,14 @@ * source code. */ -use Doctrine\DBAL\Connection; use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryInterface; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; -use Neos\ContentRepository\Core\Service\SubscriptionServiceFactory; +use Neos\ContentRepository\Core\Service\ContentRepositoryMaintainerFactory; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\ContentRepositoryRegistry\Exception\ContentRepositoryNotFoundException; -use Neos\EventStore\EventStoreInterface; +use PHPUnit\Framework\Assert; /** * The CR registry subject provider trait for behavioral tests @@ -53,24 +52,18 @@ protected function setUpCRRegistry(): void /** * @Given /^I initialize content repository "([^"]*)"$/ */ - public function iInitializeContentRepository(string $contentRepositoryId): void + public function iInitializeContentRepository(string $rawContentRepositoryId): void { - $contentRepository = $this->getContentRepository(ContentRepositoryId::fromString($contentRepositoryId)); - $subscriptionService = $this->contentRepositoryRegistry->buildService($contentRepository->id, new SubscriptionServiceFactory()); - /** @var EventStoreInterface $eventStore */ - $eventStore = (new \ReflectionClass($contentRepository))->getProperty('eventStore')->getValue($contentRepository); - /** @var Connection $databaseConnection */ - $databaseConnection = (new \ReflectionClass($eventStore))->getProperty('connection')->getValue($eventStore); - $eventTableName = sprintf('cr_%s_events', $contentRepositoryId); - $databaseConnection->executeStatement('TRUNCATE ' . $eventTableName); + $contentRepositoryId = ContentRepositoryId::fromString($rawContentRepositoryId); - if (!in_array($contentRepository->id, self::$alreadySetUpContentRepositories)) { - $subscriptionService->setupEventStore(); - $subscriptionService->subscriptionEngine->setup(); - self::$alreadySetUpContentRepositories[] = $contentRepository->id; + $contentRepositoryMaintainer = $this->contentRepositoryRegistry->buildService($contentRepositoryId, new ContentRepositoryMaintainerFactory()); + if (!in_array($contentRepositoryId, self::$alreadySetUpContentRepositories)) { + $result = $contentRepositoryMaintainer->setUp(); + Assert::assertNull($result); + self::$alreadySetUpContentRepositories[] = $contentRepositoryId; } - $subscriptionService->subscriptionEngine->reset(); - $subscriptionService->subscriptionEngine->boot(); + $result = $contentRepositoryMaintainer->prune(); + Assert::assertNull($result); } /** diff --git a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php index 0e8ee668a87..d7a03eed6bb 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php +++ b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php @@ -4,26 +4,19 @@ namespace Neos\ContentRepositoryRegistry\Command; use Neos\ContentRepository\Core\Projection\ProjectionStatusType; -use Neos\ContentRepository\Core\Service\SubscriptionServiceFactory; +use Neos\ContentRepository\Core\Service\ContentRepositoryMaintainerFactory; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; -use Neos\ContentRepository\Core\Subscription\SubscriptionStatus; +use Neos\ContentRepository\Core\Subscription\SubscriptionId; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; -use Neos\EventStore\Model\Event\SequenceNumber; use Neos\EventStore\Model\EventStore\StatusType; +use Neos\Flow\Annotations as Flow; use Neos\Flow\Cli\CommandController; -use stdClass; -use Symfony\Component\Console\Helper\ProgressBar; -use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\Output; final class CrCommandController extends CommandController { - - public function __construct( - private readonly ContentRepositoryRegistry $contentRepositoryRegistry, - ) { - parent::__construct(); - } + #[Flow\Inject()] + protected ContentRepositoryRegistry $contentRepositoryRegistry; /** * Sets up and checks required dependencies for a Content Repository instance @@ -40,73 +33,14 @@ public function __construct( public function setupCommand(string $contentRepository = 'default'): void { $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); - $subscriptionService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, new SubscriptionServiceFactory()); - $subscriptionService->setupEventStore(); - $setupResult = $subscriptionService->subscriptionEngine->setup(); - if ($setupResult->errors === null) { - $this->outputLine('Content Repository "%s" was set up', [$contentRepositoryId->value]); - return; - } - $this->outputLine('Setup of Content Repository "%s" produced the following error%s', [$contentRepositoryId->value, $setupResult->errors->count() === 1 ? '' : 's']); - foreach ($setupResult->errors as $error) { - $this->outputLine('Subscription "%s": %s', [$error->subscriptionId->value, $error->message]); - } - $this->quit(1); - } + $contentRepositoryMaintainer = $this->contentRepositoryRegistry->buildService($contentRepositoryId, new ContentRepositoryMaintainerFactory()); - public function subscriptionsBootCommand(string $contentRepository = 'default', bool $quiet = false): void - { - $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); - $subscriptionService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, new SubscriptionServiceFactory()); - if (!$quiet) { - $this->outputLine('Booting new subscriptions'); - // render memory consumption and time remaining - $this->output->getProgressBar()->setFormat('debug'); - $this->output->progressStart(); - $bootResult = $subscriptionService->subscriptionEngine->boot(progressCallback: fn () => $this->output->progressAdvance()); - $this->output->progressFinish(); - $this->outputLine(); - if ($bootResult->hasFailed() === false) { - $this->outputLine('Done'); - return; - } - } else { - $bootResult = $subscriptionService->subscriptionEngine->boot(); - } - if ($bootResult->hasFailed()) { - $this->outputLine('Booting of Content Repository "%s" produced the following error%s', [$contentRepositoryId->value, $bootResult->errors->count() === 1 ? '' : 's']); - foreach ($bootResult->errors as $error) { - $this->outputLine('Subscription "%s": %s', [$error->subscriptionId->value, $error->message]); - } + $result = $contentRepositoryMaintainer->setUp(); + if ($result !== null) { + $this->outputLine('%s', [$result->getMessage()]); $this->quit(1); } - } - - public function subscriptionsCatchUpCommand(string $contentRepository = 'default', bool $quiet = false): void - { - $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); - $subscriptionService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, new SubscriptionServiceFactory()); - $subscriptionService->subscriptionEngine->catchUpActive(); - } - - public function subscriptionsResetCommand(string $contentRepository = 'default', bool $force = false): void - { - if (!$force && !$this->output->askConfirmation('Are you sure? (y/n) ', false)) { - $this->outputLine('Cancelled'); - $this->quit(); - } - $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); - $subscriptionService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, new SubscriptionServiceFactory()); - $resetResult = $subscriptionService->subscriptionEngine->reset(); - if ($resetResult->errors === null) { - $this->outputLine('Content Repository "%s" was reset', [$contentRepositoryId->value]); - return; - } - $this->outputLine('Reset of Content Repository "%s" produced the following error%s', [$contentRepositoryId->value, $resetResult->errors->count() === 1 ? '' : 's']); - foreach ($resetResult->errors as $error) { - $this->outputLine('Subscription "%s": %s', [$error->subscriptionId->value, $error->message]); - } - $this->quit(1); + $this->outputLine('Content Repository "%s" was set up', [$contentRepositoryId->value]); } /** @@ -124,86 +58,184 @@ public function statusCommand(string $contentRepository = 'default', bool $verbo $this->output->getOutput()->setVerbosity(Output::VERBOSITY_QUIET); } $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); - $subscriptionService = $this->contentRepositoryRegistry->buildService($contentRepositoryId, new SubscriptionServiceFactory()); - $eventStoreStatus = $subscriptionService->eventStoreStatus(); - $hasErrors = false; - $setupRequired = false; - $bootingRequired = false; - $resetRequired = false; + $contentRepositoryMaintainer = $this->contentRepositoryRegistry->buildService($contentRepositoryId, new ContentRepositoryMaintainerFactory()); + $eventStoreStatus = $contentRepositoryMaintainer->eventStoreStatus(); $this->output('Event Store: '); $this->outputLine(match ($eventStoreStatus->type) { StatusType::OK => 'OK', StatusType::SETUP_REQUIRED => 'Setup required!', StatusType::ERROR => 'ERROR', }); - $hasErrors |= $eventStoreStatus->type === StatusType::ERROR; if ($verbose && $eventStoreStatus->details !== '') { $this->outputFormatted($eventStoreStatus->details, [], 2); } $this->outputLine(); - $this->outputLine('Subscriptions:'); - $subscriptionStatuses = $subscriptionService->subscriptionEngine->subscriptionStatuses(); - if ($subscriptionStatuses->isEmpty()) { - $this->outputLine('There are no registered subscriptions yet, please run ./flow cr:setup'); - $this->quit(1); - } - foreach ($subscriptionStatuses as $status) { - $this->outputLine(' %s:', [$status->subscriptionId->value]); - $this->output(' Subscription: ', [$status->subscriptionId->value]); - $this->output(match ($status->subscriptionStatus) { - SubscriptionStatus::NEW => 'NEW', - SubscriptionStatus::BOOTING => 'BOOTING', - SubscriptionStatus::ACTIVE => 'ACTIVE', - SubscriptionStatus::DETACHED => 'DETACHED', - SubscriptionStatus::ERROR => 'ERROR', + + $subscriptionStatuses = $contentRepositoryMaintainer->subscriptionStatuses(); + foreach ($subscriptionStatuses as $subscriptionStatus) { + // todo reimplement 40e8d35e09ee690406c6a9cfc823c775d4ee3b51 + $this->output('Projection "%s": ', [$subscriptionStatus->subscriptionId->value]); + $projectionStatus = $subscriptionStatus->projectionStatus; + if ($projectionStatus === null) { + $this->outputLine('No status available.'); // todo this means detached? + continue; + } + $this->outputLine(match ($projectionStatus->type) { + ProjectionStatusType::OK => 'OK', + ProjectionStatusType::SETUP_REQUIRED => 'Setup required!', + ProjectionStatusType::REPLAY_REQUIRED => 'Replay required!', + ProjectionStatusType::ERROR => 'ERROR', }); - $this->outputLine(' at position %d', [$status->subscriptionPosition->value]); - $hasErrors |= $status->subscriptionStatus === SubscriptionStatus::ERROR; - $bootingRequired |= $status->subscriptionStatus === SubscriptionStatus::BOOTING; - if ($verbose && $status->subscriptionError !== null) { - $lines = explode(chr(10), $status->subscriptionError->errorMessage ?: 'No details available.'); + if ($verbose && ($projectionStatus->type !== ProjectionStatusType::OK || $projectionStatus->details)) { + $lines = explode(chr(10), $projectionStatus->details ?: 'No details available.'); foreach ($lines as $line) { - $this->outputLine(' %s', [$line]); - } - } - if ($status->projectionStatus !== null) { - $this->output(' Projection: '); - $this->outputLine(match ($status->projectionStatus->type) { - ProjectionStatusType::OK => 'OK', - ProjectionStatusType::SETUP_REQUIRED => 'Setup required!', - ProjectionStatusType::REPLAY_REQUIRED => 'Replay required!', - ProjectionStatusType::ERROR => 'ERROR', - }); - $hasErrors |= $status->projectionStatus->type === ProjectionStatusType::ERROR; - $setupRequired |= $status->projectionStatus->type === ProjectionStatusType::SETUP_REQUIRED; - $resetRequired |= $status->projectionStatus->type === ProjectionStatusType::REPLAY_REQUIRED; - if ($verbose && ($status->projectionStatus->type !== ProjectionStatusType::OK || $status->projectionStatus->details)) { - $lines = explode(chr(10), $status->projectionStatus->details ?: 'No details available.'); - foreach ($lines as $line) { - $this->outputLine(' ' . $line); - } - $this->outputLine(); + $this->outputLine(' ' . $line); } + $this->outputLine(); } } - if ($verbose) { + if ($eventStoreStatus->type !== StatusType::OK || !$subscriptionStatuses->isOk()) { + $this->quit(1); + } + } + + /** + * Replays the specified projection of a Content Repository by resetting its state and performing a full catchup. + * + * @param string $projection Identifier of the projection to replay like it was configured (e.g. "contentGraph", "Vendor.Package:YourProjection") + * @param string $contentRepository Identifier of the Content Repository instance to operate on + * @param bool $force Replay the projection without confirmation. This may take some time! + * @param bool $quiet If set only fatal errors are rendered to the output (must be used with --force flag to avoid user input) + */ + public function projectionReplayCommand(string $projection, string $contentRepository = 'default', bool $force = false, bool $quiet = false): void + { + if ($quiet) { + $this->output->getOutput()->setVerbosity(Output::VERBOSITY_QUIET); + } + if (!$force && $quiet) { + $this->outputLine('Cannot run in quiet mode without --force. Please acknowledge that this command will reset and replay this projection. This may take some time.'); + $this->quit(1); + } + + if (!$force && !$this->output->askConfirmation(sprintf('> This will replay the projection "%s" in "%s", which may take some time. Are you sure to proceed? (y/n) ', $projection, $contentRepository), false)) { + $this->outputLine('Abort.'); + return; + } + + $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); + $contentRepositoryMaintainer = $this->contentRepositoryRegistry->buildService($contentRepositoryId, new ContentRepositoryMaintainerFactory()); + + $progressCallback = null; + if (!$quiet) { + $this->outputLine('Replaying events for projection "%s" of Content Repository "%s" ...', [$projection, $contentRepositoryId->value]); + // render memory consumption and time remaining + $this->output->getProgressBar()->setFormat('debug'); + $this->output->progressStart(); + $progressCallback = fn () => $this->output->progressAdvance(); + } + + $result = $contentRepositoryMaintainer->replayProjection(SubscriptionId::fromString($projection), progressCallback: $progressCallback); + + if (!$quiet) { + $this->output->progressFinish(); $this->outputLine(); - if ($setupRequired) { - $this->outputLine('Setup required, please run ./flow cr:setup'); - } - if ($bootingRequired) { - $this->outputLine('Some subscriptions need to be booted, please run ./flow cr:subscriptionsboot'); - } - if ($resetRequired) { - $this->outputLine('Some subscriptions need to be replayed, please run ./flow cr:subscriptionsreset'); - } - if ($hasErrors) { - $this->outputLine('Some subscriptions/projections have failed'); - } } - if ($hasErrors) { + + if ($result !== null) { + $this->outputLine('%s', [$result->getMessage()]); + $this->quit(1); + } elseif (!$quiet) { + $this->outputLine('Done.'); + } + } + + /** + * Replays all projections of the specified Content Repository by resetting their states and performing a full catchup + * + * @param string $contentRepository Identifier of the Content Repository instance to operate on + * @param bool $force Replay the projection without confirmation. This may take some time! + * @param bool $quiet If set only fatal errors are rendered to the output (must be used with --force flag to avoid user input) + */ + public function projectionReplayAllCommand(string $contentRepository = 'default', bool $force = false, bool $quiet = false): void + { + if ($quiet) { + $this->output->getOutput()->setVerbosity(Output::VERBOSITY_QUIET); + } + + if (!$force && $quiet) { + $this->outputLine('Cannot run in quiet mode without --force. Please acknowledge that this command will reset and replay this projection. This may take some time.'); + $this->quit(1); + } + + if (!$force && !$this->output->askConfirmation(sprintf('> This will replay all projections in "%s", which may take some time. Are you sure to proceed? (y/n) ', $contentRepository), false)) { + $this->outputLine('Abort.'); + return; + } + + $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); + $contentRepositoryMaintainer = $this->contentRepositoryRegistry->buildService($contentRepositoryId, new ContentRepositoryMaintainerFactory()); + + $progressCallback = null; + if (!$quiet) { + $this->outputLine('Replaying events for all projections of Content Repository "%s" ...', [$contentRepositoryId->value]); + // render memory consumption and time remaining + // todo maybe reintroduce pretty output: https://github.com/neos/neos-development-collection/pull/5010 but without using highestSequenceNumber + $this->output->getProgressBar()->setFormat('debug'); + $this->output->progressStart(); + $progressCallback = fn () => $this->output->progressAdvance(); + } + + $result = $contentRepositoryMaintainer->replayAllProjections(progressCallback: $progressCallback); + + if (!$quiet) { + $this->output->progressFinish(); + $this->outputLine(); + } + + if ($result !== null) { + $this->outputLine('%s', [$result->getMessage()]); + $this->quit(1); + } elseif (!$quiet) { + $this->outputLine('Done.'); + } + } + + /** + * Catchup one specific projection. + * + * The explicit catchup is required for new projections in the booting state, after installing a new projection or fixing its errors. + * + * @param string $projection Identifier of the projection to catchup like it was configured (e.g. "contentGraph", "Vendor.Package:YourProjection") + * @param string $contentRepository Identifier of the Content Repository instance to operate on + * @param bool $quiet If set only fatal errors are rendered to the output (must be used with --force flag to avoid user input) + */ + public function projectionCatchupCommand(string $projection, string $contentRepository = 'default', bool $quiet = false): void + { + $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); + $contentRepositoryMaintainer = $this->contentRepositoryRegistry->buildService($contentRepositoryId, new ContentRepositoryMaintainerFactory()); + + $progressCallback = null; + if (!$quiet) { + $this->outputLine('Catchup projection "%s" of Content Repository "%s" ...', [$projection, $contentRepositoryId->value]); + // render memory consumption and time remaining + $this->output->getProgressBar()->setFormat('debug'); + $this->output->progressStart(); + $progressCallback = fn () => $this->output->progressAdvance(); + } + + $result = $contentRepositoryMaintainer->catchupProjection(SubscriptionId::fromString($projection), progressCallback: $progressCallback); + + if (!$quiet) { + $this->output->progressFinish(); + $this->outputLine(); + } + + if ($result !== null) { + $this->outputLine('%s', [$result->getMessage()]); $this->quit(1); + } elseif (!$quiet) { + $this->outputLine('Done.'); } } } diff --git a/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionCatchupProcessor.php b/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionReplayProcessor.php similarity index 53% rename from Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionCatchupProcessor.php rename to Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionReplayProcessor.php index 71bada85b93..b97a37eb27d 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionCatchupProcessor.php +++ b/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionReplayProcessor.php @@ -3,22 +3,22 @@ namespace Neos\ContentRepositoryRegistry\Processors; -use Neos\ContentRepository\Core\Service\SubscriptionService; +use Neos\ContentRepository\Core\Service\ContentRepositoryMaintainer; use Neos\ContentRepository\Export\ProcessingContext; use Neos\ContentRepository\Export\ProcessorInterface; /** * @internal */ -final readonly class ProjectionCatchupProcessor implements ProcessorInterface +final readonly class ProjectionReplayProcessor implements ProcessorInterface { public function __construct( - private SubscriptionService $subscriptionService, + private ContentRepositoryMaintainer $contentRepositoryMaintainer, ) { } public function run(ProcessingContext $context): void { - $this->subscriptionService->subscriptionEngine->catchUpActive(); + $this->contentRepositoryMaintainer->replayAllProjections(); } } diff --git a/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionResetProcessor.php b/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionResetProcessor.php deleted file mode 100644 index 1dfdd7d4208..00000000000 --- a/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionResetProcessor.php +++ /dev/null @@ -1,24 +0,0 @@ -subscriptionService->subscriptionEngine->reset(); - } -} diff --git a/Neos.Neos/Classes/Domain/Pruning/ContentRepositoryPruningProcessor.php b/Neos.Neos/Classes/Domain/Pruning/ContentRepositoryPruningProcessor.php index 0b94195c9d1..9cb7cf60b24 100644 --- a/Neos.Neos/Classes/Domain/Pruning/ContentRepositoryPruningProcessor.php +++ b/Neos.Neos/Classes/Domain/Pruning/ContentRepositoryPruningProcessor.php @@ -14,22 +14,25 @@ namespace Neos\Neos\Domain\Pruning; -use Neos\ContentRepository\Core\Service\ContentStreamPruner; +use Neos\ContentRepository\Core\Service\ContentRepositoryMaintainer; use Neos\ContentRepository\Export\ProcessingContext; use Neos\ContentRepository\Export\ProcessorInterface; /** - * Pruning processor that removes all events from the given cr + * Pruning processor that removes all events from the given cr and resets the projections */ final readonly class ContentRepositoryPruningProcessor implements ProcessorInterface { public function __construct( - private ContentStreamPruner $contentStreamPruner, + private ContentRepositoryMaintainer $contentRepositoryMaintainer, ) { } public function run(ProcessingContext $context): void { - $this->contentStreamPruner->pruneAllWorkspacesAndContentStreamsFromEventStream(); + $result = $this->contentRepositoryMaintainer->prune(); + if ($result !== null) { + throw new \RuntimeException($result->getMessage(), 1732461335); + } } } diff --git a/Neos.Neos/Classes/Domain/Service/SiteImportService.php b/Neos.Neos/Classes/Domain/Service/SiteImportService.php index 494b4779c8f..e0489cc6440 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteImportService.php +++ b/Neos.Neos/Classes/Domain/Service/SiteImportService.php @@ -17,8 +17,8 @@ use Doctrine\DBAL\Exception as DBALException; use League\Flysystem\Filesystem; use League\Flysystem\Local\LocalFilesystemAdapter; -use Neos\ContentRepository\Core\ContentRepository; -use Neos\ContentRepository\Core\Service\SubscriptionServiceFactory; +use Neos\ContentRepository\Core\Service\ContentRepositoryMaintainer; +use Neos\ContentRepository\Core\Service\ContentRepositoryMaintainerFactory; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\Export\Factory\EventStoreImportProcessorFactory; @@ -28,7 +28,8 @@ use Neos\ContentRepository\Export\Processors\AssetRepositoryImportProcessor; use Neos\ContentRepository\Export\Severity; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; -use Neos\ContentRepositoryRegistry\Processors\ProjectionCatchupProcessor; +use Neos\ContentRepositoryRegistry\Processors\ProjectionReplayProcessor; +use Neos\EventStore\Model\EventStore\StatusType; use Neos\Flow\Annotations as Flow; use Neos\Flow\Persistence\Doctrine\Service as DoctrineService; use Neos\Flow\Persistence\PersistenceManagerInterface; @@ -67,8 +68,10 @@ public function importFromPath(ContentRepositoryId $contentRepositoryId, string } $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); + $contentRepositoryMaintainer = $this->contentRepositoryRegistry->buildService($contentRepositoryId, new ContentRepositoryMaintainerFactory()); + $this->requireDataBaseSchemaToBeSetup(); - $this->requireContentRepositoryToBeSetup($contentRepository); + $this->requireContentRepositoryToBeSetup($contentRepositoryMaintainer, $contentRepositoryId); $filesystem = new Filesystem(new LocalFilesystemAdapter($path)); $context = new ProcessingContext($filesystem, $onMessage); @@ -78,7 +81,8 @@ public function importFromPath(ContentRepositoryId $contentRepositoryId, string 'Create Neos sites' => new SiteCreationProcessor($this->siteRepository, $this->domainRepository, $this->persistenceManager), 'Import events' => $this->contentRepositoryRegistry->buildService($contentRepositoryId, new EventStoreImportProcessorFactory(WorkspaceName::forLive(), keepEventIds: true)), 'Import assets' => new AssetRepositoryImportProcessor($this->assetRepository, $this->resourceRepository, $this->resourceManager, $this->persistenceManager), - 'Catchup all projections' => new ProjectionCatchupProcessor($this->contentRepositoryRegistry->buildService($contentRepositoryId, new SubscriptionServiceFactory())), + // todo we do a replay here even though it will redo the live workspace creation. But otherwise the catchup hooks are not skipped because it seems like a regular catchup + 'Catchup all projections' => new ProjectionReplayProcessor($contentRepositoryMaintainer), ]); foreach ($processors as $processorLabel => $processor) { @@ -87,13 +91,13 @@ public function importFromPath(ContentRepositoryId $contentRepositoryId, string } } - private function requireContentRepositoryToBeSetup(ContentRepository $contentRepository): void + private function requireContentRepositoryToBeSetup(ContentRepositoryMaintainer $contentRepositoryMaintainer, ContentRepositoryId $contentRepositoryId): void { -// TODO reimplement -// $status = $contentRepository->status(); -// if (!$status->isOk()) { -// throw new \RuntimeException(sprintf('Content repository %s is not setup correctly, please run `./flow cr:setup`', $contentRepository->id->value)); -// } + $eventStoreStatus = $contentRepositoryMaintainer->eventStoreStatus(); + $subscriptionStatuses = $contentRepositoryMaintainer->subscriptionStatuses(); + if ($eventStoreStatus->type !== StatusType::OK || !$subscriptionStatuses->isOk()) { + throw new \RuntimeException(sprintf('Content repository %s is not setup correctly, please run `./flow cr:setup`', $contentRepositoryId->value)); + } } private function requireDataBaseSchemaToBeSetup(): void diff --git a/Neos.Neos/Classes/Domain/Service/SitePruningService.php b/Neos.Neos/Classes/Domain/Service/SitePruningService.php index 968ec5d9427..18ab6f1d78f 100644 --- a/Neos.Neos/Classes/Domain/Service/SitePruningService.php +++ b/Neos.Neos/Classes/Domain/Service/SitePruningService.php @@ -16,8 +16,7 @@ use League\Flysystem\Filesystem; use League\Flysystem\Local\LocalFilesystemAdapter; -use Neos\ContentRepository\Core\Service\ContentStreamPrunerFactory; -use Neos\ContentRepository\Core\Service\SubscriptionServiceFactory; +use Neos\ContentRepository\Core\Service\ContentRepositoryMaintainerFactory; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\Export\ProcessingContext; @@ -25,7 +24,6 @@ use Neos\ContentRepository\Export\Processors; use Neos\ContentRepository\Export\Severity; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; -use Neos\ContentRepositoryRegistry\Processors\ProjectionResetProcessor; use Neos\Flow\Annotations as Flow; use Neos\Flow\Persistence\PersistenceManagerInterface; use Neos\Neos\Domain\Pruning\ContentRepositoryPruningProcessor; @@ -66,17 +64,11 @@ public function pruneAll(ContentRepositoryId $contentRepositoryId, \Closure $onP $this->domainRepository, $this->persistenceManager ), - 'Prune content repository' => new ContentRepositoryPruningProcessor( - $this->contentRepositoryRegistry->buildService( - $contentRepositoryId, - new ContentStreamPrunerFactory() - ) - ), 'Prune roles and metadata' => new RoleAndMetadataPruningProcessor($contentRepositoryId, $this->workspaceMetadataAndRoleRepository), - 'Reset all projections' => new ProjectionResetProcessor( + 'Prune content repository' => new ContentRepositoryPruningProcessor( $this->contentRepositoryRegistry->buildService( $contentRepositoryId, - new SubscriptionServiceFactory() + new ContentRepositoryMaintainerFactory() ) ) ]); diff --git a/Neos.Neos/Tests/Behavior/Features/FrontendRouting/Basic.feature b/Neos.Neos/Tests/Behavior/Features/FrontendRouting/Basic.feature index 640ac7e62bf..c9fde1e6e6b 100644 --- a/Neos.Neos/Tests/Behavior/Features/FrontendRouting/Basic.feature +++ b/Neos.Neos/Tests/Behavior/Features/FrontendRouting/Basic.feature @@ -113,7 +113,7 @@ Feature: Basic routing functionality (match & resolve document nodes in one dime # !!! when caches were still enabled (without calling DocumentUriPathFinder->disableCache()), the replay below will # show really "interesting" (non-correct) results. This was bug #4253. - When I replay the "Neos\Neos\FrontendRouting\Projection\DocumentUriPathProjection" projection + When I replay the "Neos.Neos:DocumentUriPathProjection" projection Then the node "sir-david-nodenborough" in content stream "cs-identifier" and dimension "{}" should resolve to URL "/david-nodenborough-updated-b" And the node "earl-o-documentbourgh" in content stream "cs-identifier" and dimension "{}" should resolve to URL "/david-nodenborough-updated-b/earl-document" From bace8ffed072862b24b5763b51b38a6b66020fd5 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 24 Nov 2024 19:32:05 +0100 Subject: [PATCH 02/24] TASK: Rename `ProjectionStatus` and introduce `ProjectionSubscriptionStatus` Like the `ProjectionSubscription` projections subscribers for projections will have an explicit state: `ProjectionSubscriptionStatus`. If its extended to allow other subscribers another status type should be introduced. replayRequired todo remove we cannot figurea that out in the status after all! --- .../DoctrineDbalContentGraphProjection.php | 12 ++--- .../Projection/HypergraphProjection.php | 12 ++--- .../TestSuite/DebugEventProjection.php | 8 +-- .../AbstractSubscriptionEngineTestCase.php | 10 ++-- .../Subscription/CatchUpHookErrorTest.php | 12 ++--- .../Subscription/ProjectionErrorTest.php | 26 ++++----- .../SubscriptionActiveStatusTest.php | 8 +-- .../SubscriptionBootingStatusTest.php | 6 +-- .../SubscriptionDetachedStatusTest.php | 20 +++---- .../SubscriptionGetStatusTest.php | 22 ++++---- .../SubscriptionNewStatusTest.php | 18 +++---- .../Subscription/SubscriptionResetTest.php | 4 +- .../Subscription/SubscriptionSetupTest.php | 50 ++++++++--------- .../Projection/ProjectionInterface.php | 4 +- .../Projection/ProjectionSetupStatus.php | 38 +++++++++++++ ...Type.php => ProjectionSetupStatusType.php} | 5 +- .../Classes/Projection/ProjectionStatus.php | 54 ------------------- .../Service/ContentRepositoryMaintainer.php | 4 +- .../Engine/SubscriptionEngine.php | 14 ++--- ...s.php => ProjectionSubscriptionStatus.php} | 13 +++-- .../Subscription/Subscriber/Subscribers.php | 6 +++ ...nStatuses.php => SubscriptionStatuses.php} | 22 +++++--- .../Classes/Command/CrCommandController.php | 17 +++--- .../Projection/DocumentUriPathProjection.php | 12 ++--- .../ChangeProjection.php | 12 ++--- 25 files changed, 204 insertions(+), 205 deletions(-) create mode 100644 Neos.ContentRepository.Core/Classes/Projection/ProjectionSetupStatus.php rename Neos.ContentRepository.Core/Classes/Projection/{ProjectionStatusType.php => ProjectionSetupStatusType.php} (75%) delete mode 100644 Neos.ContentRepository.Core/Classes/Projection/ProjectionStatus.php rename Neos.ContentRepository.Core/Classes/Subscription/{SubscriptionAndProjectionStatus.php => ProjectionSubscriptionStatus.php} (70%) rename Neos.ContentRepository.Core/Classes/Subscription/{SubscriptionAndProjectionStatuses.php => SubscriptionStatuses.php} (56%) diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php index 4e998fde99a..a436c61d14f 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php @@ -67,7 +67,7 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphReadModelInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeTags; use Neos\ContentRepository\Core\Projection\ContentGraph\Timestamps; -use Neos\ContentRepository\Core\Projection\ProjectionStatus; +use Neos\ContentRepository\Core\Projection\ProjectionSetupStatus; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateClassification; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; @@ -112,23 +112,23 @@ public function setUp(): void } } - public function status(): ProjectionStatus + public function setUpStatus(): ProjectionSetupStatus { try { $this->dbal->connect(); } catch (\Throwable $e) { - return ProjectionStatus::error(sprintf('Failed to connect to database: %s', $e->getMessage())); + return ProjectionSetupStatus::error(sprintf('Failed to connect to database: %s', $e->getMessage())); } try { $requiredSqlStatements = $this->determineRequiredSqlStatements(); } catch (\Throwable $e) { - return ProjectionStatus::error(sprintf('Failed to determine required SQL statements: %s', $e->getMessage())); + return ProjectionSetupStatus::error(sprintf('Failed to determine required SQL statements: %s', $e->getMessage())); } if ($requiredSqlStatements !== []) { - return ProjectionStatus::setupRequired(sprintf('The following SQL statement%s required: %s', count($requiredSqlStatements) !== 1 ? 's are' : ' is', implode(chr(10), $requiredSqlStatements))); + return ProjectionSetupStatus::setupRequired(sprintf('The following SQL statement%s required: %s', count($requiredSqlStatements) !== 1 ? 's are' : ' is', implode(chr(10), $requiredSqlStatements))); } - return ProjectionStatus::ok(); + return ProjectionSetupStatus::ok(); } public function resetState(): void diff --git a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Projection/HypergraphProjection.php b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Projection/HypergraphProjection.php index 5961442b9e6..12640cfdd72 100644 --- a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Projection/HypergraphProjection.php +++ b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Projection/HypergraphProjection.php @@ -42,7 +42,7 @@ use Neos\ContentRepository\Core\Infrastructure\DbalSchemaDiff; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphProjectionInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphReadModelInterface; -use Neos\ContentRepository\Core\Projection\ProjectionStatus; +use Neos\ContentRepository\Core\Projection\ProjectionSetupStatus; use Neos\EventStore\Model\EventEnvelope; /** @@ -89,22 +89,22 @@ public function setUp(): void '); } - public function status(): ProjectionStatus + public function setUpStatus(): ProjectionSetupStatus { try { $this->getDatabaseConnection()->connect(); } catch (\Throwable $e) { - return ProjectionStatus::error(sprintf('Failed to connect to database: %s', $e->getMessage())); + return ProjectionSetupStatus::error(sprintf('Failed to connect to database: %s', $e->getMessage())); } try { $requiredSqlStatements = $this->determineRequiredSqlStatements(); } catch (\Throwable $e) { - return ProjectionStatus::error(sprintf('Failed to determine required SQL statements: %s', $e->getMessage())); + return ProjectionSetupStatus::error(sprintf('Failed to determine required SQL statements: %s', $e->getMessage())); } if ($requiredSqlStatements !== []) { - return ProjectionStatus::setupRequired(sprintf('The following SQL statement%s required: %s', count($requiredSqlStatements) !== 1 ? 's are' : ' is', implode(chr(10), $requiredSqlStatements))); + return ProjectionSetupStatus::setupRequired(sprintf('The following SQL statement%s required: %s', count($requiredSqlStatements) !== 1 ? 's are' : ' is', implode(chr(10), $requiredSqlStatements))); } - return ProjectionStatus::ok(); + return ProjectionSetupStatus::ok(); } /** diff --git a/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/DebugEventProjection.php b/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/DebugEventProjection.php index 7f71796e96d..4fcd47f66fd 100644 --- a/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/DebugEventProjection.php +++ b/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/DebugEventProjection.php @@ -14,7 +14,7 @@ use Neos\ContentRepository\Core\Infrastructure\DbalSchemaFactory; use Neos\ContentRepository\Core\Projection\ProjectionInterface; use Neos\ContentRepository\Core\Projection\ProjectionStateInterface; -use Neos\ContentRepository\Core\Projection\ProjectionStatus; +use Neos\ContentRepository\Core\Projection\ProjectionSetupStatus; use Neos\EventStore\Model\EventEnvelope; use Neos\Flow\Annotations as Flow; @@ -52,13 +52,13 @@ public function setUp(): void } } - public function status(): ProjectionStatus + public function setUpStatus(): ProjectionSetupStatus { $requiredSqlStatements = $this->determineRequiredSqlStatements(); if ($requiredSqlStatements !== []) { - return ProjectionStatus::setupRequired(sprintf('Requires %d SQL statements', count($requiredSqlStatements))); + return ProjectionSetupStatus::setupRequired(sprintf('Requires %d SQL statements', count($requiredSqlStatements))); } - return ProjectionStatus::ok(); + return ProjectionSetupStatus::ok(); } /** diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/AbstractSubscriptionEngineTestCase.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/AbstractSubscriptionEngineTestCase.php index c4e9506aee9..bc48af8b01c 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/AbstractSubscriptionEngineTestCase.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/AbstractSubscriptionEngineTestCase.php @@ -14,12 +14,12 @@ use Neos\ContentRepository\Core\Projection\CatchUpHook\CatchUpHookInterface; use Neos\ContentRepository\Core\Projection\ProjectionInterface; use Neos\ContentRepository\Core\Projection\ProjectionStateInterface; -use Neos\ContentRepository\Core\Projection\ProjectionStatus; +use Neos\ContentRepository\Core\Projection\ProjectionSetupStatus; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\Subscription\Engine\SubscriptionEngine; use Neos\ContentRepository\Core\Subscription\Store\SubscriptionCriteria; -use Neos\ContentRepository\Core\Subscription\SubscriptionAndProjectionStatus; +use Neos\ContentRepository\Core\Subscription\ProjectionSubscriptionStatus; use Neos\ContentRepository\Core\Subscription\SubscriptionId; use Neos\ContentRepository\Core\Subscription\SubscriptionStatus; use Neos\ContentRepository\TestSuite\Fakes\FakeCatchUpHookFactory; @@ -134,7 +134,7 @@ final protected function resetDatabase(Connection $connection, ContentRepository $connection->prepare('SET FOREIGN_KEY_CHECKS = 1;')->executeStatement(); } - final protected function subscriptionStatus(string $subscriptionId): ?SubscriptionAndProjectionStatus + final protected function subscriptionStatus(string $subscriptionId): ?ProjectionSubscriptionStatus { return $this->subscriptionEngine->subscriptionStatuses(SubscriptionCriteria::create(ids: [SubscriptionId::fromString($subscriptionId)]))->first(); } @@ -156,12 +156,12 @@ final protected function expectOkayStatus($subscriptionId, SubscriptionStatus $s { $actual = $this->subscriptionStatus($subscriptionId); self::assertEquals( - SubscriptionAndProjectionStatus::create( + ProjectionSubscriptionStatus::create( subscriptionId: SubscriptionId::fromString($subscriptionId), subscriptionStatus: $status, subscriptionPosition: $sequenceNumber, subscriptionError: null, - projectionStatus: ProjectionStatus::ok(), + setupStatus: ProjectionSetupStatus::ok(), ), $actual ); diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/CatchUpHookErrorTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/CatchUpHookErrorTest.php index 9948144bbad..eeb3300229e 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/CatchUpHookErrorTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/CatchUpHookErrorTest.php @@ -5,9 +5,9 @@ namespace Neos\ContentRepository\BehavioralTests\Tests\Functional\Subscription; use Neos\ContentRepository\Core\Feature\ContentStreamCreation\Event\ContentStreamWasCreated; -use Neos\ContentRepository\Core\Projection\ProjectionStatus; +use Neos\ContentRepository\Core\Projection\ProjectionSetupStatus; use Neos\ContentRepository\Core\Subscription\Exception\CatchUpFailed; -use Neos\ContentRepository\Core\Subscription\SubscriptionAndProjectionStatus; +use Neos\ContentRepository\Core\Subscription\ProjectionSubscriptionStatus; use Neos\ContentRepository\Core\Subscription\SubscriptionError; use Neos\ContentRepository\Core\Subscription\SubscriptionId; use Neos\ContentRepository\Core\Subscription\SubscriptionStatus; @@ -37,12 +37,12 @@ public function error_onBeforeEvent_projectionIsNotRun() $this->secondFakeProjection->injectSaboteur(fn () => self::fail('Projection apply is not expected to be called!')); - $expectedFailure = SubscriptionAndProjectionStatus::create( + $expectedFailure = ProjectionSubscriptionStatus::create( subscriptionId: SubscriptionId::fromString('Vendor.Package:SecondFakeProjection'), subscriptionStatus: SubscriptionStatus::ERROR, subscriptionPosition: SequenceNumber::none(), subscriptionError: SubscriptionError::fromPreviousStatusAndException(SubscriptionStatus::ACTIVE, $exception), - projectionStatus: ProjectionStatus::ok(), + setupStatus: ProjectionSetupStatus::ok(), ); self::assertEmpty( @@ -83,12 +83,12 @@ public function error_onAfterEvent_projectionIsRolledBack() // TODO pass the error subscription status to onAfterCatchUp, so that in case of an error it can be prevented that mails f.x. will be sent? $this->catchupHookForFakeProjection->expects(self::once())->method('onAfterCatchUp'); - $expectedFailure = SubscriptionAndProjectionStatus::create( + $expectedFailure = ProjectionSubscriptionStatus::create( subscriptionId: SubscriptionId::fromString('Vendor.Package:SecondFakeProjection'), subscriptionStatus: SubscriptionStatus::ERROR, subscriptionPosition: SequenceNumber::none(), subscriptionError: SubscriptionError::fromPreviousStatusAndException(SubscriptionStatus::ACTIVE, $exception), - projectionStatus: ProjectionStatus::ok(), + setupStatus: ProjectionSetupStatus::ok(), ); self::assertEmpty( diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/ProjectionErrorTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/ProjectionErrorTest.php index 8f82f1ef9e9..03085882caf 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/ProjectionErrorTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/ProjectionErrorTest.php @@ -6,13 +6,13 @@ use Neos\ContentRepository\Core\Feature\ContentStreamCreation\Event\ContentStreamWasCreated; use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateRootWorkspace; -use Neos\ContentRepository\Core\Projection\ProjectionStatus; +use Neos\ContentRepository\Core\Projection\ProjectionSetupStatus; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\Core\Subscription\Engine\Error; use Neos\ContentRepository\Core\Subscription\Engine\Errors; use Neos\ContentRepository\Core\Subscription\Engine\ProcessedResult; -use Neos\ContentRepository\Core\Subscription\SubscriptionAndProjectionStatus; +use Neos\ContentRepository\Core\Subscription\ProjectionSubscriptionStatus; use Neos\ContentRepository\Core\Subscription\SubscriptionError; use Neos\ContentRepository\Core\Subscription\SubscriptionId; use Neos\ContentRepository\Core\Subscription\SubscriptionStatus; @@ -28,7 +28,7 @@ public function projectionWithError() $this->eventStore->setup(); $this->fakeProjection->expects(self::once())->method('setUp'); $this->subscriptionEngine->setup(); - $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); + $this->fakeProjection->expects(self::any())->method('setUpStatus')->willReturn(ProjectionSetupStatus::ok()); $result = $this->subscriptionEngine->boot(); self::assertEquals(ProcessedResult::success(0), $result); $this->expectOkayStatus('contentGraph', SubscriptionStatus::ACTIVE, SequenceNumber::none()); @@ -41,12 +41,12 @@ public function projectionWithError() $this->fakeProjection->expects(self::once())->method('apply')->with(self::isInstanceOf(ContentStreamWasCreated::class))->willThrowException( $exception = new \RuntimeException('This projection is kaputt.') ); - $expectedStatusForFailedProjection = SubscriptionAndProjectionStatus::create( + $expectedStatusForFailedProjection = ProjectionSubscriptionStatus::create( subscriptionId: SubscriptionId::fromString('Vendor.Package:FakeProjection'), subscriptionStatus: SubscriptionStatus::ERROR, subscriptionPosition: SequenceNumber::none(), subscriptionError: SubscriptionError::fromPreviousStatusAndException(SubscriptionStatus::ACTIVE, $exception), - projectionStatus: ProjectionStatus::ok(), + setupStatus: ProjectionSetupStatus::ok(), ); $result = $this->subscriptionEngine->catchUpActive(); @@ -80,7 +80,7 @@ public function fixFailedProjection() { $this->eventStore->setup(); $this->fakeProjection->expects(self::once())->method('setUp'); - $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); + $this->fakeProjection->expects(self::any())->method('setUpStatus')->willReturn(ProjectionSetupStatus::ok()); $this->subscriptionEngine->setup(); $this->subscriptionEngine->boot(); @@ -93,12 +93,12 @@ public function fixFailedProjection() null // okay again ); - $expectedFailure = SubscriptionAndProjectionStatus::create( + $expectedFailure = ProjectionSubscriptionStatus::create( subscriptionId: SubscriptionId::fromString('Vendor.Package:FakeProjection'), subscriptionStatus: SubscriptionStatus::ERROR, subscriptionPosition: SequenceNumber::none(), subscriptionError: SubscriptionError::fromPreviousStatusAndException(SubscriptionStatus::ACTIVE, $exception), - projectionStatus: ProjectionStatus::ok(), + setupStatus: ProjectionSetupStatus::ok(), ); $result = $this->subscriptionEngine->catchUpActive(); @@ -153,12 +153,12 @@ public function projectionIsRolledBackAfterError() $this->secondFakeProjection->injectSaboteur(fn () => throw $exception); - $expectedFailure = SubscriptionAndProjectionStatus::create( + $expectedFailure = ProjectionSubscriptionStatus::create( subscriptionId: SubscriptionId::fromString('Vendor.Package:SecondFakeProjection'), subscriptionStatus: SubscriptionStatus::ERROR, subscriptionPosition: SequenceNumber::none(), subscriptionError: SubscriptionError::fromPreviousStatusAndException(SubscriptionStatus::ACTIVE, $exception), - projectionStatus: ProjectionStatus::ok(), + setupStatus: ProjectionSetupStatus::ok(), ); self::assertEmpty( @@ -230,12 +230,12 @@ public function projectionIsRolledBackAfterErrorButKeepsSuccessFullEvents() $result = $this->subscriptionEngine->catchUpActive(); self::assertTrue($result->hasFailed()); - $expectedFailure = SubscriptionAndProjectionStatus::create( + $expectedFailure = ProjectionSubscriptionStatus::create( subscriptionId: SubscriptionId::fromString('Vendor.Package:SecondFakeProjection'), subscriptionStatus: SubscriptionStatus::ERROR, subscriptionPosition: SequenceNumber::fromInteger(1), subscriptionError: SubscriptionError::fromPreviousStatusAndException(SubscriptionStatus::ACTIVE, $exception), - projectionStatus: ProjectionStatus::ok(), + setupStatus: ProjectionSetupStatus::ok(), ); self::assertEquals( @@ -280,7 +280,7 @@ public function projectionErrorWithMultipleProjectionsInContentRepositoryHandle( { $this->eventStore->setup(); $this->fakeProjection->expects(self::once())->method('setUp'); - $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); + $this->fakeProjection->expects(self::any())->method('setUpStatus')->willReturn(ProjectionSetupStatus::ok()); $this->subscriptionEngine->setup(); $this->subscriptionEngine->boot(); diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionActiveStatusTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionActiveStatusTest.php index 421d9a4c212..a23b4e0080d 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionActiveStatusTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionActiveStatusTest.php @@ -5,7 +5,7 @@ namespace Neos\ContentRepository\BehavioralTests\Tests\Functional\Subscription; use Neos\ContentRepository\Core\Feature\ContentStreamCreation\Event\ContentStreamWasCreated; -use Neos\ContentRepository\Core\Projection\ProjectionStatus; +use Neos\ContentRepository\Core\Projection\ProjectionSetupStatus; use Neos\ContentRepository\Core\Subscription\Engine\ProcessedResult; use Neos\ContentRepository\Core\Subscription\Engine\SubscriptionEngineCriteria; use Neos\ContentRepository\Core\Subscription\SubscriptionId; @@ -24,7 +24,7 @@ public function setupProjectionsAndCatchup() $result = $this->subscriptionEngine->boot(); self::assertEquals(ProcessedResult::success(0), $result); - $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); + $this->fakeProjection->expects(self::any())->method('setUpStatus')->willReturn(ProjectionSetupStatus::ok()); $this->expectOkayStatus('contentGraph', SubscriptionStatus::ACTIVE, SequenceNumber::none()); $this->expectOkayStatus('Vendor.Package:FakeProjection', SubscriptionStatus::ACTIVE, SequenceNumber::none()); $this->expectOkayStatus('Vendor.Package:SecondFakeProjection', SubscriptionStatus::ACTIVE, SequenceNumber::none()); @@ -53,7 +53,7 @@ public function setupProjectionsAndCatchup() public function filteringCatchUpActive() { $this->fakeProjection->expects(self::once())->method('setUp'); - $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); + $this->fakeProjection->expects(self::any())->method('setUpStatus')->willReturn(ProjectionSetupStatus::ok()); $this->eventStore->setup(); @@ -88,7 +88,7 @@ public function catchupWithNoEventsKeepsThePreviousPositionOfTheSubscribers() $this->eventStore->setup(); $this->fakeProjection->expects(self::once())->method('setUp'); - $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); + $this->fakeProjection->expects(self::any())->method('setUpStatus')->willReturn(ProjectionSetupStatus::ok()); $this->subscriptionEngine->setup(); $result = $this->subscriptionEngine->boot(); diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionBootingStatusTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionBootingStatusTest.php index a89370bd57e..7c8a005dd83 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionBootingStatusTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionBootingStatusTest.php @@ -5,7 +5,7 @@ namespace Neos\ContentRepository\BehavioralTests\Tests\Functional\Subscription; use Neos\ContentRepository\Core\Feature\ContentStreamCreation\Event\ContentStreamWasCreated; -use Neos\ContentRepository\Core\Projection\ProjectionStatus; +use Neos\ContentRepository\Core\Projection\ProjectionSetupStatus; use Neos\ContentRepository\Core\Subscription\Engine\ProcessedResult; use Neos\ContentRepository\Core\Subscription\Engine\SubscriptionEngineCriteria; use Neos\ContentRepository\Core\Subscription\SubscriptionId; @@ -24,7 +24,7 @@ public function existingEventStoreEventsAreCaughtUpOnBoot() $this->fakeProjection->expects(self::once())->method('setUp'); $this->subscriptionEngine->setup(); - $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); + $this->fakeProjection->expects(self::any())->method('setUpStatus')->willReturn(ProjectionSetupStatus::ok()); $this->expectOkayStatus('contentGraph', SubscriptionStatus::BOOTING, SequenceNumber::none()); $this->expectOkayStatus('Vendor.Package:FakeProjection', SubscriptionStatus::BOOTING, SequenceNumber::none()); @@ -44,7 +44,7 @@ public function existingEventStoreEventsAreCaughtUpOnBoot() public function filteringCatchUpBoot() { $this->fakeProjection->expects(self::once())->method('setUp'); - $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); + $this->fakeProjection->expects(self::any())->method('setUpStatus')->willReturn(ProjectionSetupStatus::ok()); $this->eventStore->setup(); diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionDetachedStatusTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionDetachedStatusTest.php index 855b68d473b..68a00a94969 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionDetachedStatusTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionDetachedStatusTest.php @@ -4,9 +4,9 @@ namespace Neos\ContentRepository\BehavioralTests\Tests\Functional\Subscription; -use Neos\ContentRepository\Core\Projection\ProjectionStatus; +use Neos\ContentRepository\Core\Projection\ProjectionSetupStatus; use Neos\ContentRepository\Core\Subscription\Engine\ProcessedResult; -use Neos\ContentRepository\Core\Subscription\SubscriptionAndProjectionStatus; +use Neos\ContentRepository\Core\Subscription\ProjectionSubscriptionStatus; use Neos\ContentRepository\Core\Subscription\SubscriptionId; use Neos\ContentRepository\Core\Subscription\SubscriptionStatus; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; @@ -26,7 +26,7 @@ public function resetContentRepositoryRegistry(): void public function projectionIsDetachedOnCatchupActive() { $this->fakeProjection->expects(self::once())->method('setUp'); - $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); + $this->fakeProjection->expects(self::any())->method('setUpStatus')->willReturn(ProjectionSetupStatus::ok()); $this->eventStore->setup(); $this->subscriptionEngine->setup(); @@ -54,12 +54,12 @@ public function projectionIsDetachedOnCatchupActive() // todo result should reflect that there was an detachment? Throw error in CR? self::assertEquals(ProcessedResult::success(1), $result); - $expectedDetachedState = SubscriptionAndProjectionStatus::create( + $expectedDetachedState = ProjectionSubscriptionStatus::create( subscriptionId: SubscriptionId::fromString('Vendor.Package:FakeProjection'), subscriptionStatus: SubscriptionStatus::DETACHED, subscriptionPosition: SequenceNumber::none(), subscriptionError: null, - projectionStatus: null // not calculate-able at this point! + setupStatus: null // not calculate-able at this point! ); self::assertEquals( $expectedDetachedState, @@ -94,7 +94,7 @@ public function projectionIsDetachedOnSetupAndReattachedIfPossible() { $this->fakeProjection->expects(self::exactly(2))->method('setUp'); $this->fakeProjection->expects(self::once())->method('apply'); - $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); + $this->fakeProjection->expects(self::any())->method('setUpStatus')->willReturn(ProjectionSetupStatus::ok()); $this->eventStore->setup(); $this->subscriptionEngine->setup(); @@ -120,12 +120,12 @@ public function projectionIsDetachedOnSetupAndReattachedIfPossible() // todo result should reflect that there was an detachment? self::assertNull($result->errors); - $expectedDetachedState = SubscriptionAndProjectionStatus::create( + $expectedDetachedState = ProjectionSubscriptionStatus::create( subscriptionId: SubscriptionId::fromString('Vendor.Package:FakeProjection'), subscriptionStatus: SubscriptionStatus::DETACHED, subscriptionPosition: SequenceNumber::fromInteger(1), subscriptionError: null, - projectionStatus: null // not calculate-able at this point! + setupStatus: null // not calculate-able at this point! ); self::assertEquals( $expectedDetachedState, @@ -147,12 +147,12 @@ public function projectionIsDetachedOnSetupAndReattachedIfPossible() $this->setupContentRepositoryDependencies($this->contentRepository->id); self::assertEquals( - SubscriptionAndProjectionStatus::create( + ProjectionSubscriptionStatus::create( subscriptionId: SubscriptionId::fromString('Vendor.Package:FakeProjection'), subscriptionStatus: SubscriptionStatus::DETACHED, subscriptionPosition: SequenceNumber::fromInteger(1), subscriptionError: null, - projectionStatus: ProjectionStatus::ok() // state _IS_ calculate-able at this point, todo better reflect meaning: is detached, but re-attachable! + setupStatus: ProjectionSetupStatus::ok() // state _IS_ calculate-able at this point, todo better reflect meaning: is detached, but re-attachable! ), $this->subscriptionStatus('Vendor.Package:FakeProjection') ); diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionGetStatusTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionGetStatusTest.php index 000944385f3..5090d3ee72f 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionGetStatusTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionGetStatusTest.php @@ -5,10 +5,10 @@ namespace Neos\ContentRepository\BehavioralTests\Tests\Functional\Subscription; use Doctrine\DBAL\Connection; -use Neos\ContentRepository\Core\Projection\ProjectionStatus; +use Neos\ContentRepository\Core\Projection\ProjectionSetupStatus; use Neos\ContentRepository\Core\Subscription\Engine\SubscriptionEngineCriteria; -use Neos\ContentRepository\Core\Subscription\SubscriptionAndProjectionStatus; -use Neos\ContentRepository\Core\Subscription\SubscriptionAndProjectionStatuses; +use Neos\ContentRepository\Core\Subscription\ProjectionSubscriptionStatus; +use Neos\ContentRepository\Core\Subscription\SubscriptionStatuses; use Neos\ContentRepository\Core\Subscription\SubscriptionId; use Neos\ContentRepository\Core\Subscription\SubscriptionStatus; use Neos\EventStore\Model\Event\SequenceNumber; @@ -46,31 +46,31 @@ public function statusOnEmptyDatabase() $this->subscriptionEngine->setup(SubscriptionEngineCriteria::create([SubscriptionId::fromString('contentGraph')])); $this->expectOkayStatus('contentGraph', SubscriptionStatus::BOOTING, SequenceNumber::none()); - $this->fakeProjection->expects(self::once())->method('status')->willReturn(ProjectionStatus::setupRequired('fake needs setup.')); + $this->fakeProjection->expects(self::once())->method('setUpStatus')->willReturn(ProjectionSetupStatus::setupRequired('fake needs setup.')); $actualStatuses = $this->subscriptionEngine->subscriptionStatuses(); - $expected = SubscriptionAndProjectionStatuses::fromArray([ - SubscriptionAndProjectionStatus::create( + $expected = SubscriptionStatuses::fromArray([ + ProjectionSubscriptionStatus::create( subscriptionId: SubscriptionId::fromString('contentGraph'), subscriptionStatus: SubscriptionStatus::BOOTING, subscriptionPosition: SequenceNumber::none(), subscriptionError: null, - projectionStatus: ProjectionStatus::ok(), + setupStatus: ProjectionSetupStatus::ok(), ), - SubscriptionAndProjectionStatus::create( + ProjectionSubscriptionStatus::create( subscriptionId: SubscriptionId::fromString('Vendor.Package:FakeProjection'), subscriptionStatus: SubscriptionStatus::NEW, subscriptionPosition: SequenceNumber::none(), subscriptionError: null, - projectionStatus: ProjectionStatus::setupRequired('fake needs setup.'), + setupStatus: ProjectionSetupStatus::setupRequired('fake needs setup.'), ), - SubscriptionAndProjectionStatus::create( + ProjectionSubscriptionStatus::create( subscriptionId: SubscriptionId::fromString('Vendor.Package:SecondFakeProjection'), subscriptionStatus: SubscriptionStatus::NEW, subscriptionPosition: SequenceNumber::none(), subscriptionError: null, - projectionStatus: ProjectionStatus::setupRequired('Requires 1 SQL statements'), + setupStatus: ProjectionSetupStatus::setupRequired('Requires 1 SQL statements'), ), ]); diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionNewStatusTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionNewStatusTest.php index 7cc7c2c1670..2d7db9869fa 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionNewStatusTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionNewStatusTest.php @@ -6,10 +6,10 @@ use Neos\ContentRepository\Core\Projection\ProjectionInterface; use Neos\ContentRepository\Core\Projection\ProjectionStateInterface; -use Neos\ContentRepository\Core\Projection\ProjectionStatus; +use Neos\ContentRepository\Core\Projection\ProjectionSetupStatus; use Neos\ContentRepository\Core\Subscription\Engine\ProcessedResult; use Neos\ContentRepository\Core\Subscription\Engine\SubscriptionEngineCriteria; -use Neos\ContentRepository\Core\Subscription\SubscriptionAndProjectionStatus; +use Neos\ContentRepository\Core\Subscription\ProjectionSubscriptionStatus; use Neos\ContentRepository\Core\Subscription\SubscriptionId; use Neos\ContentRepository\Core\Subscription\SubscriptionStatus; use Neos\ContentRepository\TestSuite\Fakes\FakeProjectionFactory; @@ -30,7 +30,7 @@ public function resetContentRepositoryRegistry(): void public function newProjectionIsFoundWhenConfigurationIsAdded() { $this->fakeProjection->expects(self::exactly(2))->method('setUp'); - $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); + $this->fakeProjection->expects(self::any())->method('setUpStatus')->willReturn(ProjectionSetupStatus::ok()); $this->eventStore->setup(); $this->subscriptionEngine->setup(); @@ -43,10 +43,10 @@ public function newProjectionIsFoundWhenConfigurationIsAdded() $newFakeProjection = $this->getMockBuilder(ProjectionInterface::class)->disableAutoReturnValueGeneration()->getMock(); $newFakeProjection->method('getState')->willReturn(new class implements ProjectionStateInterface {}); - $newFakeProjection->expects(self::exactly(3))->method('status')->willReturnOnConsecutiveCalls( - ProjectionStatus::setupRequired('Set me up'), - ProjectionStatus::ok(), - ProjectionStatus::ok(), + $newFakeProjection->expects(self::exactly(3))->method('setUpStatus')->willReturnOnConsecutiveCalls( + ProjectionSetupStatus::setupRequired('Set me up'), + ProjectionSetupStatus::ok(), + ProjectionSetupStatus::ok(), ); FakeProjectionFactory::setProjection( @@ -73,12 +73,12 @@ public function newProjectionIsFoundWhenConfigurationIsAdded() self::assertNull($result->errors); self::assertEquals( - SubscriptionAndProjectionStatus::create( + ProjectionSubscriptionStatus::create( subscriptionId: SubscriptionId::fromString('Vendor.Package:NewFakeProjection'), subscriptionStatus: SubscriptionStatus::NEW, subscriptionPosition: SequenceNumber::none(), subscriptionError: null, - projectionStatus: ProjectionStatus::setupRequired('Set me up') + setupStatus: ProjectionSetupStatus::setupRequired('Set me up') ), $this->subscriptionStatus('Vendor.Package:NewFakeProjection') ); diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionResetTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionResetTest.php index cbebb621ceb..b5eb56b12d9 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionResetTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionResetTest.php @@ -4,7 +4,7 @@ namespace Neos\ContentRepository\BehavioralTests\Tests\Functional\Subscription; -use Neos\ContentRepository\Core\Projection\ProjectionStatus; +use Neos\ContentRepository\Core\Projection\ProjectionSetupStatus; use Neos\ContentRepository\Core\Subscription\Engine\SubscriptionEngineCriteria; use Neos\ContentRepository\Core\Subscription\SubscriptionId; use Neos\ContentRepository\Core\Subscription\SubscriptionStatus; @@ -16,7 +16,7 @@ final class SubscriptionResetTest extends AbstractSubscriptionEngineTestCase public function filteringReset() { $this->fakeProjection->expects(self::once())->method('setUp'); - $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); + $this->fakeProjection->expects(self::any())->method('setUpStatus')->willReturn(ProjectionSetupStatus::ok()); $this->eventStore->setup(); diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionSetupTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionSetupTest.php index d3b3e6d5ada..4bfaf10ad59 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionSetupTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionSetupTest.php @@ -4,10 +4,10 @@ namespace Neos\ContentRepository\BehavioralTests\Tests\Functional\Subscription; -use Neos\ContentRepository\Core\Projection\ProjectionStatus; +use Neos\ContentRepository\Core\Projection\ProjectionSetupStatus; use Neos\ContentRepository\Core\Subscription\Engine\SubscriptionEngineCriteria; -use Neos\ContentRepository\Core\Subscription\SubscriptionAndProjectionStatus; -use Neos\ContentRepository\Core\Subscription\SubscriptionAndProjectionStatuses; +use Neos\ContentRepository\Core\Subscription\ProjectionSubscriptionStatus; +use Neos\ContentRepository\Core\Subscription\SubscriptionStatuses; use Neos\ContentRepository\Core\Subscription\SubscriptionError; use Neos\ContentRepository\Core\Subscription\SubscriptionId; use Neos\ContentRepository\Core\Subscription\SubscriptionStatus; @@ -23,30 +23,30 @@ public function setupOnEmptyDatabase() $this->fakeProjection->expects(self::once())->method('setUp'); $this->subscriptionEngine->setup(); - $this->fakeProjection->expects(self::exactly(2))->method('status')->willReturn(ProjectionStatus::ok()); + $this->fakeProjection->expects(self::exactly(2))->method('setUpStatus')->willReturn(ProjectionSetupStatus::ok()); $actualStatuses = $this->subscriptionEngine->subscriptionStatuses(); - $expected = SubscriptionAndProjectionStatuses::fromArray([ - SubscriptionAndProjectionStatus::create( + $expected = SubscriptionStatuses::fromArray([ + ProjectionSubscriptionStatus::create( subscriptionId: SubscriptionId::fromString('contentGraph'), subscriptionStatus: SubscriptionStatus::BOOTING, subscriptionPosition: SequenceNumber::none(), subscriptionError: null, - projectionStatus: ProjectionStatus::ok(), + setupStatus: ProjectionSetupStatus::ok(), ), - SubscriptionAndProjectionStatus::create( + ProjectionSubscriptionStatus::create( subscriptionId: SubscriptionId::fromString('Vendor.Package:FakeProjection'), subscriptionStatus: SubscriptionStatus::BOOTING, subscriptionPosition: SequenceNumber::none(), subscriptionError: null, - projectionStatus: ProjectionStatus::ok(), + setupStatus: ProjectionSetupStatus::ok(), ), - SubscriptionAndProjectionStatus::create( + ProjectionSubscriptionStatus::create( subscriptionId: SubscriptionId::fromString('Vendor.Package:SecondFakeProjection'), subscriptionStatus: SubscriptionStatus::BOOTING, subscriptionPosition: SequenceNumber::none(), subscriptionError: null, - projectionStatus: ProjectionStatus::ok(), + setupStatus: ProjectionSetupStatus::ok(), ), ]); @@ -65,7 +65,7 @@ public function setupOnEmptyDatabase() public function filteringSetup() { $this->fakeProjection->expects(self::once())->method('setUp'); - $this->fakeProjection->expects(self::once())->method('status')->willReturn(ProjectionStatus::ok()); + $this->fakeProjection->expects(self::once())->method('setUpStatus')->willReturn(ProjectionSetupStatus::ok()); $this->eventStore->setup(); @@ -77,12 +77,12 @@ public function filteringSetup() $this->expectOkayStatus('Vendor.Package:FakeProjection', SubscriptionStatus::BOOTING, SequenceNumber::none()); self::assertEquals( - SubscriptionAndProjectionStatus::create( + ProjectionSubscriptionStatus::create( subscriptionId: SubscriptionId::fromString('Vendor.Package:SecondFakeProjection'), subscriptionStatus: SubscriptionStatus::NEW, subscriptionPosition: SequenceNumber::none(), subscriptionError: null, - projectionStatus: ProjectionStatus::ok() + setupStatus: ProjectionSetupStatus::ok() ), $this->subscriptionStatus('Vendor.Package:SecondFakeProjection') ); @@ -92,7 +92,7 @@ public function filteringSetup() public function setupIsInvokedForBootingSubscribers() { $this->fakeProjection->expects(self::exactly(2))->method('setUp'); - $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); + $this->fakeProjection->expects(self::any())->method('setUpStatus')->willReturn(ProjectionSetupStatus::ok()); // hard reset, so that the tests actually need sql migrations $this->secondFakeProjection->dropTables(); @@ -110,12 +110,12 @@ public function setupIsInvokedForBootingSubscribers() $this->secondFakeProjection->schemaNeedsAdditionalColumn('column_after_update'); self::assertEquals( - SubscriptionAndProjectionStatus::create( + ProjectionSubscriptionStatus::create( subscriptionId: SubscriptionId::fromString('Vendor.Package:SecondFakeProjection'), subscriptionStatus: SubscriptionStatus::BOOTING, subscriptionPosition: SequenceNumber::none(), subscriptionError: null, - projectionStatus: ProjectionStatus::setupRequired('Requires 1 SQL statements') + setupStatus: ProjectionSetupStatus::setupRequired('Requires 1 SQL statements') ), $this->subscriptionStatus('Vendor.Package:SecondFakeProjection') ); @@ -133,7 +133,7 @@ public function setupIsInvokedForPreviouslyActiveSubscribers() $this->fakeProjection->expects(self::exactly(2))->method('setUp'); $this->fakeProjection->expects(self::once())->method('apply'); - $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); + $this->fakeProjection->expects(self::any())->method('setUpStatus')->willReturn(ProjectionSetupStatus::ok()); // hard reset, so that the tests actually need sql migrations $this->secondFakeProjection->dropTables(); @@ -144,12 +144,12 @@ public function setupIsInvokedForPreviouslyActiveSubscribers() self::assertNull($result->errors); self::assertEquals( - SubscriptionAndProjectionStatus::create( + ProjectionSubscriptionStatus::create( subscriptionId: SubscriptionId::fromString('Vendor.Package:SecondFakeProjection'), subscriptionStatus: SubscriptionStatus::NEW, subscriptionPosition: SequenceNumber::none(), subscriptionError: null, - projectionStatus: ProjectionStatus::setupRequired('Requires 1 SQL statements') + setupStatus: ProjectionSetupStatus::setupRequired('Requires 1 SQL statements') ), $this->subscriptionStatus('Vendor.Package:SecondFakeProjection') ); @@ -176,12 +176,12 @@ public function setupIsInvokedForPreviouslyActiveSubscribers() $this->secondFakeProjection->schemaNeedsAdditionalColumn('column_after_update'); self::assertEquals( - SubscriptionAndProjectionStatus::create( + ProjectionSubscriptionStatus::create( subscriptionId: SubscriptionId::fromString('Vendor.Package:SecondFakeProjection'), subscriptionStatus: SubscriptionStatus::ACTIVE, subscriptionPosition: SequenceNumber::fromInteger(1), subscriptionError: null, - projectionStatus: ProjectionStatus::setupRequired('Requires 1 SQL statements') + setupStatus: ProjectionSetupStatus::setupRequired('Requires 1 SQL statements') ), $this->subscriptionStatus('Vendor.Package:SecondFakeProjection') ); @@ -198,19 +198,19 @@ public function failingSetupWillMarkProjectionAsErrored() $this->fakeProjection->expects(self::once())->method('setUp')->willThrowException( $exception = new \RuntimeException('Projection could not be setup') ); - $this->fakeProjection->expects(self::once())->method('status')->willReturn(ProjectionStatus::setupRequired('Needs setup')); + $this->fakeProjection->expects(self::once())->method('setUpStatus')->willReturn(ProjectionSetupStatus::setupRequired('Needs setup')); $this->eventStore->setup(); $result = $this->subscriptionEngine->setup(); self::assertSame('Projection could not be setup', $result->errors?->first()->message); - $expectedFailure = SubscriptionAndProjectionStatus::create( + $expectedFailure = ProjectionSubscriptionStatus::create( subscriptionId: SubscriptionId::fromString('Vendor.Package:FakeProjection'), subscriptionStatus: SubscriptionStatus::ERROR, subscriptionPosition: SequenceNumber::none(), subscriptionError: SubscriptionError::fromPreviousStatusAndException(SubscriptionStatus::NEW, $exception), - projectionStatus: ProjectionStatus::setupRequired('Needs setup'), + setupStatus: ProjectionSetupStatus::setupRequired('Needs setup'), ); self::assertEquals( diff --git a/Neos.ContentRepository.Core/Classes/Projection/ProjectionInterface.php b/Neos.ContentRepository.Core/Classes/Projection/ProjectionInterface.php index 7e195ff4644..1b5a8519102 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ProjectionInterface.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ProjectionInterface.php @@ -26,9 +26,9 @@ interface ProjectionInterface public function setUp(): void; /** - * Determines the status of the projection (not to confuse with {@see getState()}) + * Determines the setup status of the projection */ - public function status(): ProjectionStatus; + public function setUpStatus(): ProjectionSetupStatus; public function apply(EventInterface $event, EventEnvelope $eventEnvelope): void; diff --git a/Neos.ContentRepository.Core/Classes/Projection/ProjectionSetupStatus.php b/Neos.ContentRepository.Core/Classes/Projection/ProjectionSetupStatus.php new file mode 100644 index 00000000000..fd9d4061713 --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/Projection/ProjectionSetupStatus.php @@ -0,0 +1,38 @@ +type, $details); - } -} diff --git a/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php b/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php index 1db74979dae..8ea8f51c241 100644 --- a/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php +++ b/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php @@ -8,7 +8,7 @@ use Neos\ContentRepository\Core\Subscription\Engine\Errors; use Neos\ContentRepository\Core\Subscription\Engine\SubscriptionEngine; use Neos\ContentRepository\Core\Subscription\Engine\SubscriptionEngineCriteria; -use Neos\ContentRepository\Core\Subscription\SubscriptionAndProjectionStatuses; +use Neos\ContentRepository\Core\Subscription\SubscriptionStatuses; use Neos\ContentRepository\Core\Subscription\SubscriptionId; use Neos\Error\Messages\Error; use Neos\EventStore\EventStoreInterface; @@ -55,7 +55,7 @@ public function eventStoreStatus(): EventStoreStatus return $this->eventStore->status(); } - public function subscriptionStatuses(): SubscriptionAndProjectionStatuses + public function subscriptionStatuses(): SubscriptionStatuses { return $this->subscriptionEngine->subscriptionStatuses(); } diff --git a/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionEngine.php b/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionEngine.php index 5d8bb9ac086..dadca3f87ad 100644 --- a/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionEngine.php +++ b/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionEngine.php @@ -11,8 +11,8 @@ use Neos\ContentRepository\Core\Service\ContentRepositoryMaintainer; use Neos\ContentRepository\Core\Subscription\Exception\CatchUpFailed; use Neos\ContentRepository\Core\Subscription\Exception\SubscriptionEngineAlreadyProcessingException; -use Neos\ContentRepository\Core\Subscription\SubscriptionAndProjectionStatus; -use Neos\ContentRepository\Core\Subscription\SubscriptionAndProjectionStatuses; +use Neos\ContentRepository\Core\Subscription\ProjectionSubscriptionStatus; +use Neos\ContentRepository\Core\Subscription\SubscriptionStatuses; use Neos\ContentRepository\Core\Subscription\SubscriptionStatusFilter; use Neos\EventStore\EventStoreInterface; use Neos\EventStore\Model\Event\SequenceNumber; @@ -105,26 +105,26 @@ public function reset(SubscriptionEngineCriteria|null $criteria = null): Result return $errors === [] ? Result::success() : Result::failed(Errors::fromArray($errors)); } - public function subscriptionStatuses(SubscriptionCriteria|null $criteria = null): SubscriptionAndProjectionStatuses + public function subscriptionStatuses(SubscriptionCriteria|null $criteria = null): SubscriptionStatuses { $statuses = []; try { $subscriptions = $this->subscriptionStore->findByCriteria($criteria ?? SubscriptionCriteria::noConstraints()); } catch (TableNotFoundException) { // the schema is not setup - thus there are no subscribers - return SubscriptionAndProjectionStatuses::createEmpty(); + return SubscriptionStatuses::createEmpty(); } foreach ($subscriptions as $subscription) { $subscriber = $this->subscribers->contain($subscription->id) ? $this->subscribers->get($subscription->id) : null; - $statuses[] = SubscriptionAndProjectionStatus::create( + $statuses[] = ProjectionSubscriptionStatus::create( subscriptionId: $subscription->id, subscriptionStatus: $subscription->status, subscriptionPosition: $subscription->position, subscriptionError: $subscription->error, - projectionStatus: $subscriber?->projection->status(), + setupStatus: $subscriber?->projection->setUpStatus(), ); } - return SubscriptionAndProjectionStatuses::fromArray($statuses); + return SubscriptionStatuses::fromArray($statuses); } private function handleEvent(EventEnvelope $eventEnvelope, EventInterface $domainEvent, Subscription $subscription): Error|null diff --git a/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionAndProjectionStatus.php b/Neos.ContentRepository.Core/Classes/Subscription/ProjectionSubscriptionStatus.php similarity index 70% rename from Neos.ContentRepository.Core/Classes/Subscription/SubscriptionAndProjectionStatus.php rename to Neos.ContentRepository.Core/Classes/Subscription/ProjectionSubscriptionStatus.php index 56fca0ac88a..eb9a2218c1d 100644 --- a/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionAndProjectionStatus.php +++ b/Neos.ContentRepository.Core/Classes/Subscription/ProjectionSubscriptionStatus.php @@ -4,30 +4,33 @@ namespace Neos\ContentRepository\Core\Subscription; -use Neos\ContentRepository\Core\Projection\ProjectionStatus; +use Neos\ContentRepository\Core\Projection\ProjectionSetupStatus; use Neos\EventStore\Model\Event\SequenceNumber; /** * @api */ -final readonly class SubscriptionAndProjectionStatus +final readonly class ProjectionSubscriptionStatus { private function __construct( public SubscriptionId $subscriptionId, public SubscriptionStatus $subscriptionStatus, public SequenceNumber $subscriptionPosition, public SubscriptionError|null $subscriptionError, - public ProjectionStatus|null $projectionStatus, + public ProjectionSetupStatus|null $setupStatus, ) { } + /** + * @internal + */ public static function create( SubscriptionId $subscriptionId, SubscriptionStatus $subscriptionStatus, SequenceNumber $subscriptionPosition, SubscriptionError|null $subscriptionError, - ProjectionStatus|null $projectionStatus + ProjectionSetupStatus|null $setupStatus ): self { - return new self($subscriptionId, $subscriptionStatus, $subscriptionPosition, $subscriptionError, $projectionStatus); + return new self($subscriptionId, $subscriptionStatus, $subscriptionPosition, $subscriptionError, $setupStatus); } } diff --git a/Neos.ContentRepository.Core/Classes/Subscription/Subscriber/Subscribers.php b/Neos.ContentRepository.Core/Classes/Subscription/Subscriber/Subscribers.php index ba40fbddb1a..17288fc927a 100644 --- a/Neos.ContentRepository.Core/Classes/Subscription/Subscriber/Subscribers.php +++ b/Neos.ContentRepository.Core/Classes/Subscription/Subscriber/Subscribers.php @@ -7,6 +7,12 @@ use Neos\ContentRepository\Core\Subscription\SubscriptionId; /** + * A collection of the registered subscribers. + * + * Currently only projections are the available subscribers, but when the concept is extended, + * other *Subscriber value objects will also be hold in this set. + * Like a possible "ListeningSubscriber" to only listen to events without the capabilities of a full-blown projection. + * * @implements \IteratorAggregate * @internal */ diff --git a/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionAndProjectionStatuses.php b/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionStatuses.php similarity index 56% rename from Neos.ContentRepository.Core/Classes/Subscription/SubscriptionAndProjectionStatuses.php rename to Neos.ContentRepository.Core/Classes/Subscription/SubscriptionStatuses.php index 1c3351ed666..b372bc4062b 100644 --- a/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionAndProjectionStatuses.php +++ b/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionStatuses.php @@ -4,19 +4,27 @@ namespace Neos\ContentRepository\Core\Subscription; +use Neos\ContentRepository\Core\Projection\ProjectionSetupStatusType; + /** + * A collection of the states of the subscribers. + * + * Currently only projections are the available subscribers, but when the concept is extended, + * other *SubscriptionStatus value objects will also be hold in this set. + * Like "ListeningSubscriptionStatus" if a "ListeningSubscriber" is introduced. + * * @api - * @implements \IteratorAggregate + * @implements \IteratorAggregate */ -final readonly class SubscriptionAndProjectionStatuses implements \IteratorAggregate +final readonly class SubscriptionStatuses implements \IteratorAggregate { /** - * @var array $statuses + * @var array $statuses */ private array $statuses; private function __construct( - SubscriptionAndProjectionStatus ...$statuses, + ProjectionSubscriptionStatus ...$statuses, ) { $this->statuses = $statuses; } @@ -27,14 +35,14 @@ public static function createEmpty(): self } /** - * @param array $statuses + * @param array $statuses */ public static function fromArray(array $statuses): self { return new self(...$statuses); } - public function first(): ?SubscriptionAndProjectionStatus + public function first(): ?ProjectionSubscriptionStatus { foreach ($this->statuses as $status) { return $status; @@ -58,7 +66,7 @@ public function isOk(): bool if ($status->subscriptionStatus === SubscriptionStatus::ERROR) { return false; } - if ($status->projectionStatus?->type !== ProjectionStatusType::OK) { + if ($status->setupStatus?->type !== ProjectionSetupStatusType::OK) { return false; } } diff --git a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php index d7a03eed6bb..6f1d1b0f04c 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php +++ b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php @@ -3,7 +3,7 @@ namespace Neos\ContentRepositoryRegistry\Command; -use Neos\ContentRepository\Core\Projection\ProjectionStatusType; +use Neos\ContentRepository\Core\Projection\ProjectionSetupStatusType; use Neos\ContentRepository\Core\Service\ContentRepositoryMaintainerFactory; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\Subscription\SubscriptionId; @@ -73,21 +73,20 @@ public function statusCommand(string $contentRepository = 'default', bool $verbo $this->outputLine(); $subscriptionStatuses = $contentRepositoryMaintainer->subscriptionStatuses(); - foreach ($subscriptionStatuses as $subscriptionStatus) { + foreach ($subscriptionStatuses as $projectionSubscriptionStatus) { // todo reimplement 40e8d35e09ee690406c6a9cfc823c775d4ee3b51 - $this->output('Projection "%s": ', [$subscriptionStatus->subscriptionId->value]); - $projectionStatus = $subscriptionStatus->projectionStatus; + $this->output('Projection "%s": ', [$projectionSubscriptionStatus->subscriptionId->value]); + $projectionStatus = $projectionSubscriptionStatus->setupStatus; if ($projectionStatus === null) { $this->outputLine('No status available.'); // todo this means detached? continue; } $this->outputLine(match ($projectionStatus->type) { - ProjectionStatusType::OK => 'OK', - ProjectionStatusType::SETUP_REQUIRED => 'Setup required!', - ProjectionStatusType::REPLAY_REQUIRED => 'Replay required!', - ProjectionStatusType::ERROR => 'ERROR', + ProjectionSetupStatusType::OK => 'OK', + ProjectionSetupStatusType::SETUP_REQUIRED => 'Setup required!', + ProjectionSetupStatusType::ERROR => 'ERROR', }); - if ($verbose && ($projectionStatus->type !== ProjectionStatusType::OK || $projectionStatus->details)) { + if ($verbose && ($projectionStatus->type !== ProjectionSetupStatusType::OK || $projectionStatus->details)) { $lines = explode(chr(10), $projectionStatus->details ?: 'No details available.'); foreach ($lines as $line) { $this->outputLine(' ' . $line); diff --git a/Neos.Neos/Classes/FrontendRouting/Projection/DocumentUriPathProjection.php b/Neos.Neos/Classes/FrontendRouting/Projection/DocumentUriPathProjection.php index 016814020bc..194dca2bab1 100644 --- a/Neos.Neos/Classes/FrontendRouting/Projection/DocumentUriPathProjection.php +++ b/Neos.Neos/Classes/FrontendRouting/Projection/DocumentUriPathProjection.php @@ -28,7 +28,7 @@ use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\NodeType\NodeTypeName; use Neos\ContentRepository\Core\Projection\ProjectionInterface; -use Neos\ContentRepository\Core\Projection\ProjectionStatus; +use Neos\ContentRepository\Core\Projection\ProjectionSetupStatus; use Neos\ContentRepository\Core\Projection\WithMarkStaleInterface; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\EventStore\Model\EventEnvelope; @@ -65,22 +65,22 @@ public function setUp(): void } } - public function status(): ProjectionStatus + public function setUpStatus(): ProjectionSetupStatus { try { $this->dbal->connect(); } catch (\Throwable $e) { - return ProjectionStatus::error(sprintf('Failed to connect to database: %s', $e->getMessage())); + return ProjectionSetupStatus::error(sprintf('Failed to connect to database: %s', $e->getMessage())); } try { $requiredSqlStatements = $this->determineRequiredSqlStatements(); } catch (\Throwable $e) { - return ProjectionStatus::error(sprintf('Failed to determine required SQL statements: %s', $e->getMessage())); + return ProjectionSetupStatus::error(sprintf('Failed to determine required SQL statements: %s', $e->getMessage())); } if ($requiredSqlStatements !== []) { - return ProjectionStatus::setupRequired(sprintf('The following SQL statement%s required: %s', count($requiredSqlStatements) !== 1 ? 's are' : ' is', implode(chr(10), $requiredSqlStatements))); + return ProjectionSetupStatus::setupRequired(sprintf('The following SQL statement%s required: %s', count($requiredSqlStatements) !== 1 ? 's are' : ' is', implode(chr(10), $requiredSqlStatements))); } - return ProjectionStatus::ok(); + return ProjectionSetupStatus::ok(); } /** diff --git a/Neos.Neos/Classes/PendingChangesProjection/ChangeProjection.php b/Neos.Neos/Classes/PendingChangesProjection/ChangeProjection.php index ead06f7b588..269ebf29598 100644 --- a/Neos.Neos/Classes/PendingChangesProjection/ChangeProjection.php +++ b/Neos.Neos/Classes/PendingChangesProjection/ChangeProjection.php @@ -40,7 +40,7 @@ use Neos\ContentRepository\Core\Infrastructure\DbalSchemaDiff; use Neos\ContentRepository\Core\Infrastructure\DbalSchemaFactory; use Neos\ContentRepository\Core\Projection\ProjectionInterface; -use Neos\ContentRepository\Core\Projection\ProjectionStatus; +use Neos\ContentRepository\Core\Projection\ProjectionSetupStatus; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\EventStore\Model\EventEnvelope; @@ -75,22 +75,22 @@ public function setUp(): void } } - public function status(): ProjectionStatus + public function setUpStatus(): ProjectionSetupStatus { try { $this->dbal->connect(); } catch (\Throwable $e) { - return ProjectionStatus::error(sprintf('Failed to connect to database: %s', $e->getMessage())); + return ProjectionSetupStatus::error(sprintf('Failed to connect to database: %s', $e->getMessage())); } try { $requiredSqlStatements = $this->determineRequiredSqlStatements(); } catch (\Throwable $e) { - return ProjectionStatus::error(sprintf('Failed to determine required SQL statements: %s', $e->getMessage())); + return ProjectionSetupStatus::error(sprintf('Failed to determine required SQL statements: %s', $e->getMessage())); } if ($requiredSqlStatements !== []) { - return ProjectionStatus::setupRequired(sprintf('The following SQL statement%s required: %s', count($requiredSqlStatements) !== 1 ? 's are' : ' is', implode(chr(10), $requiredSqlStatements))); + return ProjectionSetupStatus::setupRequired(sprintf('The following SQL statement%s required: %s', count($requiredSqlStatements) !== 1 ? 's are' : ' is', implode(chr(10), $requiredSqlStatements))); } - return ProjectionStatus::ok(); + return ProjectionSetupStatus::ok(); } /** From 8ff0f61fae1f52cb34dd6fb49a407a3995dd62d4 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 24 Nov 2024 19:54:48 +0100 Subject: [PATCH 03/24] TASK: Introduce `DetachedSubscriptionStatus` as the projection setup status cannot be calculated ... and when extending the system to support multiple subscribers, we cannot know their original classification but have to use a special empty placeholder like: `DetachedSubscriptionStatus` This also makes the `$projectionStatus === null` detached case more explicit when using status. --- .../AbstractSubscriptionEngineTestCase.php | 3 +- .../SubscriptionDetachedStatusTest.php | 31 ++++++++------- .../DetachedSubscriptionStatus.php | 34 ++++++++++++++++ .../Engine/SubscriptionEngine.php | 13 ++++++- .../ProjectionSubscriptionStatus.php | 4 +- .../Subscription/SubscriptionStatuses.php | 26 ++++++++----- .../Classes/Command/CrCommandController.php | 39 +++++++++++-------- 7 files changed, 104 insertions(+), 46 deletions(-) create mode 100644 Neos.ContentRepository.Core/Classes/Subscription/DetachedSubscriptionStatus.php diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/AbstractSubscriptionEngineTestCase.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/AbstractSubscriptionEngineTestCase.php index bc48af8b01c..2ca859b0182 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/AbstractSubscriptionEngineTestCase.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/AbstractSubscriptionEngineTestCase.php @@ -17,6 +17,7 @@ use Neos\ContentRepository\Core\Projection\ProjectionSetupStatus; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; +use Neos\ContentRepository\Core\Subscription\DetachedSubscriptionStatus; use Neos\ContentRepository\Core\Subscription\Engine\SubscriptionEngine; use Neos\ContentRepository\Core\Subscription\Store\SubscriptionCriteria; use Neos\ContentRepository\Core\Subscription\ProjectionSubscriptionStatus; @@ -134,7 +135,7 @@ final protected function resetDatabase(Connection $connection, ContentRepository $connection->prepare('SET FOREIGN_KEY_CHECKS = 1;')->executeStatement(); } - final protected function subscriptionStatus(string $subscriptionId): ?ProjectionSubscriptionStatus + final protected function subscriptionStatus(string $subscriptionId): ProjectionSubscriptionStatus|DetachedSubscriptionStatus|null { return $this->subscriptionEngine->subscriptionStatuses(SubscriptionCriteria::create(ids: [SubscriptionId::fromString($subscriptionId)]))->first(); } diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionDetachedStatusTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionDetachedStatusTest.php index 68a00a94969..88dbd65e056 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionDetachedStatusTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionDetachedStatusTest.php @@ -5,6 +5,7 @@ namespace Neos\ContentRepository\BehavioralTests\Tests\Functional\Subscription; use Neos\ContentRepository\Core\Projection\ProjectionSetupStatus; +use Neos\ContentRepository\Core\Subscription\DetachedSubscriptionStatus; use Neos\ContentRepository\Core\Subscription\Engine\ProcessedResult; use Neos\ContentRepository\Core\Subscription\ProjectionSubscriptionStatus; use Neos\ContentRepository\Core\Subscription\SubscriptionId; @@ -45,8 +46,15 @@ public function projectionIsDetachedOnCatchupActive() $this->getObject(ContentRepositoryRegistry::class)->resetFactoryInstance($this->contentRepository->id); $this->setupContentRepositoryDependencies($this->contentRepository->id); - // todo status is stale??, should be DETACHED, and also cr:setup should marke detached projections?!! - // $this->expectOkayStatus('Vendor.Package:FakeProjection', SubscriptionStatus::ACTIVE, SequenceNumber::none()); + self::assertEquals( + DetachedSubscriptionStatus::create( + subscriptionId: SubscriptionId::fromString('Vendor.Package:FakeProjection'), + // the state is still active as we do not mutate it during the setup call! + subscriptionStatus: SubscriptionStatus::ACTIVE, + subscriptionPosition: SequenceNumber::none() + ), + $this->subscriptionStatus('Vendor.Package:FakeProjection') + ); $this->fakeProjection->expects(self::never())->method('apply'); // catchup to mark detached subscribers @@ -54,15 +62,12 @@ public function projectionIsDetachedOnCatchupActive() // todo result should reflect that there was an detachment? Throw error in CR? self::assertEquals(ProcessedResult::success(1), $result); - $expectedDetachedState = ProjectionSubscriptionStatus::create( - subscriptionId: SubscriptionId::fromString('Vendor.Package:FakeProjection'), - subscriptionStatus: SubscriptionStatus::DETACHED, - subscriptionPosition: SequenceNumber::none(), - subscriptionError: null, - setupStatus: null // not calculate-able at this point! - ); self::assertEquals( - $expectedDetachedState, + $expectedDetachedState = DetachedSubscriptionStatus::create( + subscriptionId: SubscriptionId::fromString('Vendor.Package:FakeProjection'), + subscriptionStatus: SubscriptionStatus::DETACHED, + subscriptionPosition: SequenceNumber::none() + ), $this->subscriptionStatus('Vendor.Package:FakeProjection') ); @@ -120,12 +125,10 @@ public function projectionIsDetachedOnSetupAndReattachedIfPossible() // todo result should reflect that there was an detachment? self::assertNull($result->errors); - $expectedDetachedState = ProjectionSubscriptionStatus::create( + $expectedDetachedState = DetachedSubscriptionStatus::create( subscriptionId: SubscriptionId::fromString('Vendor.Package:FakeProjection'), subscriptionStatus: SubscriptionStatus::DETACHED, - subscriptionPosition: SequenceNumber::fromInteger(1), - subscriptionError: null, - setupStatus: null // not calculate-able at this point! + subscriptionPosition: SequenceNumber::fromInteger(1) ); self::assertEquals( $expectedDetachedState, diff --git a/Neos.ContentRepository.Core/Classes/Subscription/DetachedSubscriptionStatus.php b/Neos.ContentRepository.Core/Classes/Subscription/DetachedSubscriptionStatus.php new file mode 100644 index 00000000000..fd61ddf9f7a --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/Subscription/DetachedSubscriptionStatus.php @@ -0,0 +1,34 @@ +subscribers->contain($subscription->id) ? $this->subscribers->get($subscription->id) : null; + if (!$this->subscribers->contain($subscription->id)) { + $statuses[] = DetachedSubscriptionStatus::create( + $subscription->id, + $subscription->status, + $subscription->position + ); + continue; + } + $subscriber = $this->subscribers->get($subscription->id); $statuses[] = ProjectionSubscriptionStatus::create( subscriptionId: $subscription->id, subscriptionStatus: $subscription->status, subscriptionPosition: $subscription->position, subscriptionError: $subscription->error, - setupStatus: $subscriber?->projection->setUpStatus(), + setupStatus: $subscriber->projection->setUpStatus(), ); } return SubscriptionStatuses::fromArray($statuses); diff --git a/Neos.ContentRepository.Core/Classes/Subscription/ProjectionSubscriptionStatus.php b/Neos.ContentRepository.Core/Classes/Subscription/ProjectionSubscriptionStatus.php index eb9a2218c1d..13c366fe308 100644 --- a/Neos.ContentRepository.Core/Classes/Subscription/ProjectionSubscriptionStatus.php +++ b/Neos.ContentRepository.Core/Classes/Subscription/ProjectionSubscriptionStatus.php @@ -17,7 +17,7 @@ private function __construct( public SubscriptionStatus $subscriptionStatus, public SequenceNumber $subscriptionPosition, public SubscriptionError|null $subscriptionError, - public ProjectionSetupStatus|null $setupStatus, + public ProjectionSetupStatus $setupStatus, ) { } @@ -29,7 +29,7 @@ public static function create( SubscriptionStatus $subscriptionStatus, SequenceNumber $subscriptionPosition, SubscriptionError|null $subscriptionError, - ProjectionSetupStatus|null $setupStatus + ProjectionSetupStatus $setupStatus ): self { return new self($subscriptionId, $subscriptionStatus, $subscriptionPosition, $subscriptionError, $setupStatus); } diff --git a/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionStatuses.php b/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionStatuses.php index b372bc4062b..d8dcddabc14 100644 --- a/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionStatuses.php +++ b/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionStatuses.php @@ -13,18 +13,21 @@ * other *SubscriptionStatus value objects will also be hold in this set. * Like "ListeningSubscriptionStatus" if a "ListeningSubscriber" is introduced. * + * In case the subscriber is not available currently - e.g. will be detached, a {@see DetachedSubscriptionStatus} will be returned. + * Note that ProjectionSubscriptionStatus with status == Detached can be returned, if the projection is installed again! + * * @api - * @implements \IteratorAggregate + * @implements \IteratorAggregate */ final readonly class SubscriptionStatuses implements \IteratorAggregate { /** - * @var array $statuses + * @var array $statuses */ private array $statuses; private function __construct( - ProjectionSubscriptionStatus ...$statuses, + ProjectionSubscriptionStatus|DetachedSubscriptionStatus ...$statuses, ) { $this->statuses = $statuses; } @@ -35,14 +38,14 @@ public static function createEmpty(): self } /** - * @param array $statuses + * @param array $statuses */ public static function fromArray(array $statuses): self { return new self(...$statuses); } - public function first(): ?ProjectionSubscriptionStatus + public function first(): ProjectionSubscriptionStatus|DetachedSubscriptionStatus|null { foreach ($this->statuses as $status) { return $status; @@ -63,11 +66,14 @@ public function isEmpty(): bool public function isOk(): bool { foreach ($this->statuses as $status) { - if ($status->subscriptionStatus === SubscriptionStatus::ERROR) { - return false; - } - if ($status->setupStatus?->type !== ProjectionSetupStatusType::OK) { - return false; + // ignores DetachedSubscriptionStatus + if ($status instanceof ProjectionSubscriptionStatus) { + if ($status->subscriptionStatus === SubscriptionStatus::ERROR) { + return false; + } + if ($status->setupStatus->type !== ProjectionSetupStatusType::OK) { + return false; + } } } return true; diff --git a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php index 6f1d1b0f04c..fdae52a57ad 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php +++ b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php @@ -6,7 +6,10 @@ use Neos\ContentRepository\Core\Projection\ProjectionSetupStatusType; use Neos\ContentRepository\Core\Service\ContentRepositoryMaintainerFactory; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; +use Neos\ContentRepository\Core\Subscription\DetachedSubscriptionStatus; +use Neos\ContentRepository\Core\Subscription\ProjectionSubscriptionStatus; use Neos\ContentRepository\Core\Subscription\SubscriptionId; +use Neos\ContentRepository\Core\Subscription\SubscriptionStatus; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\EventStore\Model\EventStore\StatusType; use Neos\Flow\Annotations as Flow; @@ -73,25 +76,27 @@ public function statusCommand(string $contentRepository = 'default', bool $verbo $this->outputLine(); $subscriptionStatuses = $contentRepositoryMaintainer->subscriptionStatuses(); - foreach ($subscriptionStatuses as $projectionSubscriptionStatus) { - // todo reimplement 40e8d35e09ee690406c6a9cfc823c775d4ee3b51 - $this->output('Projection "%s": ', [$projectionSubscriptionStatus->subscriptionId->value]); - $projectionStatus = $projectionSubscriptionStatus->setupStatus; - if ($projectionStatus === null) { - $this->outputLine('No status available.'); // todo this means detached? - continue; + foreach ($subscriptionStatuses as $subscriptionStatus) { + if ($subscriptionStatus instanceof DetachedSubscriptionStatus) { + $this->output('Subscriber "%s" %s detached.', [$subscriptionStatus->subscriptionId->value, $subscriptionStatus->subscriptionStatus === SubscriptionStatus::DETACHED ? 'is' : 'will be']); } - $this->outputLine(match ($projectionStatus->type) { - ProjectionSetupStatusType::OK => 'OK', - ProjectionSetupStatusType::SETUP_REQUIRED => 'Setup required!', - ProjectionSetupStatusType::ERROR => 'ERROR', - }); - if ($verbose && ($projectionStatus->type !== ProjectionSetupStatusType::OK || $projectionStatus->details)) { - $lines = explode(chr(10), $projectionStatus->details ?: 'No details available.'); - foreach ($lines as $line) { - $this->outputLine(' ' . $line); + if ($subscriptionStatus instanceof ProjectionSubscriptionStatus) { + // todo reimplement 40e8d35e09ee690406c6a9cfc823c775d4ee3b51 + $this->output('Projection "%s": ', [$subscriptionStatus->subscriptionId->value]); + $projectionStatus = $subscriptionStatus->setupStatus; + + $this->outputLine(match ($projectionStatus->type) { + ProjectionSetupStatusType::OK => 'OK', + ProjectionSetupStatusType::SETUP_REQUIRED => 'Setup required!', + ProjectionSetupStatusType::ERROR => 'ERROR', + }); + if ($verbose && ($projectionStatus->type !== ProjectionSetupStatusType::OK || $projectionStatus->details)) { + $lines = explode(chr(10), $projectionStatus->details ?: 'No details available.'); + foreach ($lines as $line) { + $this->outputLine(' ' . $line); + } + $this->outputLine(); } - $this->outputLine(); } } if ($eventStoreStatus->type !== StatusType::OK || !$subscriptionStatuses->isOk()) { From 9675572da094d5ff7e1fc5ba974fb8356991cfb6 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 24 Nov 2024 19:58:06 +0100 Subject: [PATCH 04/24] TASK: Inline `pruneAllWorkspacesAndContentStreamsFromEventStream` into CR Maintainer --- .../Service/ContentRepositoryMaintainer.php | 62 +++++++++++++++++-- .../ContentRepositoryMaintainerFactory.php | 7 +-- .../Classes/Service/ContentStreamPruner.php | 55 ---------------- 3 files changed, 59 insertions(+), 65 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php b/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php index 8ea8f51c241..bf83d89ea80 100644 --- a/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php +++ b/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php @@ -5,6 +5,8 @@ namespace Neos\ContentRepository\Core\Service; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; +use Neos\ContentRepository\Core\Feature\ContentStreamEventStreamName; +use Neos\ContentRepository\Core\Feature\WorkspaceEventStreamName; use Neos\ContentRepository\Core\Subscription\Engine\Errors; use Neos\ContentRepository\Core\Subscription\Engine\SubscriptionEngine; use Neos\ContentRepository\Core\Subscription\Engine\SubscriptionEngineCriteria; @@ -12,7 +14,11 @@ use Neos\ContentRepository\Core\Subscription\SubscriptionId; use Neos\Error\Messages\Error; use Neos\EventStore\EventStoreInterface; +use Neos\EventStore\Model\Event\EventType; +use Neos\EventStore\Model\Event\EventTypes; +use Neos\EventStore\Model\Event\StreamName; use Neos\EventStore\Model\EventStore\Status as EventStoreStatus; +use Neos\EventStore\Model\EventStream\EventStreamFilter; use Neos\EventStore\Model\EventStream\VirtualStreamName; /** @@ -27,8 +33,7 @@ */ public function __construct( private EventStoreInterface $eventStore, - private SubscriptionEngine $subscriptionEngine, - private ContentStreamPruner $contentStreamPruner + private SubscriptionEngine $subscriptionEngine ) { } @@ -119,8 +124,13 @@ public function catchupProjection(SubscriptionId $subscriptionId, \Closure|null */ public function prune(): Error|null { - // todo move pruneAllWorkspacesAndContentStreamsFromEventStream here. - $this->contentStreamPruner->pruneAllWorkspacesAndContentStreamsFromEventStream(); + // prune all streams: + foreach ($this->findAllContentStreamStreamNames() as $contentStreamStreamName) { + $this->eventStore->deleteStream($contentStreamStreamName); + } + foreach ($this->findAllWorkspaceStreamNames() as $workspaceStreamName) { + $this->eventStore->deleteStream($workspaceStreamName); + } $resetResult = $this->subscriptionEngine->reset(); if ($resetResult->errors !== null) { return self::createErrorForReason('reset', $resetResult->errors); @@ -143,4 +153,48 @@ private static function createErrorForReason(string $method, Errors $errors): Er } return new Error(join("\n", $message)); } + + /** + * @return list + */ + private function findAllContentStreamStreamNames(): array + { + $events = $this->eventStore->load( + VirtualStreamName::forCategory(ContentStreamEventStreamName::EVENT_STREAM_NAME_PREFIX), + EventStreamFilter::create( + EventTypes::create( + // we are only interested in the creation events to limit the amount of events to fetch + EventType::fromString('ContentStreamWasCreated'), + EventType::fromString('ContentStreamWasForked') + ) + ) + ); + $allStreamNames = []; + foreach ($events as $eventEnvelope) { + $allStreamNames[] = $eventEnvelope->streamName; + } + return array_unique($allStreamNames, SORT_REGULAR); + } + + /** + * @return list + */ + private function findAllWorkspaceStreamNames(): array + { + $events = $this->eventStore->load( + VirtualStreamName::forCategory(WorkspaceEventStreamName::EVENT_STREAM_NAME_PREFIX), + EventStreamFilter::create( + EventTypes::create( + // we are only interested in the creation events to limit the amount of events to fetch + EventType::fromString('RootWorkspaceWasCreated'), + EventType::fromString('WorkspaceWasCreated') + ) + ) + ); + $allStreamNames = []; + foreach ($events as $eventEnvelope) { + $allStreamNames[] = $eventEnvelope->streamName; + } + return array_unique($allStreamNames, SORT_REGULAR); + } } diff --git a/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainerFactory.php b/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainerFactory.php index e4ddcf4e76d..234b01fcb1d 100644 --- a/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainerFactory.php +++ b/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainerFactory.php @@ -18,12 +18,7 @@ public function build( ): ContentRepositoryMaintainer { return new ContentRepositoryMaintainer( $serviceFactoryDependencies->eventStore, - $serviceFactoryDependencies->subscriptionEngine, - new ContentStreamPruner( - $serviceFactoryDependencies->eventStore, - $serviceFactoryDependencies->eventNormalizer, - $serviceFactoryDependencies->subscriptionEngine, - ) + $serviceFactoryDependencies->subscriptionEngine ); } } diff --git a/Neos.ContentRepository.Core/Classes/Service/ContentStreamPruner.php b/Neos.ContentRepository.Core/Classes/Service/ContentStreamPruner.php index a71f8a4d7d2..5f1991c0a77 100644 --- a/Neos.ContentRepository.Core/Classes/Service/ContentStreamPruner.php +++ b/Neos.ContentRepository.Core/Classes/Service/ContentStreamPruner.php @@ -4,7 +4,6 @@ namespace Neos\ContentRepository\Core\Service; -use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\EventStore\EventNormalizer; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; use Neos\ContentRepository\Core\Feature\ContentStreamCreation\Event\ContentStreamWasCreated; @@ -27,7 +26,6 @@ use Neos\EventStore\EventStoreInterface; use Neos\EventStore\Model\Event\EventType; use Neos\EventStore\Model\Event\EventTypes; -use Neos\EventStore\Model\Event\StreamName; use Neos\EventStore\Model\EventStream\EventStreamFilter; use Neos\EventStore\Model\EventStream\ExpectedVersion; use Neos\EventStore\Model\EventStream\VirtualStreamName; @@ -200,15 +198,6 @@ public function pruneRemovedFromEventStream(\Closure $outputFn): void } } - public function pruneAllWorkspacesAndContentStreamsFromEventStream(): void - { - foreach ($this->findAllContentStreamStreamNames() as $contentStreamStreamName) { - $this->eventStore->deleteStream($contentStreamStreamName); - } - foreach ($this->findAllWorkspaceStreamNames() as $workspaceStreamName) { - $this->eventStore->deleteStream($workspaceStreamName); - } - } /** * Find all removed content streams that are unused in the event stream @@ -411,48 +400,4 @@ private function findAllContentStreams(): array } return $cs; } - - /** - * @return list - */ - private function findAllContentStreamStreamNames(): array - { - $events = $this->eventStore->load( - VirtualStreamName::forCategory(ContentStreamEventStreamName::EVENT_STREAM_NAME_PREFIX), - EventStreamFilter::create( - EventTypes::create( - // we are only interested in the creation events to limit the amount of events to fetch - EventType::fromString('ContentStreamWasCreated'), - EventType::fromString('ContentStreamWasForked') - ) - ) - ); - $allStreamNames = []; - foreach ($events as $eventEnvelope) { - $allStreamNames[] = $eventEnvelope->streamName; - } - return array_unique($allStreamNames, SORT_REGULAR); - } - - /** - * @return list - */ - private function findAllWorkspaceStreamNames(): array - { - $events = $this->eventStore->load( - VirtualStreamName::forCategory(WorkspaceEventStreamName::EVENT_STREAM_NAME_PREFIX), - EventStreamFilter::create( - EventTypes::create( - // we are only interested in the creation events to limit the amount of events to fetch - EventType::fromString('RootWorkspaceWasCreated'), - EventType::fromString('WorkspaceWasCreated') - ) - ) - ); - $allStreamNames = []; - foreach ($events as $eventEnvelope) { - $allStreamNames[] = $eventEnvelope->streamName; - } - return array_unique($allStreamNames, SORT_REGULAR); - } } From 655ac3c43f8d15f074c91e5bfbda6cabc77d06ca Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 24 Nov 2024 20:26:57 +0100 Subject: [PATCH 05/24] TASK: Reimplement 40e8d35e09ee690406c6a9cfc823c775d4ee3b51 Under consideration of the new `ProjectionSubscriptionStatus` --- .../Subscription/SubscriptionStatuses.php | 18 ----- .../Classes/Command/CrCommandController.php | 73 +++++++++++++++---- .../Domain/Service/SiteImportService.php | 13 +++- 3 files changed, 68 insertions(+), 36 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionStatuses.php b/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionStatuses.php index d8dcddabc14..e6860cdd045 100644 --- a/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionStatuses.php +++ b/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionStatuses.php @@ -4,8 +4,6 @@ namespace Neos\ContentRepository\Core\Subscription; -use Neos\ContentRepository\Core\Projection\ProjectionSetupStatusType; - /** * A collection of the states of the subscribers. * @@ -62,20 +60,4 @@ public function isEmpty(): bool { return $this->statuses === []; } - - public function isOk(): bool - { - foreach ($this->statuses as $status) { - // ignores DetachedSubscriptionStatus - if ($status instanceof ProjectionSubscriptionStatus) { - if ($status->subscriptionStatus === SubscriptionStatus::ERROR) { - return false; - } - if ($status->setupStatus->type !== ProjectionSetupStatusType::OK) { - return false; - } - } - } - return true; - } } diff --git a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php index fdae52a57ad..ee609417152 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php +++ b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php @@ -62,44 +62,85 @@ public function statusCommand(string $contentRepository = 'default', bool $verbo } $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); $contentRepositoryMaintainer = $this->contentRepositoryRegistry->buildService($contentRepositoryId, new ContentRepositoryMaintainerFactory()); - $eventStoreStatus = $contentRepositoryMaintainer->eventStoreStatus(); + $hasErrors = false; + $setupRequired = false; + $bootingRequired = false; $this->output('Event Store: '); $this->outputLine(match ($eventStoreStatus->type) { StatusType::OK => 'OK', StatusType::SETUP_REQUIRED => 'Setup required!', StatusType::ERROR => 'ERROR', }); + $hasErrors |= $eventStoreStatus->type === StatusType::ERROR; if ($verbose && $eventStoreStatus->details !== '') { $this->outputFormatted($eventStoreStatus->details, [], 2); } $this->outputLine(); - + $this->outputLine('Subscriptions:'); $subscriptionStatuses = $contentRepositoryMaintainer->subscriptionStatuses(); - foreach ($subscriptionStatuses as $subscriptionStatus) { - if ($subscriptionStatus instanceof DetachedSubscriptionStatus) { - $this->output('Subscriber "%s" %s detached.', [$subscriptionStatus->subscriptionId->value, $subscriptionStatus->subscriptionStatus === SubscriptionStatus::DETACHED ? 'is' : 'will be']); + if ($subscriptionStatuses->isEmpty()) { + $this->outputLine('There are no registered subscriptions yet, please run ./flow cr:setup'); + $this->quit(1); + } + foreach ($subscriptionStatuses as $status) { + if ($status instanceof DetachedSubscriptionStatus) { + $this->outputLine(' %s:', [$status->subscriptionId->value]); + $this->output(' Subscription: '); + $this->output('%s DETACHED', [$status->subscriptionId->value, $status->subscriptionStatus === SubscriptionStatus::DETACHED ? 'is' : 'will be']); + $this->outputLine(' at position %d', [$status->subscriptionPosition->value]); } - if ($subscriptionStatus instanceof ProjectionSubscriptionStatus) { - // todo reimplement 40e8d35e09ee690406c6a9cfc823c775d4ee3b51 - $this->output('Projection "%s": ', [$subscriptionStatus->subscriptionId->value]); - $projectionStatus = $subscriptionStatus->setupStatus; - - $this->outputLine(match ($projectionStatus->type) { + if ($status instanceof ProjectionSubscriptionStatus) { + $this->outputLine(' %s:', [$status->subscriptionId->value]); + $this->output(' Projection: '); + $this->output(match ($status->subscriptionStatus) { + SubscriptionStatus::NEW => 'NEW', + SubscriptionStatus::BOOTING => 'BOOTING', + SubscriptionStatus::ACTIVE => 'ACTIVE', + SubscriptionStatus::DETACHED => 'DETACHED', + SubscriptionStatus::ERROR => 'ERROR', + }); + $this->outputLine(' at position %d', [$status->subscriptionPosition->value]); + $hasErrors |= $status->subscriptionStatus === SubscriptionStatus::ERROR; + $bootingRequired |= $status->subscriptionStatus === SubscriptionStatus::BOOTING; + // detached can be reattached via setup: + $setupRequired |= $status->subscriptionStatus === SubscriptionStatus::DETACHED; + if ($verbose && $status->subscriptionError !== null) { + $lines = explode(chr(10), $status->subscriptionError->errorMessage ?: 'No details available.'); + foreach ($lines as $line) { + $this->outputLine(' %s', [$line]); + } + } + $this->output(' Setup: '); + $this->outputLine(match ($status->setupStatus->type) { ProjectionSetupStatusType::OK => 'OK', - ProjectionSetupStatusType::SETUP_REQUIRED => 'Setup required!', + ProjectionSetupStatusType::SETUP_REQUIRED => 'SETUP REQUIRED', ProjectionSetupStatusType::ERROR => 'ERROR', }); - if ($verbose && ($projectionStatus->type !== ProjectionSetupStatusType::OK || $projectionStatus->details)) { - $lines = explode(chr(10), $projectionStatus->details ?: 'No details available.'); + $hasErrors |= $status->setupStatus->type === ProjectionSetupStatusType::ERROR; + $setupRequired |= $status->setupStatus->type === ProjectionSetupStatusType::SETUP_REQUIRED; + if ($verbose && ($status->setupStatus->type !== ProjectionSetupStatusType::OK || $status->setupStatus->details)) { + $lines = explode(chr(10), $status->setupStatus->details ?: 'No details available.'); foreach ($lines as $line) { - $this->outputLine(' ' . $line); + $this->outputLine(' ' . $line); } $this->outputLine(); } } } - if ($eventStoreStatus->type !== StatusType::OK || !$subscriptionStatuses->isOk()) { + if ($verbose) { + $this->outputLine(); + if ($setupRequired) { + $this->outputLine('Setup required, please run ./flow cr:setup'); + } + if ($bootingRequired) { + $this->outputLine('Catchup needed for projections, please run ./flow cr:projectioncatchup [projection-name]'); + } + if ($hasErrors) { + $this->outputLine('Some projections are not okay'); + } + } + if ($hasErrors) { $this->quit(1); } } diff --git a/Neos.Neos/Classes/Domain/Service/SiteImportService.php b/Neos.Neos/Classes/Domain/Service/SiteImportService.php index e0489cc6440..6d0bc8f7b44 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteImportService.php +++ b/Neos.Neos/Classes/Domain/Service/SiteImportService.php @@ -17,10 +17,12 @@ use Doctrine\DBAL\Exception as DBALException; use League\Flysystem\Filesystem; use League\Flysystem\Local\LocalFilesystemAdapter; +use Neos\ContentRepository\Core\Projection\ProjectionSetupStatusType; use Neos\ContentRepository\Core\Service\ContentRepositoryMaintainer; use Neos\ContentRepository\Core\Service\ContentRepositoryMaintainerFactory; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; +use Neos\ContentRepository\Core\Subscription\ProjectionSubscriptionStatus; use Neos\ContentRepository\Export\Factory\EventStoreImportProcessorFactory; use Neos\ContentRepository\Export\ProcessingContext; use Neos\ContentRepository\Export\ProcessorInterface; @@ -94,10 +96,17 @@ public function importFromPath(ContentRepositoryId $contentRepositoryId, string private function requireContentRepositoryToBeSetup(ContentRepositoryMaintainer $contentRepositoryMaintainer, ContentRepositoryId $contentRepositoryId): void { $eventStoreStatus = $contentRepositoryMaintainer->eventStoreStatus(); - $subscriptionStatuses = $contentRepositoryMaintainer->subscriptionStatuses(); - if ($eventStoreStatus->type !== StatusType::OK || !$subscriptionStatuses->isOk()) { + if ($eventStoreStatus->type !== StatusType::OK) { throw new \RuntimeException(sprintf('Content repository %s is not setup correctly, please run `./flow cr:setup`', $contentRepositoryId->value)); } + $subscriptionStatuses = $contentRepositoryMaintainer->subscriptionStatuses(); + foreach ($subscriptionStatuses as $status) { + if ($status instanceof ProjectionSubscriptionStatus) { + if ($status->setupStatus->type !== ProjectionSetupStatusType::OK) { + throw new \RuntimeException(sprintf('Projection %s in content repository %s is not setup correctly, please run `./flow cr:setup`', $status->subscriptionId->value, $contentRepositoryId->value)); + } + } + } } private function requireDataBaseSchemaToBeSetup(): void From e235e69652037ee2f8273795229bc94efc686df4 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 24 Nov 2024 21:12:26 +0100 Subject: [PATCH 06/24] TASK: Document new `ContentRepositoryMaintainer` --- .../Service/ContentRepositoryMaintainer.php | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php b/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php index bf83d89ea80..e1adc7dc2b3 100644 --- a/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php +++ b/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php @@ -4,6 +4,7 @@ namespace Neos\ContentRepository\Core\Service; +use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; use Neos\ContentRepository\Core\Feature\ContentStreamEventStreamName; use Neos\ContentRepository\Core\Feature\WorkspaceEventStreamName; @@ -22,7 +23,33 @@ use Neos\EventStore\Model\EventStream\VirtualStreamName; /** - * API to set up and manage a content repository + * Set up and manage a content repository + * + * Initialisation / Tear down + * -------------------------- + * The method {@see setUp} sets up the content repository like event store and projection database tables. + * It is non-destructive. + * + * Resetting a content repository with {@see prune} method will purge the event stream and reset all projection states. + * + * Staus information + * ----------------- + * The status of the content repository e.g. if a setup is required or if all subscriptions are active and their position + * can be examined with two methods: + * + * The event store status is available via {@see eventStoreStatus}, while the subscription status are returned + * via {@see subscriptionStatuses}. Further documentation in {@see SubscriptionStatuses}. + * + * Replay / Catchup of projections + * ------------------------------- + * The methods {@see replayProjection}, {@see replayAllProjections} and {@see catchupProjection} + * can be leveraged to interact with the projection catchup. In the happy path no interaction is necessary, + * as {@see ContentRepository::handle()} triggers the projections after applying the events. + * + * For initialising on a new database - which contains events already - a replay will make sure that the projections + * are emptied and reapply the events. + * + * The explicit catchup of a projection is only required when adding new projections after installation, of after fixing a projection error. * * @api */ @@ -102,6 +129,8 @@ public function replayAllProjections(\Closure|null $progressCallback = null): Er */ public function catchupProjection(SubscriptionId $subscriptionId, \Closure|null $progressCallback = null): Error|null { + // todo if a projection is in error state and will be explicit caught up here we might as well attempt that without saying the user should setup? + // also setup then can avoid doing the repairing! $bootResult = $this->subscriptionEngine->boot(SubscriptionEngineCriteria::create([$subscriptionId]), progressCallback: $progressCallback); if ($bootResult->errors !== null) { return self::createErrorForReason('catchup', $bootResult->errors); From 611ca3794fba12c40db4802291d2a14938d77204 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 25 Nov 2024 14:13:50 +0100 Subject: [PATCH 07/24] TASK: Rename `ProjectionSetupStatus` back to `ProjectionStatus` reverts partially bace8ffed072862b24b5763b51b38a6b66020fd5 --- .../DoctrineDbalContentGraphProjection.php | 12 ++++---- .../Projection/HypergraphProjection.php | 12 ++++---- .../TestSuite/DebugEventProjection.php | 8 +++--- .../AbstractSubscriptionEngineTestCase.php | 4 +-- .../Subscription/CatchUpHookErrorTest.php | 6 ++-- .../Subscription/ProjectionErrorTest.php | 16 +++++------ .../SubscriptionActiveStatusTest.php | 8 +++--- .../SubscriptionBootingStatusTest.php | 6 ++-- .../SubscriptionDetachedStatusTest.php | 8 +++--- .../SubscriptionGetStatusTest.php | 10 +++---- .../SubscriptionNewStatusTest.php | 14 +++++----- .../Subscription/SubscriptionResetTest.php | 4 +-- .../Subscription/SubscriptionSetupTest.php | 28 +++++++++---------- .../Projection/ProjectionInterface.php | 4 +-- .../Projection/ProjectionSetupStatusType.php | 15 ---------- ...onSetupStatus.php => ProjectionStatus.php} | 14 ++++++---- .../Projection/ProjectionStatusType.php | 25 +++++++++++++++++ .../Engine/SubscriptionEngine.php | 2 +- .../ProjectionSubscriptionStatus.php | 6 ++-- .../Classes/Command/CrCommandController.php | 14 +++++----- .../Domain/Service/SiteImportService.php | 4 +-- .../Projection/DocumentUriPathProjection.php | 12 ++++---- .../ChangeProjection.php | 12 ++++---- 23 files changed, 129 insertions(+), 115 deletions(-) delete mode 100644 Neos.ContentRepository.Core/Classes/Projection/ProjectionSetupStatusType.php rename Neos.ContentRepository.Core/Classes/Projection/{ProjectionSetupStatus.php => ProjectionStatus.php} (56%) create mode 100644 Neos.ContentRepository.Core/Classes/Projection/ProjectionStatusType.php diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php index a436c61d14f..4e998fde99a 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/DoctrineDbalContentGraphProjection.php @@ -67,7 +67,7 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphReadModelInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeTags; use Neos\ContentRepository\Core\Projection\ContentGraph\Timestamps; -use Neos\ContentRepository\Core\Projection\ProjectionSetupStatus; +use Neos\ContentRepository\Core\Projection\ProjectionStatus; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateClassification; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; @@ -112,23 +112,23 @@ public function setUp(): void } } - public function setUpStatus(): ProjectionSetupStatus + public function status(): ProjectionStatus { try { $this->dbal->connect(); } catch (\Throwable $e) { - return ProjectionSetupStatus::error(sprintf('Failed to connect to database: %s', $e->getMessage())); + return ProjectionStatus::error(sprintf('Failed to connect to database: %s', $e->getMessage())); } try { $requiredSqlStatements = $this->determineRequiredSqlStatements(); } catch (\Throwable $e) { - return ProjectionSetupStatus::error(sprintf('Failed to determine required SQL statements: %s', $e->getMessage())); + return ProjectionStatus::error(sprintf('Failed to determine required SQL statements: %s', $e->getMessage())); } if ($requiredSqlStatements !== []) { - return ProjectionSetupStatus::setupRequired(sprintf('The following SQL statement%s required: %s', count($requiredSqlStatements) !== 1 ? 's are' : ' is', implode(chr(10), $requiredSqlStatements))); + return ProjectionStatus::setupRequired(sprintf('The following SQL statement%s required: %s', count($requiredSqlStatements) !== 1 ? 's are' : ' is', implode(chr(10), $requiredSqlStatements))); } - return ProjectionSetupStatus::ok(); + return ProjectionStatus::ok(); } public function resetState(): void diff --git a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Projection/HypergraphProjection.php b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Projection/HypergraphProjection.php index 12640cfdd72..5961442b9e6 100644 --- a/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Projection/HypergraphProjection.php +++ b/Neos.ContentGraph.PostgreSQLAdapter/src/Domain/Projection/HypergraphProjection.php @@ -42,7 +42,7 @@ use Neos\ContentRepository\Core\Infrastructure\DbalSchemaDiff; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphProjectionInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphReadModelInterface; -use Neos\ContentRepository\Core\Projection\ProjectionSetupStatus; +use Neos\ContentRepository\Core\Projection\ProjectionStatus; use Neos\EventStore\Model\EventEnvelope; /** @@ -89,22 +89,22 @@ public function setUp(): void '); } - public function setUpStatus(): ProjectionSetupStatus + public function status(): ProjectionStatus { try { $this->getDatabaseConnection()->connect(); } catch (\Throwable $e) { - return ProjectionSetupStatus::error(sprintf('Failed to connect to database: %s', $e->getMessage())); + return ProjectionStatus::error(sprintf('Failed to connect to database: %s', $e->getMessage())); } try { $requiredSqlStatements = $this->determineRequiredSqlStatements(); } catch (\Throwable $e) { - return ProjectionSetupStatus::error(sprintf('Failed to determine required SQL statements: %s', $e->getMessage())); + return ProjectionStatus::error(sprintf('Failed to determine required SQL statements: %s', $e->getMessage())); } if ($requiredSqlStatements !== []) { - return ProjectionSetupStatus::setupRequired(sprintf('The following SQL statement%s required: %s', count($requiredSqlStatements) !== 1 ? 's are' : ' is', implode(chr(10), $requiredSqlStatements))); + return ProjectionStatus::setupRequired(sprintf('The following SQL statement%s required: %s', count($requiredSqlStatements) !== 1 ? 's are' : ' is', implode(chr(10), $requiredSqlStatements))); } - return ProjectionSetupStatus::ok(); + return ProjectionStatus::ok(); } /** diff --git a/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/DebugEventProjection.php b/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/DebugEventProjection.php index 4fcd47f66fd..7f71796e96d 100644 --- a/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/DebugEventProjection.php +++ b/Neos.ContentRepository.BehavioralTests/Classes/TestSuite/DebugEventProjection.php @@ -14,7 +14,7 @@ use Neos\ContentRepository\Core\Infrastructure\DbalSchemaFactory; use Neos\ContentRepository\Core\Projection\ProjectionInterface; use Neos\ContentRepository\Core\Projection\ProjectionStateInterface; -use Neos\ContentRepository\Core\Projection\ProjectionSetupStatus; +use Neos\ContentRepository\Core\Projection\ProjectionStatus; use Neos\EventStore\Model\EventEnvelope; use Neos\Flow\Annotations as Flow; @@ -52,13 +52,13 @@ public function setUp(): void } } - public function setUpStatus(): ProjectionSetupStatus + public function status(): ProjectionStatus { $requiredSqlStatements = $this->determineRequiredSqlStatements(); if ($requiredSqlStatements !== []) { - return ProjectionSetupStatus::setupRequired(sprintf('Requires %d SQL statements', count($requiredSqlStatements))); + return ProjectionStatus::setupRequired(sprintf('Requires %d SQL statements', count($requiredSqlStatements))); } - return ProjectionSetupStatus::ok(); + return ProjectionStatus::ok(); } /** diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/AbstractSubscriptionEngineTestCase.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/AbstractSubscriptionEngineTestCase.php index 2ca859b0182..b80bf57dd81 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/AbstractSubscriptionEngineTestCase.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/AbstractSubscriptionEngineTestCase.php @@ -14,7 +14,7 @@ use Neos\ContentRepository\Core\Projection\CatchUpHook\CatchUpHookInterface; use Neos\ContentRepository\Core\Projection\ProjectionInterface; use Neos\ContentRepository\Core\Projection\ProjectionStateInterface; -use Neos\ContentRepository\Core\Projection\ProjectionSetupStatus; +use Neos\ContentRepository\Core\Projection\ProjectionStatus; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\Subscription\DetachedSubscriptionStatus; @@ -162,7 +162,7 @@ final protected function expectOkayStatus($subscriptionId, SubscriptionStatus $s subscriptionStatus: $status, subscriptionPosition: $sequenceNumber, subscriptionError: null, - setupStatus: ProjectionSetupStatus::ok(), + setupStatus: ProjectionStatus::ok(), ), $actual ); diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/CatchUpHookErrorTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/CatchUpHookErrorTest.php index eeb3300229e..3396ec98639 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/CatchUpHookErrorTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/CatchUpHookErrorTest.php @@ -5,7 +5,7 @@ namespace Neos\ContentRepository\BehavioralTests\Tests\Functional\Subscription; use Neos\ContentRepository\Core\Feature\ContentStreamCreation\Event\ContentStreamWasCreated; -use Neos\ContentRepository\Core\Projection\ProjectionSetupStatus; +use Neos\ContentRepository\Core\Projection\ProjectionStatus; use Neos\ContentRepository\Core\Subscription\Exception\CatchUpFailed; use Neos\ContentRepository\Core\Subscription\ProjectionSubscriptionStatus; use Neos\ContentRepository\Core\Subscription\SubscriptionError; @@ -42,7 +42,7 @@ public function error_onBeforeEvent_projectionIsNotRun() subscriptionStatus: SubscriptionStatus::ERROR, subscriptionPosition: SequenceNumber::none(), subscriptionError: SubscriptionError::fromPreviousStatusAndException(SubscriptionStatus::ACTIVE, $exception), - setupStatus: ProjectionSetupStatus::ok(), + setupStatus: ProjectionStatus::ok(), ); self::assertEmpty( @@ -88,7 +88,7 @@ public function error_onAfterEvent_projectionIsRolledBack() subscriptionStatus: SubscriptionStatus::ERROR, subscriptionPosition: SequenceNumber::none(), subscriptionError: SubscriptionError::fromPreviousStatusAndException(SubscriptionStatus::ACTIVE, $exception), - setupStatus: ProjectionSetupStatus::ok(), + setupStatus: ProjectionStatus::ok(), ); self::assertEmpty( diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/ProjectionErrorTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/ProjectionErrorTest.php index 03085882caf..53ded4f33c3 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/ProjectionErrorTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/ProjectionErrorTest.php @@ -6,7 +6,7 @@ use Neos\ContentRepository\Core\Feature\ContentStreamCreation\Event\ContentStreamWasCreated; use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateRootWorkspace; -use Neos\ContentRepository\Core\Projection\ProjectionSetupStatus; +use Neos\ContentRepository\Core\Projection\ProjectionStatus; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\Core\Subscription\Engine\Error; @@ -28,7 +28,7 @@ public function projectionWithError() $this->eventStore->setup(); $this->fakeProjection->expects(self::once())->method('setUp'); $this->subscriptionEngine->setup(); - $this->fakeProjection->expects(self::any())->method('setUpStatus')->willReturn(ProjectionSetupStatus::ok()); + $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); $result = $this->subscriptionEngine->boot(); self::assertEquals(ProcessedResult::success(0), $result); $this->expectOkayStatus('contentGraph', SubscriptionStatus::ACTIVE, SequenceNumber::none()); @@ -46,7 +46,7 @@ public function projectionWithError() subscriptionStatus: SubscriptionStatus::ERROR, subscriptionPosition: SequenceNumber::none(), subscriptionError: SubscriptionError::fromPreviousStatusAndException(SubscriptionStatus::ACTIVE, $exception), - setupStatus: ProjectionSetupStatus::ok(), + setupStatus: ProjectionStatus::ok(), ); $result = $this->subscriptionEngine->catchUpActive(); @@ -80,7 +80,7 @@ public function fixFailedProjection() { $this->eventStore->setup(); $this->fakeProjection->expects(self::once())->method('setUp'); - $this->fakeProjection->expects(self::any())->method('setUpStatus')->willReturn(ProjectionSetupStatus::ok()); + $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); $this->subscriptionEngine->setup(); $this->subscriptionEngine->boot(); @@ -98,7 +98,7 @@ public function fixFailedProjection() subscriptionStatus: SubscriptionStatus::ERROR, subscriptionPosition: SequenceNumber::none(), subscriptionError: SubscriptionError::fromPreviousStatusAndException(SubscriptionStatus::ACTIVE, $exception), - setupStatus: ProjectionSetupStatus::ok(), + setupStatus: ProjectionStatus::ok(), ); $result = $this->subscriptionEngine->catchUpActive(); @@ -158,7 +158,7 @@ public function projectionIsRolledBackAfterError() subscriptionStatus: SubscriptionStatus::ERROR, subscriptionPosition: SequenceNumber::none(), subscriptionError: SubscriptionError::fromPreviousStatusAndException(SubscriptionStatus::ACTIVE, $exception), - setupStatus: ProjectionSetupStatus::ok(), + setupStatus: ProjectionStatus::ok(), ); self::assertEmpty( @@ -235,7 +235,7 @@ public function projectionIsRolledBackAfterErrorButKeepsSuccessFullEvents() subscriptionStatus: SubscriptionStatus::ERROR, subscriptionPosition: SequenceNumber::fromInteger(1), subscriptionError: SubscriptionError::fromPreviousStatusAndException(SubscriptionStatus::ACTIVE, $exception), - setupStatus: ProjectionSetupStatus::ok(), + setupStatus: ProjectionStatus::ok(), ); self::assertEquals( @@ -280,7 +280,7 @@ public function projectionErrorWithMultipleProjectionsInContentRepositoryHandle( { $this->eventStore->setup(); $this->fakeProjection->expects(self::once())->method('setUp'); - $this->fakeProjection->expects(self::any())->method('setUpStatus')->willReturn(ProjectionSetupStatus::ok()); + $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); $this->subscriptionEngine->setup(); $this->subscriptionEngine->boot(); diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionActiveStatusTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionActiveStatusTest.php index a23b4e0080d..421d9a4c212 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionActiveStatusTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionActiveStatusTest.php @@ -5,7 +5,7 @@ namespace Neos\ContentRepository\BehavioralTests\Tests\Functional\Subscription; use Neos\ContentRepository\Core\Feature\ContentStreamCreation\Event\ContentStreamWasCreated; -use Neos\ContentRepository\Core\Projection\ProjectionSetupStatus; +use Neos\ContentRepository\Core\Projection\ProjectionStatus; use Neos\ContentRepository\Core\Subscription\Engine\ProcessedResult; use Neos\ContentRepository\Core\Subscription\Engine\SubscriptionEngineCriteria; use Neos\ContentRepository\Core\Subscription\SubscriptionId; @@ -24,7 +24,7 @@ public function setupProjectionsAndCatchup() $result = $this->subscriptionEngine->boot(); self::assertEquals(ProcessedResult::success(0), $result); - $this->fakeProjection->expects(self::any())->method('setUpStatus')->willReturn(ProjectionSetupStatus::ok()); + $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); $this->expectOkayStatus('contentGraph', SubscriptionStatus::ACTIVE, SequenceNumber::none()); $this->expectOkayStatus('Vendor.Package:FakeProjection', SubscriptionStatus::ACTIVE, SequenceNumber::none()); $this->expectOkayStatus('Vendor.Package:SecondFakeProjection', SubscriptionStatus::ACTIVE, SequenceNumber::none()); @@ -53,7 +53,7 @@ public function setupProjectionsAndCatchup() public function filteringCatchUpActive() { $this->fakeProjection->expects(self::once())->method('setUp'); - $this->fakeProjection->expects(self::any())->method('setUpStatus')->willReturn(ProjectionSetupStatus::ok()); + $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); $this->eventStore->setup(); @@ -88,7 +88,7 @@ public function catchupWithNoEventsKeepsThePreviousPositionOfTheSubscribers() $this->eventStore->setup(); $this->fakeProjection->expects(self::once())->method('setUp'); - $this->fakeProjection->expects(self::any())->method('setUpStatus')->willReturn(ProjectionSetupStatus::ok()); + $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); $this->subscriptionEngine->setup(); $result = $this->subscriptionEngine->boot(); diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionBootingStatusTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionBootingStatusTest.php index 7c8a005dd83..a89370bd57e 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionBootingStatusTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionBootingStatusTest.php @@ -5,7 +5,7 @@ namespace Neos\ContentRepository\BehavioralTests\Tests\Functional\Subscription; use Neos\ContentRepository\Core\Feature\ContentStreamCreation\Event\ContentStreamWasCreated; -use Neos\ContentRepository\Core\Projection\ProjectionSetupStatus; +use Neos\ContentRepository\Core\Projection\ProjectionStatus; use Neos\ContentRepository\Core\Subscription\Engine\ProcessedResult; use Neos\ContentRepository\Core\Subscription\Engine\SubscriptionEngineCriteria; use Neos\ContentRepository\Core\Subscription\SubscriptionId; @@ -24,7 +24,7 @@ public function existingEventStoreEventsAreCaughtUpOnBoot() $this->fakeProjection->expects(self::once())->method('setUp'); $this->subscriptionEngine->setup(); - $this->fakeProjection->expects(self::any())->method('setUpStatus')->willReturn(ProjectionSetupStatus::ok()); + $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); $this->expectOkayStatus('contentGraph', SubscriptionStatus::BOOTING, SequenceNumber::none()); $this->expectOkayStatus('Vendor.Package:FakeProjection', SubscriptionStatus::BOOTING, SequenceNumber::none()); @@ -44,7 +44,7 @@ public function existingEventStoreEventsAreCaughtUpOnBoot() public function filteringCatchUpBoot() { $this->fakeProjection->expects(self::once())->method('setUp'); - $this->fakeProjection->expects(self::any())->method('setUpStatus')->willReturn(ProjectionSetupStatus::ok()); + $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); $this->eventStore->setup(); diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionDetachedStatusTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionDetachedStatusTest.php index 88dbd65e056..dabdcce9cac 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionDetachedStatusTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionDetachedStatusTest.php @@ -4,7 +4,7 @@ namespace Neos\ContentRepository\BehavioralTests\Tests\Functional\Subscription; -use Neos\ContentRepository\Core\Projection\ProjectionSetupStatus; +use Neos\ContentRepository\Core\Projection\ProjectionStatus; use Neos\ContentRepository\Core\Subscription\DetachedSubscriptionStatus; use Neos\ContentRepository\Core\Subscription\Engine\ProcessedResult; use Neos\ContentRepository\Core\Subscription\ProjectionSubscriptionStatus; @@ -27,7 +27,7 @@ public function resetContentRepositoryRegistry(): void public function projectionIsDetachedOnCatchupActive() { $this->fakeProjection->expects(self::once())->method('setUp'); - $this->fakeProjection->expects(self::any())->method('setUpStatus')->willReturn(ProjectionSetupStatus::ok()); + $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); $this->eventStore->setup(); $this->subscriptionEngine->setup(); @@ -99,7 +99,7 @@ public function projectionIsDetachedOnSetupAndReattachedIfPossible() { $this->fakeProjection->expects(self::exactly(2))->method('setUp'); $this->fakeProjection->expects(self::once())->method('apply'); - $this->fakeProjection->expects(self::any())->method('setUpStatus')->willReturn(ProjectionSetupStatus::ok()); + $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); $this->eventStore->setup(); $this->subscriptionEngine->setup(); @@ -155,7 +155,7 @@ public function projectionIsDetachedOnSetupAndReattachedIfPossible() subscriptionStatus: SubscriptionStatus::DETACHED, subscriptionPosition: SequenceNumber::fromInteger(1), subscriptionError: null, - setupStatus: ProjectionSetupStatus::ok() // state _IS_ calculate-able at this point, todo better reflect meaning: is detached, but re-attachable! + setupStatus: ProjectionStatus::ok() // state _IS_ calculate-able at this point, todo better reflect meaning: is detached, but re-attachable! ), $this->subscriptionStatus('Vendor.Package:FakeProjection') ); diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionGetStatusTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionGetStatusTest.php index 5090d3ee72f..4c6d7085568 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionGetStatusTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionGetStatusTest.php @@ -5,7 +5,7 @@ namespace Neos\ContentRepository\BehavioralTests\Tests\Functional\Subscription; use Doctrine\DBAL\Connection; -use Neos\ContentRepository\Core\Projection\ProjectionSetupStatus; +use Neos\ContentRepository\Core\Projection\ProjectionStatus; use Neos\ContentRepository\Core\Subscription\Engine\SubscriptionEngineCriteria; use Neos\ContentRepository\Core\Subscription\ProjectionSubscriptionStatus; use Neos\ContentRepository\Core\Subscription\SubscriptionStatuses; @@ -46,7 +46,7 @@ public function statusOnEmptyDatabase() $this->subscriptionEngine->setup(SubscriptionEngineCriteria::create([SubscriptionId::fromString('contentGraph')])); $this->expectOkayStatus('contentGraph', SubscriptionStatus::BOOTING, SequenceNumber::none()); - $this->fakeProjection->expects(self::once())->method('setUpStatus')->willReturn(ProjectionSetupStatus::setupRequired('fake needs setup.')); + $this->fakeProjection->expects(self::once())->method('status')->willReturn(ProjectionStatus::setupRequired('fake needs setup.')); $actualStatuses = $this->subscriptionEngine->subscriptionStatuses(); @@ -56,21 +56,21 @@ public function statusOnEmptyDatabase() subscriptionStatus: SubscriptionStatus::BOOTING, subscriptionPosition: SequenceNumber::none(), subscriptionError: null, - setupStatus: ProjectionSetupStatus::ok(), + setupStatus: ProjectionStatus::ok(), ), ProjectionSubscriptionStatus::create( subscriptionId: SubscriptionId::fromString('Vendor.Package:FakeProjection'), subscriptionStatus: SubscriptionStatus::NEW, subscriptionPosition: SequenceNumber::none(), subscriptionError: null, - setupStatus: ProjectionSetupStatus::setupRequired('fake needs setup.'), + setupStatus: ProjectionStatus::setupRequired('fake needs setup.'), ), ProjectionSubscriptionStatus::create( subscriptionId: SubscriptionId::fromString('Vendor.Package:SecondFakeProjection'), subscriptionStatus: SubscriptionStatus::NEW, subscriptionPosition: SequenceNumber::none(), subscriptionError: null, - setupStatus: ProjectionSetupStatus::setupRequired('Requires 1 SQL statements'), + setupStatus: ProjectionStatus::setupRequired('Requires 1 SQL statements'), ), ]); diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionNewStatusTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionNewStatusTest.php index 2d7db9869fa..ed2d57fe2b1 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionNewStatusTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionNewStatusTest.php @@ -6,7 +6,7 @@ use Neos\ContentRepository\Core\Projection\ProjectionInterface; use Neos\ContentRepository\Core\Projection\ProjectionStateInterface; -use Neos\ContentRepository\Core\Projection\ProjectionSetupStatus; +use Neos\ContentRepository\Core\Projection\ProjectionStatus; use Neos\ContentRepository\Core\Subscription\Engine\ProcessedResult; use Neos\ContentRepository\Core\Subscription\Engine\SubscriptionEngineCriteria; use Neos\ContentRepository\Core\Subscription\ProjectionSubscriptionStatus; @@ -30,7 +30,7 @@ public function resetContentRepositoryRegistry(): void public function newProjectionIsFoundWhenConfigurationIsAdded() { $this->fakeProjection->expects(self::exactly(2))->method('setUp'); - $this->fakeProjection->expects(self::any())->method('setUpStatus')->willReturn(ProjectionSetupStatus::ok()); + $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); $this->eventStore->setup(); $this->subscriptionEngine->setup(); @@ -43,10 +43,10 @@ public function newProjectionIsFoundWhenConfigurationIsAdded() $newFakeProjection = $this->getMockBuilder(ProjectionInterface::class)->disableAutoReturnValueGeneration()->getMock(); $newFakeProjection->method('getState')->willReturn(new class implements ProjectionStateInterface {}); - $newFakeProjection->expects(self::exactly(3))->method('setUpStatus')->willReturnOnConsecutiveCalls( - ProjectionSetupStatus::setupRequired('Set me up'), - ProjectionSetupStatus::ok(), - ProjectionSetupStatus::ok(), + $newFakeProjection->expects(self::exactly(3))->method('status')->willReturnOnConsecutiveCalls( + ProjectionStatus::setupRequired('Set me up'), + ProjectionStatus::ok(), + ProjectionStatus::ok(), ); FakeProjectionFactory::setProjection( @@ -78,7 +78,7 @@ public function newProjectionIsFoundWhenConfigurationIsAdded() subscriptionStatus: SubscriptionStatus::NEW, subscriptionPosition: SequenceNumber::none(), subscriptionError: null, - setupStatus: ProjectionSetupStatus::setupRequired('Set me up') + setupStatus: ProjectionStatus::setupRequired('Set me up') ), $this->subscriptionStatus('Vendor.Package:NewFakeProjection') ); diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionResetTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionResetTest.php index b5eb56b12d9..cbebb621ceb 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionResetTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionResetTest.php @@ -4,7 +4,7 @@ namespace Neos\ContentRepository\BehavioralTests\Tests\Functional\Subscription; -use Neos\ContentRepository\Core\Projection\ProjectionSetupStatus; +use Neos\ContentRepository\Core\Projection\ProjectionStatus; use Neos\ContentRepository\Core\Subscription\Engine\SubscriptionEngineCriteria; use Neos\ContentRepository\Core\Subscription\SubscriptionId; use Neos\ContentRepository\Core\Subscription\SubscriptionStatus; @@ -16,7 +16,7 @@ final class SubscriptionResetTest extends AbstractSubscriptionEngineTestCase public function filteringReset() { $this->fakeProjection->expects(self::once())->method('setUp'); - $this->fakeProjection->expects(self::any())->method('setUpStatus')->willReturn(ProjectionSetupStatus::ok()); + $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); $this->eventStore->setup(); diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionSetupTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionSetupTest.php index 4bfaf10ad59..5982db88625 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionSetupTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionSetupTest.php @@ -4,7 +4,7 @@ namespace Neos\ContentRepository\BehavioralTests\Tests\Functional\Subscription; -use Neos\ContentRepository\Core\Projection\ProjectionSetupStatus; +use Neos\ContentRepository\Core\Projection\ProjectionStatus; use Neos\ContentRepository\Core\Subscription\Engine\SubscriptionEngineCriteria; use Neos\ContentRepository\Core\Subscription\ProjectionSubscriptionStatus; use Neos\ContentRepository\Core\Subscription\SubscriptionStatuses; @@ -23,7 +23,7 @@ public function setupOnEmptyDatabase() $this->fakeProjection->expects(self::once())->method('setUp'); $this->subscriptionEngine->setup(); - $this->fakeProjection->expects(self::exactly(2))->method('setUpStatus')->willReturn(ProjectionSetupStatus::ok()); + $this->fakeProjection->expects(self::exactly(2))->method('status')->willReturn(ProjectionStatus::ok()); $actualStatuses = $this->subscriptionEngine->subscriptionStatuses(); $expected = SubscriptionStatuses::fromArray([ @@ -32,21 +32,21 @@ public function setupOnEmptyDatabase() subscriptionStatus: SubscriptionStatus::BOOTING, subscriptionPosition: SequenceNumber::none(), subscriptionError: null, - setupStatus: ProjectionSetupStatus::ok(), + setupStatus: ProjectionStatus::ok(), ), ProjectionSubscriptionStatus::create( subscriptionId: SubscriptionId::fromString('Vendor.Package:FakeProjection'), subscriptionStatus: SubscriptionStatus::BOOTING, subscriptionPosition: SequenceNumber::none(), subscriptionError: null, - setupStatus: ProjectionSetupStatus::ok(), + setupStatus: ProjectionStatus::ok(), ), ProjectionSubscriptionStatus::create( subscriptionId: SubscriptionId::fromString('Vendor.Package:SecondFakeProjection'), subscriptionStatus: SubscriptionStatus::BOOTING, subscriptionPosition: SequenceNumber::none(), subscriptionError: null, - setupStatus: ProjectionSetupStatus::ok(), + setupStatus: ProjectionStatus::ok(), ), ]); @@ -65,7 +65,7 @@ public function setupOnEmptyDatabase() public function filteringSetup() { $this->fakeProjection->expects(self::once())->method('setUp'); - $this->fakeProjection->expects(self::once())->method('setUpStatus')->willReturn(ProjectionSetupStatus::ok()); + $this->fakeProjection->expects(self::once())->method('status')->willReturn(ProjectionStatus::ok()); $this->eventStore->setup(); @@ -82,7 +82,7 @@ public function filteringSetup() subscriptionStatus: SubscriptionStatus::NEW, subscriptionPosition: SequenceNumber::none(), subscriptionError: null, - setupStatus: ProjectionSetupStatus::ok() + setupStatus: ProjectionStatus::ok() ), $this->subscriptionStatus('Vendor.Package:SecondFakeProjection') ); @@ -92,7 +92,7 @@ public function filteringSetup() public function setupIsInvokedForBootingSubscribers() { $this->fakeProjection->expects(self::exactly(2))->method('setUp'); - $this->fakeProjection->expects(self::any())->method('setUpStatus')->willReturn(ProjectionSetupStatus::ok()); + $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); // hard reset, so that the tests actually need sql migrations $this->secondFakeProjection->dropTables(); @@ -115,7 +115,7 @@ public function setupIsInvokedForBootingSubscribers() subscriptionStatus: SubscriptionStatus::BOOTING, subscriptionPosition: SequenceNumber::none(), subscriptionError: null, - setupStatus: ProjectionSetupStatus::setupRequired('Requires 1 SQL statements') + setupStatus: ProjectionStatus::setupRequired('Requires 1 SQL statements') ), $this->subscriptionStatus('Vendor.Package:SecondFakeProjection') ); @@ -133,7 +133,7 @@ public function setupIsInvokedForPreviouslyActiveSubscribers() $this->fakeProjection->expects(self::exactly(2))->method('setUp'); $this->fakeProjection->expects(self::once())->method('apply'); - $this->fakeProjection->expects(self::any())->method('setUpStatus')->willReturn(ProjectionSetupStatus::ok()); + $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); // hard reset, so that the tests actually need sql migrations $this->secondFakeProjection->dropTables(); @@ -149,7 +149,7 @@ public function setupIsInvokedForPreviouslyActiveSubscribers() subscriptionStatus: SubscriptionStatus::NEW, subscriptionPosition: SequenceNumber::none(), subscriptionError: null, - setupStatus: ProjectionSetupStatus::setupRequired('Requires 1 SQL statements') + setupStatus: ProjectionStatus::setupRequired('Requires 1 SQL statements') ), $this->subscriptionStatus('Vendor.Package:SecondFakeProjection') ); @@ -181,7 +181,7 @@ public function setupIsInvokedForPreviouslyActiveSubscribers() subscriptionStatus: SubscriptionStatus::ACTIVE, subscriptionPosition: SequenceNumber::fromInteger(1), subscriptionError: null, - setupStatus: ProjectionSetupStatus::setupRequired('Requires 1 SQL statements') + setupStatus: ProjectionStatus::setupRequired('Requires 1 SQL statements') ), $this->subscriptionStatus('Vendor.Package:SecondFakeProjection') ); @@ -198,7 +198,7 @@ public function failingSetupWillMarkProjectionAsErrored() $this->fakeProjection->expects(self::once())->method('setUp')->willThrowException( $exception = new \RuntimeException('Projection could not be setup') ); - $this->fakeProjection->expects(self::once())->method('setUpStatus')->willReturn(ProjectionSetupStatus::setupRequired('Needs setup')); + $this->fakeProjection->expects(self::once())->method('status')->willReturn(ProjectionStatus::setupRequired('Needs setup')); $this->eventStore->setup(); @@ -210,7 +210,7 @@ public function failingSetupWillMarkProjectionAsErrored() subscriptionStatus: SubscriptionStatus::ERROR, subscriptionPosition: SequenceNumber::none(), subscriptionError: SubscriptionError::fromPreviousStatusAndException(SubscriptionStatus::NEW, $exception), - setupStatus: ProjectionSetupStatus::setupRequired('Needs setup'), + setupStatus: ProjectionStatus::setupRequired('Needs setup'), ); self::assertEquals( diff --git a/Neos.ContentRepository.Core/Classes/Projection/ProjectionInterface.php b/Neos.ContentRepository.Core/Classes/Projection/ProjectionInterface.php index 1b5a8519102..aff57389895 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ProjectionInterface.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ProjectionInterface.php @@ -26,9 +26,9 @@ interface ProjectionInterface public function setUp(): void; /** - * Determines the setup status of the projection + * Determines the setup status of the projection. E.g. are the database tables created or any columns missing. */ - public function setUpStatus(): ProjectionSetupStatus; + public function status(): ProjectionStatus; public function apply(EventInterface $event, EventEnvelope $eventEnvelope): void; diff --git a/Neos.ContentRepository.Core/Classes/Projection/ProjectionSetupStatusType.php b/Neos.ContentRepository.Core/Classes/Projection/ProjectionSetupStatusType.php deleted file mode 100644 index 3b6baf62696..00000000000 --- a/Neos.ContentRepository.Core/Classes/Projection/ProjectionSetupStatusType.php +++ /dev/null @@ -1,15 +0,0 @@ -status, subscriptionPosition: $subscription->position, subscriptionError: $subscription->error, - setupStatus: $subscriber->projection->setUpStatus(), + setupStatus: $subscriber->projection->status(), ); } return SubscriptionStatuses::fromArray($statuses); diff --git a/Neos.ContentRepository.Core/Classes/Subscription/ProjectionSubscriptionStatus.php b/Neos.ContentRepository.Core/Classes/Subscription/ProjectionSubscriptionStatus.php index 13c366fe308..0924c717b73 100644 --- a/Neos.ContentRepository.Core/Classes/Subscription/ProjectionSubscriptionStatus.php +++ b/Neos.ContentRepository.Core/Classes/Subscription/ProjectionSubscriptionStatus.php @@ -4,7 +4,7 @@ namespace Neos\ContentRepository\Core\Subscription; -use Neos\ContentRepository\Core\Projection\ProjectionSetupStatus; +use Neos\ContentRepository\Core\Projection\ProjectionStatus; use Neos\EventStore\Model\Event\SequenceNumber; /** @@ -17,7 +17,7 @@ private function __construct( public SubscriptionStatus $subscriptionStatus, public SequenceNumber $subscriptionPosition, public SubscriptionError|null $subscriptionError, - public ProjectionSetupStatus $setupStatus, + public ProjectionStatus $setupStatus, ) { } @@ -29,7 +29,7 @@ public static function create( SubscriptionStatus $subscriptionStatus, SequenceNumber $subscriptionPosition, SubscriptionError|null $subscriptionError, - ProjectionSetupStatus $setupStatus + ProjectionStatus $setupStatus ): self { return new self($subscriptionId, $subscriptionStatus, $subscriptionPosition, $subscriptionError, $setupStatus); } diff --git a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php index ee609417152..0b0c64e7c9b 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php +++ b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php @@ -3,7 +3,7 @@ namespace Neos\ContentRepositoryRegistry\Command; -use Neos\ContentRepository\Core\Projection\ProjectionSetupStatusType; +use Neos\ContentRepository\Core\Projection\ProjectionStatusType; use Neos\ContentRepository\Core\Service\ContentRepositoryMaintainerFactory; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\Subscription\DetachedSubscriptionStatus; @@ -113,13 +113,13 @@ public function statusCommand(string $contentRepository = 'default', bool $verbo } $this->output(' Setup: '); $this->outputLine(match ($status->setupStatus->type) { - ProjectionSetupStatusType::OK => 'OK', - ProjectionSetupStatusType::SETUP_REQUIRED => 'SETUP REQUIRED', - ProjectionSetupStatusType::ERROR => 'ERROR', + ProjectionStatusType::OK => 'OK', + ProjectionStatusType::SETUP_REQUIRED => 'SETUP REQUIRED', + ProjectionStatusType::ERROR => 'ERROR', }); - $hasErrors |= $status->setupStatus->type === ProjectionSetupStatusType::ERROR; - $setupRequired |= $status->setupStatus->type === ProjectionSetupStatusType::SETUP_REQUIRED; - if ($verbose && ($status->setupStatus->type !== ProjectionSetupStatusType::OK || $status->setupStatus->details)) { + $hasErrors |= $status->setupStatus->type === ProjectionStatusType::ERROR; + $setupRequired |= $status->setupStatus->type === ProjectionStatusType::SETUP_REQUIRED; + if ($verbose && ($status->setupStatus->type !== ProjectionStatusType::OK || $status->setupStatus->details)) { $lines = explode(chr(10), $status->setupStatus->details ?: 'No details available.'); foreach ($lines as $line) { $this->outputLine(' ' . $line); diff --git a/Neos.Neos/Classes/Domain/Service/SiteImportService.php b/Neos.Neos/Classes/Domain/Service/SiteImportService.php index 6d0bc8f7b44..6e7bc3bd57e 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteImportService.php +++ b/Neos.Neos/Classes/Domain/Service/SiteImportService.php @@ -17,7 +17,7 @@ use Doctrine\DBAL\Exception as DBALException; use League\Flysystem\Filesystem; use League\Flysystem\Local\LocalFilesystemAdapter; -use Neos\ContentRepository\Core\Projection\ProjectionSetupStatusType; +use Neos\ContentRepository\Core\Projection\ProjectionStatusType; use Neos\ContentRepository\Core\Service\ContentRepositoryMaintainer; use Neos\ContentRepository\Core\Service\ContentRepositoryMaintainerFactory; use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; @@ -102,7 +102,7 @@ private function requireContentRepositoryToBeSetup(ContentRepositoryMaintainer $ $subscriptionStatuses = $contentRepositoryMaintainer->subscriptionStatuses(); foreach ($subscriptionStatuses as $status) { if ($status instanceof ProjectionSubscriptionStatus) { - if ($status->setupStatus->type !== ProjectionSetupStatusType::OK) { + if ($status->setupStatus->type !== ProjectionStatusType::OK) { throw new \RuntimeException(sprintf('Projection %s in content repository %s is not setup correctly, please run `./flow cr:setup`', $status->subscriptionId->value, $contentRepositoryId->value)); } } diff --git a/Neos.Neos/Classes/FrontendRouting/Projection/DocumentUriPathProjection.php b/Neos.Neos/Classes/FrontendRouting/Projection/DocumentUriPathProjection.php index 194dca2bab1..016814020bc 100644 --- a/Neos.Neos/Classes/FrontendRouting/Projection/DocumentUriPathProjection.php +++ b/Neos.Neos/Classes/FrontendRouting/Projection/DocumentUriPathProjection.php @@ -28,7 +28,7 @@ use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\NodeType\NodeTypeName; use Neos\ContentRepository\Core\Projection\ProjectionInterface; -use Neos\ContentRepository\Core\Projection\ProjectionSetupStatus; +use Neos\ContentRepository\Core\Projection\ProjectionStatus; use Neos\ContentRepository\Core\Projection\WithMarkStaleInterface; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\EventStore\Model\EventEnvelope; @@ -65,22 +65,22 @@ public function setUp(): void } } - public function setUpStatus(): ProjectionSetupStatus + public function status(): ProjectionStatus { try { $this->dbal->connect(); } catch (\Throwable $e) { - return ProjectionSetupStatus::error(sprintf('Failed to connect to database: %s', $e->getMessage())); + return ProjectionStatus::error(sprintf('Failed to connect to database: %s', $e->getMessage())); } try { $requiredSqlStatements = $this->determineRequiredSqlStatements(); } catch (\Throwable $e) { - return ProjectionSetupStatus::error(sprintf('Failed to determine required SQL statements: %s', $e->getMessage())); + return ProjectionStatus::error(sprintf('Failed to determine required SQL statements: %s', $e->getMessage())); } if ($requiredSqlStatements !== []) { - return ProjectionSetupStatus::setupRequired(sprintf('The following SQL statement%s required: %s', count($requiredSqlStatements) !== 1 ? 's are' : ' is', implode(chr(10), $requiredSqlStatements))); + return ProjectionStatus::setupRequired(sprintf('The following SQL statement%s required: %s', count($requiredSqlStatements) !== 1 ? 's are' : ' is', implode(chr(10), $requiredSqlStatements))); } - return ProjectionSetupStatus::ok(); + return ProjectionStatus::ok(); } /** diff --git a/Neos.Neos/Classes/PendingChangesProjection/ChangeProjection.php b/Neos.Neos/Classes/PendingChangesProjection/ChangeProjection.php index 269ebf29598..ead06f7b588 100644 --- a/Neos.Neos/Classes/PendingChangesProjection/ChangeProjection.php +++ b/Neos.Neos/Classes/PendingChangesProjection/ChangeProjection.php @@ -40,7 +40,7 @@ use Neos\ContentRepository\Core\Infrastructure\DbalSchemaDiff; use Neos\ContentRepository\Core\Infrastructure\DbalSchemaFactory; use Neos\ContentRepository\Core\Projection\ProjectionInterface; -use Neos\ContentRepository\Core\Projection\ProjectionSetupStatus; +use Neos\ContentRepository\Core\Projection\ProjectionStatus; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\EventStore\Model\EventEnvelope; @@ -75,22 +75,22 @@ public function setUp(): void } } - public function setUpStatus(): ProjectionSetupStatus + public function status(): ProjectionStatus { try { $this->dbal->connect(); } catch (\Throwable $e) { - return ProjectionSetupStatus::error(sprintf('Failed to connect to database: %s', $e->getMessage())); + return ProjectionStatus::error(sprintf('Failed to connect to database: %s', $e->getMessage())); } try { $requiredSqlStatements = $this->determineRequiredSqlStatements(); } catch (\Throwable $e) { - return ProjectionSetupStatus::error(sprintf('Failed to determine required SQL statements: %s', $e->getMessage())); + return ProjectionStatus::error(sprintf('Failed to determine required SQL statements: %s', $e->getMessage())); } if ($requiredSqlStatements !== []) { - return ProjectionSetupStatus::setupRequired(sprintf('The following SQL statement%s required: %s', count($requiredSqlStatements) !== 1 ? 's are' : ' is', implode(chr(10), $requiredSqlStatements))); + return ProjectionStatus::setupRequired(sprintf('The following SQL statement%s required: %s', count($requiredSqlStatements) !== 1 ? 's are' : ' is', implode(chr(10), $requiredSqlStatements))); } - return ProjectionSetupStatus::ok(); + return ProjectionStatus::ok(); } /** From 0b8a3b55755a19cc212a232d9491eda32e1251fd Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 25 Nov 2024 14:17:20 +0100 Subject: [PATCH 08/24] TASK: Rename `SubscriptionStatuses` to `SubscriptionStatusCollection` --- .../AbstractSubscriptionEngineTestCase.php | 2 +- .../SubscriptionGetStatusTest.php | 8 +++---- .../Subscription/SubscriptionSetupTest.php | 6 ++--- .../Service/ContentRepositoryMaintainer.php | 8 +++---- .../Engine/SubscriptionEngine.php | 8 +++---- ...s.php => SubscriptionStatusCollection.php} | 24 +++++++++---------- .../Classes/Command/CrCommandController.php | 6 ++--- .../Domain/Service/SiteImportService.php | 4 ++-- 8 files changed, 33 insertions(+), 33 deletions(-) rename Neos.ContentRepository.Core/Classes/Subscription/{SubscriptionStatuses.php => SubscriptionStatusCollection.php} (74%) diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/AbstractSubscriptionEngineTestCase.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/AbstractSubscriptionEngineTestCase.php index b80bf57dd81..bd9a8ce4de2 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/AbstractSubscriptionEngineTestCase.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/AbstractSubscriptionEngineTestCase.php @@ -137,7 +137,7 @@ final protected function resetDatabase(Connection $connection, ContentRepository final protected function subscriptionStatus(string $subscriptionId): ProjectionSubscriptionStatus|DetachedSubscriptionStatus|null { - return $this->subscriptionEngine->subscriptionStatuses(SubscriptionCriteria::create(ids: [SubscriptionId::fromString($subscriptionId)]))->first(); + return $this->subscriptionEngine->subscriptionStatus(SubscriptionCriteria::create(ids: [SubscriptionId::fromString($subscriptionId)]))->first(); } final protected function commitExampleContentStreamEvent(): void diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionGetStatusTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionGetStatusTest.php index 4c6d7085568..c8be53f9d98 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionGetStatusTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionGetStatusTest.php @@ -8,7 +8,7 @@ use Neos\ContentRepository\Core\Projection\ProjectionStatus; use Neos\ContentRepository\Core\Subscription\Engine\SubscriptionEngineCriteria; use Neos\ContentRepository\Core\Subscription\ProjectionSubscriptionStatus; -use Neos\ContentRepository\Core\Subscription\SubscriptionStatuses; +use Neos\ContentRepository\Core\Subscription\SubscriptionStatusCollection; use Neos\ContentRepository\Core\Subscription\SubscriptionId; use Neos\ContentRepository\Core\Subscription\SubscriptionStatus; use Neos\EventStore\Model\Event\SequenceNumber; @@ -25,7 +25,7 @@ public function statusOnEmptyDatabase() keepSchema: false ); - $actualStatuses = $this->subscriptionEngine->subscriptionStatuses(); + $actualStatuses = $this->subscriptionEngine->subscriptionStatus(); self::assertTrue($actualStatuses->isEmpty()); self::assertNull( @@ -48,9 +48,9 @@ public function statusOnEmptyDatabase() $this->fakeProjection->expects(self::once())->method('status')->willReturn(ProjectionStatus::setupRequired('fake needs setup.')); - $actualStatuses = $this->subscriptionEngine->subscriptionStatuses(); + $actualStatuses = $this->subscriptionEngine->subscriptionStatus(); - $expected = SubscriptionStatuses::fromArray([ + $expected = SubscriptionStatusCollection::fromArray([ ProjectionSubscriptionStatus::create( subscriptionId: SubscriptionId::fromString('contentGraph'), subscriptionStatus: SubscriptionStatus::BOOTING, diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionSetupTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionSetupTest.php index 5982db88625..dd021dc8183 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionSetupTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionSetupTest.php @@ -7,7 +7,7 @@ use Neos\ContentRepository\Core\Projection\ProjectionStatus; use Neos\ContentRepository\Core\Subscription\Engine\SubscriptionEngineCriteria; use Neos\ContentRepository\Core\Subscription\ProjectionSubscriptionStatus; -use Neos\ContentRepository\Core\Subscription\SubscriptionStatuses; +use Neos\ContentRepository\Core\Subscription\SubscriptionStatusCollection; use Neos\ContentRepository\Core\Subscription\SubscriptionError; use Neos\ContentRepository\Core\Subscription\SubscriptionId; use Neos\ContentRepository\Core\Subscription\SubscriptionStatus; @@ -24,9 +24,9 @@ public function setupOnEmptyDatabase() $this->subscriptionEngine->setup(); $this->fakeProjection->expects(self::exactly(2))->method('status')->willReturn(ProjectionStatus::ok()); - $actualStatuses = $this->subscriptionEngine->subscriptionStatuses(); + $actualStatuses = $this->subscriptionEngine->subscriptionStatus(); - $expected = SubscriptionStatuses::fromArray([ + $expected = SubscriptionStatusCollection::fromArray([ ProjectionSubscriptionStatus::create( subscriptionId: SubscriptionId::fromString('contentGraph'), subscriptionStatus: SubscriptionStatus::BOOTING, diff --git a/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php b/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php index e1adc7dc2b3..6929e706be7 100644 --- a/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php +++ b/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php @@ -11,7 +11,7 @@ use Neos\ContentRepository\Core\Subscription\Engine\Errors; use Neos\ContentRepository\Core\Subscription\Engine\SubscriptionEngine; use Neos\ContentRepository\Core\Subscription\Engine\SubscriptionEngineCriteria; -use Neos\ContentRepository\Core\Subscription\SubscriptionStatuses; +use Neos\ContentRepository\Core\Subscription\SubscriptionStatusCollection; use Neos\ContentRepository\Core\Subscription\SubscriptionId; use Neos\Error\Messages\Error; use Neos\EventStore\EventStoreInterface; @@ -38,7 +38,7 @@ * can be examined with two methods: * * The event store status is available via {@see eventStoreStatus}, while the subscription status are returned - * via {@see subscriptionStatuses}. Further documentation in {@see SubscriptionStatuses}. + * via {@see SubscriptionStatusCollection}. Further documentation in {@see SubscriptionStatusCollection}. * * Replay / Catchup of projections * ------------------------------- @@ -87,9 +87,9 @@ public function eventStoreStatus(): EventStoreStatus return $this->eventStore->status(); } - public function subscriptionStatuses(): SubscriptionStatuses + public function subscriptionStatus(): SubscriptionStatusCollection { - return $this->subscriptionEngine->subscriptionStatuses(); + return $this->subscriptionEngine->subscriptionStatus(); } public function replayProjection(SubscriptionId $subscriptionId, \Closure|null $progressCallback = null): Error|null diff --git a/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionEngine.php b/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionEngine.php index 12a15e9d24c..e86537f47d8 100644 --- a/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionEngine.php +++ b/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionEngine.php @@ -13,7 +13,7 @@ use Neos\ContentRepository\Core\Subscription\Exception\CatchUpFailed; use Neos\ContentRepository\Core\Subscription\Exception\SubscriptionEngineAlreadyProcessingException; use Neos\ContentRepository\Core\Subscription\ProjectionSubscriptionStatus; -use Neos\ContentRepository\Core\Subscription\SubscriptionStatuses; +use Neos\ContentRepository\Core\Subscription\SubscriptionStatusCollection; use Neos\ContentRepository\Core\Subscription\SubscriptionStatusFilter; use Neos\EventStore\EventStoreInterface; use Neos\EventStore\Model\Event\SequenceNumber; @@ -106,14 +106,14 @@ public function reset(SubscriptionEngineCriteria|null $criteria = null): Result return $errors === [] ? Result::success() : Result::failed(Errors::fromArray($errors)); } - public function subscriptionStatuses(SubscriptionCriteria|null $criteria = null): SubscriptionStatuses + public function subscriptionStatus(SubscriptionCriteria|null $criteria = null): SubscriptionStatusCollection { $statuses = []; try { $subscriptions = $this->subscriptionStore->findByCriteria($criteria ?? SubscriptionCriteria::noConstraints()); } catch (TableNotFoundException) { // the schema is not setup - thus there are no subscribers - return SubscriptionStatuses::createEmpty(); + return SubscriptionStatusCollection::createEmpty(); } foreach ($subscriptions as $subscription) { if (!$this->subscribers->contain($subscription->id)) { @@ -133,7 +133,7 @@ public function subscriptionStatuses(SubscriptionCriteria|null $criteria = null) setupStatus: $subscriber->projection->status(), ); } - return SubscriptionStatuses::fromArray($statuses); + return SubscriptionStatusCollection::fromArray($statuses); } private function handleEvent(EventEnvelope $eventEnvelope, EventInterface $domainEvent, Subscription $subscription): Error|null diff --git a/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionStatuses.php b/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionStatusCollection.php similarity index 74% rename from Neos.ContentRepository.Core/Classes/Subscription/SubscriptionStatuses.php rename to Neos.ContentRepository.Core/Classes/Subscription/SubscriptionStatusCollection.php index e6860cdd045..4ee36d68ded 100644 --- a/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionStatuses.php +++ b/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionStatusCollection.php @@ -5,7 +5,7 @@ namespace Neos\ContentRepository\Core\Subscription; /** - * A collection of the states of the subscribers. + * A collection of the status of the subscribers. * * Currently only projections are the available subscribers, but when the concept is extended, * other *SubscriptionStatus value objects will also be hold in this set. @@ -17,17 +17,17 @@ * @api * @implements \IteratorAggregate */ -final readonly class SubscriptionStatuses implements \IteratorAggregate +final readonly class SubscriptionStatusCollection implements \IteratorAggregate { /** - * @var array $statuses + * @var array $items */ - private array $statuses; + private array $items; private function __construct( - ProjectionSubscriptionStatus|DetachedSubscriptionStatus ...$statuses, + ProjectionSubscriptionStatus|DetachedSubscriptionStatus ...$items, ) { - $this->statuses = $statuses; + $this->items = $items; } public static function createEmpty(): self @@ -36,16 +36,16 @@ public static function createEmpty(): self } /** - * @param array $statuses + * @param array $items */ - public static function fromArray(array $statuses): self + public static function fromArray(array $items): self { - return new self(...$statuses); + return new self(...$items); } public function first(): ProjectionSubscriptionStatus|DetachedSubscriptionStatus|null { - foreach ($this->statuses as $status) { + foreach ($this->items as $status) { return $status; } return null; @@ -53,11 +53,11 @@ public function first(): ProjectionSubscriptionStatus|DetachedSubscriptionStatus public function getIterator(): \Traversable { - yield from $this->statuses; + yield from $this->items; } public function isEmpty(): bool { - return $this->statuses === []; + return $this->items === []; } } diff --git a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php index 0b0c64e7c9b..47e8a3198c2 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php +++ b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php @@ -78,12 +78,12 @@ public function statusCommand(string $contentRepository = 'default', bool $verbo } $this->outputLine(); $this->outputLine('Subscriptions:'); - $subscriptionStatuses = $contentRepositoryMaintainer->subscriptionStatuses(); - if ($subscriptionStatuses->isEmpty()) { + $subscriptionStatusCollection = $contentRepositoryMaintainer->subscriptionStatus(); + if ($subscriptionStatusCollection->isEmpty()) { $this->outputLine('There are no registered subscriptions yet, please run ./flow cr:setup'); $this->quit(1); } - foreach ($subscriptionStatuses as $status) { + foreach ($subscriptionStatusCollection as $status) { if ($status instanceof DetachedSubscriptionStatus) { $this->outputLine(' %s:', [$status->subscriptionId->value]); $this->output(' Subscription: '); diff --git a/Neos.Neos/Classes/Domain/Service/SiteImportService.php b/Neos.Neos/Classes/Domain/Service/SiteImportService.php index 6e7bc3bd57e..bd6a54d17d0 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteImportService.php +++ b/Neos.Neos/Classes/Domain/Service/SiteImportService.php @@ -99,8 +99,8 @@ private function requireContentRepositoryToBeSetup(ContentRepositoryMaintainer $ if ($eventStoreStatus->type !== StatusType::OK) { throw new \RuntimeException(sprintf('Content repository %s is not setup correctly, please run `./flow cr:setup`', $contentRepositoryId->value)); } - $subscriptionStatuses = $contentRepositoryMaintainer->subscriptionStatuses(); - foreach ($subscriptionStatuses as $status) { + $subscriptionStatusCollection = $contentRepositoryMaintainer->subscriptionStatus(); + foreach ($subscriptionStatusCollection as $status) { if ($status instanceof ProjectionSubscriptionStatus) { if ($status->setupStatus->type !== ProjectionStatusType::OK) { throw new \RuntimeException(sprintf('Projection %s in content repository %s is not setup correctly, please run `./flow cr:setup`', $status->subscriptionId->value, $contentRepositoryId->value)); From 51f0cf6762ebe6aad99e3bbde3530a09e3e83ba3 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 25 Nov 2024 14:21:28 +0100 Subject: [PATCH 09/24] TASK: Leave warning hint for why we do a replay see https://github.com/neos/neos-development-collection/pull/5378#discussion_r1855529168 --- Neos.Neos/Classes/Domain/Service/SiteImportService.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Neos.Neos/Classes/Domain/Service/SiteImportService.php b/Neos.Neos/Classes/Domain/Service/SiteImportService.php index bd6a54d17d0..7aa8c3afd2b 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteImportService.php +++ b/Neos.Neos/Classes/Domain/Service/SiteImportService.php @@ -83,8 +83,9 @@ public function importFromPath(ContentRepositoryId $contentRepositoryId, string 'Create Neos sites' => new SiteCreationProcessor($this->siteRepository, $this->domainRepository, $this->persistenceManager), 'Import events' => $this->contentRepositoryRegistry->buildService($contentRepositoryId, new EventStoreImportProcessorFactory(WorkspaceName::forLive(), keepEventIds: true)), 'Import assets' => new AssetRepositoryImportProcessor($this->assetRepository, $this->resourceRepository, $this->resourceManager, $this->persistenceManager), - // todo we do a replay here even though it will redo the live workspace creation. But otherwise the catchup hooks are not skipped because it seems like a regular catchup - 'Catchup all projections' => new ProjectionReplayProcessor($contentRepositoryMaintainer), + // WARNING! We do a replay here even though it will redo the live workspace creation. But otherwise the catchup hooks cannot determine that they need to be skipped as it seems like a regular catchup + // In case we allow to import events into other root workspaces, or don't expect live to be empty (see Import events), this would need to be adjusted, as otherwise existing data will be replayed + 'Replay all projections' => new ProjectionReplayProcessor($contentRepositoryMaintainer), ]); foreach ($processors as $processorLabel => $processor) { From a2a24111359df26dfd2239324a8b233d9bfb6804 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 25 Nov 2024 14:33:53 +0100 Subject: [PATCH 10/24] TASK: Warn in `catchupProjection` if projection is not ready to be caught up --- .../Service/ContentRepositoryMaintainer.php | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php b/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php index 6929e706be7..64feda60dcf 100644 --- a/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php +++ b/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php @@ -11,6 +11,8 @@ use Neos\ContentRepository\Core\Subscription\Engine\Errors; use Neos\ContentRepository\Core\Subscription\Engine\SubscriptionEngine; use Neos\ContentRepository\Core\Subscription\Engine\SubscriptionEngineCriteria; +use Neos\ContentRepository\Core\Subscription\Store\SubscriptionCriteria; +use Neos\ContentRepository\Core\Subscription\SubscriptionStatus; use Neos\ContentRepository\Core\Subscription\SubscriptionStatusCollection; use Neos\ContentRepository\Core\Subscription\SubscriptionId; use Neos\Error\Messages\Error; @@ -129,22 +131,29 @@ public function replayAllProjections(\Closure|null $progressCallback = null): Er */ public function catchupProjection(SubscriptionId $subscriptionId, \Closure|null $progressCallback = null): Error|null { - // todo if a projection is in error state and will be explicit caught up here we might as well attempt that without saying the user should setup? - // also setup then can avoid doing the repairing! - $bootResult = $this->subscriptionEngine->boot(SubscriptionEngineCriteria::create([$subscriptionId]), progressCallback: $progressCallback); - if ($bootResult->errors !== null) { - return self::createErrorForReason('catchup', $bootResult->errors); + $subscriptionStatus = $this->subscriptionEngine->subscriptionStatus(SubscriptionCriteria::create([$subscriptionId]))->first(); + + if ($subscriptionStatus === null) { + return new Error(sprintf('Projection "%s" is not registered.', $subscriptionId->value)); } - if ($bootResult->numberOfProcessedEvents > 0) { - // the projection was bootet + + if ($subscriptionStatus->subscriptionStatus === SubscriptionStatus::BOOTING) { + $bootResult = $this->subscriptionEngine->boot(SubscriptionEngineCriteria::create([$subscriptionId]), progressCallback: $progressCallback); + if ($bootResult->errors !== null) { + return self::createErrorForReason('booting', $bootResult->errors); + } return null; } - // todo the projection was active, and we might still want to catch it up ... find reason for this? And combine boot and catchup? - $catchupResult = $this->subscriptionEngine->catchUpActive(SubscriptionEngineCriteria::create([$subscriptionId]), progressCallback: $progressCallback); - if ($catchupResult->errors !== null) { - return self::createErrorForReason('catchup', $catchupResult->errors); + + if ($subscriptionStatus->subscriptionStatus === SubscriptionStatus::ACTIVE) { + $catchupResult = $this->subscriptionEngine->catchUpActive(SubscriptionEngineCriteria::create([$subscriptionId]), progressCallback: $progressCallback); + if ($catchupResult->errors !== null) { + return self::createErrorForReason('catchup', $catchupResult->errors); + } + return null; } - return null; + + return new Error(sprintf('Cannot catch-up projection "%s" with state %s. Please setup the content repository first.', $subscriptionId->value, $subscriptionStatus->subscriptionStatus->name)); } /** From 63d1589f1321ba26a1db718474d432a022c13270 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 25 Nov 2024 19:06:06 +0100 Subject: [PATCH 11/24] TASK: Document `catchupProjection` correctly --- .../Service/ContentRepositoryMaintainer.php | 18 +++++++++--------- .../Classes/Command/CrCommandController.php | 6 ++++-- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php b/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php index 64feda60dcf..e7f5729fd12 100644 --- a/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php +++ b/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php @@ -49,9 +49,9 @@ * as {@see ContentRepository::handle()} triggers the projections after applying the events. * * For initialising on a new database - which contains events already - a replay will make sure that the projections - * are emptied and reapply the events. + * are emptied and reapply the events. After registering a new projection a setup is needed and a replay of this projection. * - * The explicit catchup of a projection is only required when adding new projections after installation, of after fixing a projection error. + * The explicit catchup of a projection is only required after fixing a projection error. * * @api */ @@ -121,13 +121,13 @@ public function replayAllProjections(\Closure|null $progressCallback = null): Er } /** - * Catchup one specific projection. + * Catchup one specific projection for debugging or fixing it. * - * The explicit catchup is required for new projections in the booting state. + * The explicit catchup is only needed for projections in the booting state with an advanced position. + * So in the case of an error or for a detached projection, the setup will move the projection back to booting keeping its current position. + * Running a full replay would work but might be overkill, instead this catchup will just attempt boot the projection back to active. * - * We don't offer an API to catch up all projections catchupAllProjection as we would have to distinct between booting or catchup if its active already. - * - * This method is only needed in rare cases for debugging or after installing a new projection or fixing its errors. + * We don't offer an API to catch up all projections at once (like catchupAllProjections). Instead, a replayAll can be used. */ public function catchupProjection(SubscriptionId $subscriptionId, \Closure|null $progressCallback = null): Error|null { @@ -201,7 +201,7 @@ private function findAllContentStreamStreamNames(): array VirtualStreamName::forCategory(ContentStreamEventStreamName::EVENT_STREAM_NAME_PREFIX), EventStreamFilter::create( EventTypes::create( - // we are only interested in the creation events to limit the amount of events to fetch + // we are only interested in the creation events to limit the amount of events to fetch EventType::fromString('ContentStreamWasCreated'), EventType::fromString('ContentStreamWasForked') ) @@ -223,7 +223,7 @@ private function findAllWorkspaceStreamNames(): array VirtualStreamName::forCategory(WorkspaceEventStreamName::EVENT_STREAM_NAME_PREFIX), EventStreamFilter::create( EventTypes::create( - // we are only interested in the creation events to limit the amount of events to fetch + // we are only interested in the creation events to limit the amount of events to fetch EventType::fromString('RootWorkspaceWasCreated'), EventType::fromString('WorkspaceWasCreated') ) diff --git a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php index 47e8a3198c2..6cc53d9d14d 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php +++ b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php @@ -247,9 +247,11 @@ public function projectionReplayAllCommand(string $contentRepository = 'default' } /** - * Catchup one specific projection. + * Catchup one specific projection for debugging or fixing it. * - * The explicit catchup is required for new projections in the booting state, after installing a new projection or fixing its errors. + * The explicit catchup is only needed for projections in the booting state with an advanced position. + * So in the case of an error or for a detached projection, the setup will move the projection back to booting keeping its current position. + * Running a full replay would work but might be overkill, instead this catchup will just attempt boot the projection back to active. * * @param string $projection Identifier of the projection to catchup like it was configured (e.g. "contentGraph", "Vendor.Package:YourProjection") * @param string $contentRepository Identifier of the Content Repository instance to operate on From 2297c141b9dc4371f4118abef2d689346f5fbad6 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 25 Nov 2024 19:33:01 +0100 Subject: [PATCH 12/24] TASK: Reintroduce `ContentRepositoryStatus` object and expose current event store position for debugging and status information --- .../Service/ContentRepositoryMaintainer.php | 24 +++++---- .../ContentRepositoryStatus.php | 50 +++++++++++++++++++ .../Classes/Command/CrCommandController.php | 21 ++++---- .../Domain/Service/SiteImportService.php | 7 ++- 4 files changed, 77 insertions(+), 25 deletions(-) create mode 100644 Neos.ContentRepository.Core/Classes/SharedModel/ContentRepository/ContentRepositoryStatus.php diff --git a/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php b/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php index e7f5729fd12..0f21315d736 100644 --- a/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php +++ b/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php @@ -8,19 +8,20 @@ use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; use Neos\ContentRepository\Core\Feature\ContentStreamEventStreamName; use Neos\ContentRepository\Core\Feature\WorkspaceEventStreamName; +use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryStatus; use Neos\ContentRepository\Core\Subscription\Engine\Errors; use Neos\ContentRepository\Core\Subscription\Engine\SubscriptionEngine; use Neos\ContentRepository\Core\Subscription\Engine\SubscriptionEngineCriteria; use Neos\ContentRepository\Core\Subscription\Store\SubscriptionCriteria; use Neos\ContentRepository\Core\Subscription\SubscriptionStatus; -use Neos\ContentRepository\Core\Subscription\SubscriptionStatusCollection; use Neos\ContentRepository\Core\Subscription\SubscriptionId; +use Neos\ContentRepository\Core\Subscription\SubscriptionStatusCollection; use Neos\Error\Messages\Error; use Neos\EventStore\EventStoreInterface; use Neos\EventStore\Model\Event\EventType; use Neos\EventStore\Model\Event\EventTypes; +use Neos\EventStore\Model\Event\SequenceNumber; use Neos\EventStore\Model\Event\StreamName; -use Neos\EventStore\Model\EventStore\Status as EventStoreStatus; use Neos\EventStore\Model\EventStream\EventStreamFilter; use Neos\EventStore\Model\EventStream\VirtualStreamName; @@ -37,10 +38,10 @@ * Staus information * ----------------- * The status of the content repository e.g. if a setup is required or if all subscriptions are active and their position - * can be examined with two methods: + * can be examined with {@see status} * - * The event store status is available via {@see eventStoreStatus}, while the subscription status are returned - * via {@see SubscriptionStatusCollection}. Further documentation in {@see SubscriptionStatusCollection}. + * The event store status is available via {@see ContentRepositoryStatus::$eventStoreStatus}, and the subscription status + * via {@see ContentRepositoryStatus::$subscriptionStatus}. Further documentation in {@see SubscriptionStatusCollection}. * * Replay / Catchup of projections * ------------------------------- @@ -84,14 +85,15 @@ public function setUp(): Error|null return null; } - public function eventStoreStatus(): EventStoreStatus + public function status(): ContentRepositoryStatus { - return $this->eventStore->status(); - } + $lastEventEnvelope = current(iterator_to_array($this->eventStore->load(VirtualStreamName::all())->backwards()->limit(1))) ?: null; - public function subscriptionStatus(): SubscriptionStatusCollection - { - return $this->subscriptionEngine->subscriptionStatus(); + return ContentRepositoryStatus::create( + $this->eventStore->status(), + $lastEventEnvelope?->sequenceNumber ?? SequenceNumber::none(), + $this->subscriptionEngine->subscriptionStatus() + ); } public function replayProjection(SubscriptionId $subscriptionId, \Closure|null $progressCallback = null): Error|null diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/ContentRepository/ContentRepositoryStatus.php b/Neos.ContentRepository.Core/Classes/SharedModel/ContentRepository/ContentRepositoryStatus.php new file mode 100644 index 00000000000..d54faac3a52 --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/SharedModel/ContentRepository/ContentRepositoryStatus.php @@ -0,0 +1,50 @@ +contentRepositoryRegistry->buildService($contentRepositoryId, new ContentRepositoryMaintainerFactory()); - $eventStoreStatus = $contentRepositoryMaintainer->eventStoreStatus(); + $crStatus = $contentRepositoryMaintainer->status(); $hasErrors = false; $setupRequired = false; $bootingRequired = false; - $this->output('Event Store: '); - $this->outputLine(match ($eventStoreStatus->type) { + $this->outputLine('Event Store:'); + $this->output(' Setup: '); + $this->outputLine(match ($crStatus->eventStoreStatus->type) { StatusType::OK => 'OK', StatusType::SETUP_REQUIRED => 'Setup required!', StatusType::ERROR => 'ERROR', }); - $hasErrors |= $eventStoreStatus->type === StatusType::ERROR; - if ($verbose && $eventStoreStatus->details !== '') { - $this->outputFormatted($eventStoreStatus->details, [], 2); + $this->output(' Position: %d', [$crStatus->eventStorePosition->value]); + $hasErrors |= $crStatus->eventStoreStatus->type === StatusType::ERROR; + if ($verbose && $crStatus->eventStoreStatus->details !== '') { + $this->outputFormatted($crStatus->eventStoreStatus->details, [], 2); } $this->outputLine(); $this->outputLine('Subscriptions:'); - $subscriptionStatusCollection = $contentRepositoryMaintainer->subscriptionStatus(); - if ($subscriptionStatusCollection->isEmpty()) { + if ($crStatus->subscriptionStatus->isEmpty()) { $this->outputLine('There are no registered subscriptions yet, please run ./flow cr:setup'); $this->quit(1); } - foreach ($subscriptionStatusCollection as $status) { + foreach ($crStatus->subscriptionStatus as $status) { if ($status instanceof DetachedSubscriptionStatus) { $this->outputLine(' %s:', [$status->subscriptionId->value]); $this->output(' Subscription: '); @@ -134,7 +135,7 @@ public function statusCommand(string $contentRepository = 'default', bool $verbo $this->outputLine('Setup required, please run ./flow cr:setup'); } if ($bootingRequired) { - $this->outputLine('Catchup needed for projections, please run ./flow cr:projectioncatchup [projection-name]'); + $this->outputLine('Catchup or replay needed for BOOTING projections'); } if ($hasErrors) { $this->outputLine('Some projections are not okay'); diff --git a/Neos.Neos/Classes/Domain/Service/SiteImportService.php b/Neos.Neos/Classes/Domain/Service/SiteImportService.php index 7aa8c3afd2b..2c8d5aca54a 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteImportService.php +++ b/Neos.Neos/Classes/Domain/Service/SiteImportService.php @@ -96,12 +96,11 @@ public function importFromPath(ContentRepositoryId $contentRepositoryId, string private function requireContentRepositoryToBeSetup(ContentRepositoryMaintainer $contentRepositoryMaintainer, ContentRepositoryId $contentRepositoryId): void { - $eventStoreStatus = $contentRepositoryMaintainer->eventStoreStatus(); - if ($eventStoreStatus->type !== StatusType::OK) { + $status = $contentRepositoryMaintainer->status(); + if ($status->eventStoreStatus->type !== StatusType::OK) { throw new \RuntimeException(sprintf('Content repository %s is not setup correctly, please run `./flow cr:setup`', $contentRepositoryId->value)); } - $subscriptionStatusCollection = $contentRepositoryMaintainer->subscriptionStatus(); - foreach ($subscriptionStatusCollection as $status) { + foreach ($status->subscriptionStatus as $status) { if ($status instanceof ProjectionSubscriptionStatus) { if ($status->setupStatus->type !== ProjectionStatusType::OK) { throw new \RuntimeException(sprintf('Projection %s in content repository %s is not setup correctly, please run `./flow cr:setup`', $status->subscriptionId->value, $contentRepositoryId->value)); From 1220f822aac149a38337f1bf0aaf670d5136f18a Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 25 Nov 2024 19:36:18 +0100 Subject: [PATCH 13/24] TASK: Swap Projection and Setup in output so that Setup comes first --- .../Classes/Command/CrCommandController.php | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php index 37ec012b47a..6aa5af59a81 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php +++ b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php @@ -73,7 +73,7 @@ public function statusCommand(string $contentRepository = 'default', bool $verbo StatusType::SETUP_REQUIRED => 'Setup required!', StatusType::ERROR => 'ERROR', }); - $this->output(' Position: %d', [$crStatus->eventStorePosition->value]); + $this->outputLine(' Position: %d', [$crStatus->eventStorePosition->value]); $hasErrors |= $crStatus->eventStoreStatus->type === StatusType::ERROR; if ($verbose && $crStatus->eventStoreStatus->details !== '') { $this->outputFormatted($crStatus->eventStoreStatus->details, [], 2); @@ -93,6 +93,21 @@ public function statusCommand(string $contentRepository = 'default', bool $verbo } if ($status instanceof ProjectionSubscriptionStatus) { $this->outputLine(' %s:', [$status->subscriptionId->value]); + $this->output(' Setup: '); + $this->outputLine(match ($status->setupStatus->type) { + ProjectionStatusType::OK => 'OK', + ProjectionStatusType::SETUP_REQUIRED => 'SETUP REQUIRED', + ProjectionStatusType::ERROR => 'ERROR', + }); + $hasErrors |= $status->setupStatus->type === ProjectionStatusType::ERROR; + $setupRequired |= $status->setupStatus->type === ProjectionStatusType::SETUP_REQUIRED; + if ($verbose && ($status->setupStatus->type !== ProjectionStatusType::OK || $status->setupStatus->details)) { + $lines = explode(chr(10), $status->setupStatus->details ?: 'No details available.'); + foreach ($lines as $line) { + $this->outputLine(' ' . $line); + } + $this->outputLine(); + } $this->output(' Projection: '); $this->output(match ($status->subscriptionStatus) { SubscriptionStatus::NEW => 'NEW', @@ -112,21 +127,6 @@ public function statusCommand(string $contentRepository = 'default', bool $verbo $this->outputLine(' %s', [$line]); } } - $this->output(' Setup: '); - $this->outputLine(match ($status->setupStatus->type) { - ProjectionStatusType::OK => 'OK', - ProjectionStatusType::SETUP_REQUIRED => 'SETUP REQUIRED', - ProjectionStatusType::ERROR => 'ERROR', - }); - $hasErrors |= $status->setupStatus->type === ProjectionStatusType::ERROR; - $setupRequired |= $status->setupStatus->type === ProjectionStatusType::SETUP_REQUIRED; - if ($verbose && ($status->setupStatus->type !== ProjectionStatusType::OK || $status->setupStatus->details)) { - $lines = explode(chr(10), $status->setupStatus->details ?: 'No details available.'); - foreach ($lines as $line) { - $this->outputLine(' ' . $line); - } - $this->outputLine(); - } } } if ($verbose) { From 800fd5317d2b0fdd28e008ff6c66eb2d13b97498 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Tue, 26 Nov 2024 14:24:43 +0100 Subject: [PATCH 14/24] WIP: Introduce `cr:reactivateSubscription` in hindsight that well revert that setup updates the error state or detached state --- .../Service/ContentRepositoryMaintainer.php | 60 +++++++++---------- .../Engine/SubscriptionEngine.php | 7 ++- .../Classes/Command/CrCommandController.php | 27 +++++---- 3 files changed, 47 insertions(+), 47 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php b/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php index 0f21315d736..4b79aa012e2 100644 --- a/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php +++ b/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php @@ -13,7 +13,6 @@ use Neos\ContentRepository\Core\Subscription\Engine\SubscriptionEngine; use Neos\ContentRepository\Core\Subscription\Engine\SubscriptionEngineCriteria; use Neos\ContentRepository\Core\Subscription\Store\SubscriptionCriteria; -use Neos\ContentRepository\Core\Subscription\SubscriptionStatus; use Neos\ContentRepository\Core\Subscription\SubscriptionId; use Neos\ContentRepository\Core\Subscription\SubscriptionStatusCollection; use Neos\Error\Messages\Error; @@ -43,16 +42,28 @@ * The event store status is available via {@see ContentRepositoryStatus::$eventStoreStatus}, and the subscription status * via {@see ContentRepositoryStatus::$subscriptionStatus}. Further documentation in {@see SubscriptionStatusCollection}. * - * Replay / Catchup of projections - * ------------------------------- - * The methods {@see replayProjection}, {@see replayAllProjections} and {@see catchupProjection} - * can be leveraged to interact with the projection catchup. In the happy path no interaction is necessary, - * as {@see ContentRepository::handle()} triggers the projections after applying the events. + * Projection subscriptions + * ------------------------ + * + * This maintainer offers also the public API to interact with the projection catchup. In the happy path, + * no interaction is necessary, as {@see ContentRepository::handle()} triggers the projections after applying the events. + * + * Special cases: + * + * *Replay* * * For initialising on a new database - which contains events already - a replay will make sure that the projections - * are emptied and reapply the events. After registering a new projection a setup is needed and a replay of this projection. + * are emptied and reapply the events. This can be triggered via {@see replayProjection} or {@see replayAllProjections} + * + * And after registering a new projection a setup as well as a replay of this projection is also required. * - * The explicit catchup of a projection is only required after fixing a projection error. + * *Reactivate* + * + * In case a projection is detached but is reinstalled a reactivation is needed via {@see reactivateSubscription} + * + * Also in case a projection runs into the error status, its code needs to be fixed, and it can also be attempted to be reactivated. + * + * Note that in both cases a projection replay would also work, but with the difference that the projection is reset as well. * * @api */ @@ -123,39 +134,22 @@ public function replayAllProjections(\Closure|null $progressCallback = null): Er } /** - * Catchup one specific projection for debugging or fixing it. - * - * The explicit catchup is only needed for projections in the booting state with an advanced position. - * So in the case of an error or for a detached projection, the setup will move the projection back to booting keeping its current position. - * Running a full replay would work but might be overkill, instead this catchup will just attempt boot the projection back to active. + * Reactivate a projection * - * We don't offer an API to catch up all projections at once (like catchupAllProjections). Instead, a replayAll can be used. + * The explicit catchup is only needed for projections in the error or detached status with an advanced position. + * Running a full replay would work but might be overkill, instead this reactivation will just attempt + * catchup the projection back to active from its current position. */ - public function catchupProjection(SubscriptionId $subscriptionId, \Closure|null $progressCallback = null): Error|null + public function reactivateSubscription(SubscriptionId $subscriptionId, \Closure|null $progressCallback = null): Error|null { $subscriptionStatus = $this->subscriptionEngine->subscriptionStatus(SubscriptionCriteria::create([$subscriptionId]))->first(); if ($subscriptionStatus === null) { - return new Error(sprintf('Projection "%s" is not registered.', $subscriptionId->value)); + return new Error(sprintf('Subscription "%s" is not registered.', $subscriptionId->value)); } - if ($subscriptionStatus->subscriptionStatus === SubscriptionStatus::BOOTING) { - $bootResult = $this->subscriptionEngine->boot(SubscriptionEngineCriteria::create([$subscriptionId]), progressCallback: $progressCallback); - if ($bootResult->errors !== null) { - return self::createErrorForReason('booting', $bootResult->errors); - } - return null; - } - - if ($subscriptionStatus->subscriptionStatus === SubscriptionStatus::ACTIVE) { - $catchupResult = $this->subscriptionEngine->catchUpActive(SubscriptionEngineCriteria::create([$subscriptionId]), progressCallback: $progressCallback); - if ($catchupResult->errors !== null) { - return self::createErrorForReason('catchup', $catchupResult->errors); - } - return null; - } - - return new Error(sprintf('Cannot catch-up projection "%s" with state %s. Please setup the content repository first.', $subscriptionId->value, $subscriptionStatus->subscriptionStatus->name)); + // todo implement https://github.com/patchlevel/event-sourcing/blob/b8591c56b21b049f46bead8e7ab424fd2afe9917/src/Subscription/Engine/DefaultSubscriptionEngine.php#L624 + return null; } /** diff --git a/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionEngine.php b/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionEngine.php index e86537f47d8..d6a0ce79379 100644 --- a/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionEngine.php +++ b/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionEngine.php @@ -28,7 +28,12 @@ use Neos\ContentRepository\Core\Subscription\Subscriptions; /** - * @internal public API is the {@see ContentRepository::handle()} and the {@see ContentRepositoryMaintainer} + * This is the internal core for the catchup + * + * All functionality is low level and well encapsulated and abstracted by the {@see ContentRepositoryMaintainer} + * It presents the only API way to interact with catchup and offers more maintenance tasks. + * + * @internal implementation detail of {@see ContentRepository::handle()} and the {@see ContentRepositoryMaintainer} */ final class SubscriptionEngine { diff --git a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php index 6aa5af59a81..e4eb21b1c75 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php +++ b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php @@ -64,6 +64,7 @@ public function statusCommand(string $contentRepository = 'default', bool $verbo $contentRepositoryMaintainer = $this->contentRepositoryRegistry->buildService($contentRepositoryId, new ContentRepositoryMaintainerFactory()); $crStatus = $contentRepositoryMaintainer->status(); $hasErrors = false; + $reactivationRequired = false; $setupRequired = false; $bootingRequired = false; $this->outputLine('Event Store:'); @@ -118,9 +119,9 @@ public function statusCommand(string $contentRepository = 'default', bool $verbo }); $this->outputLine(' at position %d', [$status->subscriptionPosition->value]); $hasErrors |= $status->subscriptionStatus === SubscriptionStatus::ERROR; + $reactivationRequired |= $status->subscriptionStatus === SubscriptionStatus::ERROR; $bootingRequired |= $status->subscriptionStatus === SubscriptionStatus::BOOTING; - // detached can be reattached via setup: - $setupRequired |= $status->subscriptionStatus === SubscriptionStatus::DETACHED; + $reactivationRequired |= $status->subscriptionStatus === SubscriptionStatus::DETACHED; if ($verbose && $status->subscriptionError !== null) { $lines = explode(chr(10), $status->subscriptionError->errorMessage ?: 'No details available.'); foreach ($lines as $line) { @@ -135,10 +136,10 @@ public function statusCommand(string $contentRepository = 'default', bool $verbo $this->outputLine('Setup required, please run ./flow cr:setup'); } if ($bootingRequired) { - $this->outputLine('Catchup or replay needed for BOOTING projections'); + $this->outputLine('Replay needed for BOOTING projections, please run ./flow cr:projectionreplay [subscription-id]'); } - if ($hasErrors) { - $this->outputLine('Some projections are not okay'); + if ($reactivationRequired) { + $this->outputLine('Reactivation of ERROR or DETACHED projection required, please run ./flow cr:reactivatesubscription [subscription-id]'); } } if ($hasErrors) { @@ -248,31 +249,31 @@ public function projectionReplayAllCommand(string $contentRepository = 'default' } /** - * Catchup one specific projection for debugging or fixing it. + * Reactivate a projection * - * The explicit catchup is only needed for projections in the booting state with an advanced position. - * So in the case of an error or for a detached projection, the setup will move the projection back to booting keeping its current position. - * Running a full replay would work but might be overkill, instead this catchup will just attempt boot the projection back to active. + * The explicit catchup is only needed for projections in the error or detached status with an advanced position. + * Running a full replay would work but might be overkill, instead this reactivation will just attempt + * catchup the projection back to active from its current position. * - * @param string $projection Identifier of the projection to catchup like it was configured (e.g. "contentGraph", "Vendor.Package:YourProjection") + * @param string $projection Identifier of the projection to reactivate like it was configured (e.g. "contentGraph", "Vendor.Package:YourProjection") * @param string $contentRepository Identifier of the Content Repository instance to operate on * @param bool $quiet If set only fatal errors are rendered to the output (must be used with --force flag to avoid user input) */ - public function projectionCatchupCommand(string $projection, string $contentRepository = 'default', bool $quiet = false): void + public function reactivateSubscriptionCommand(string $projection, string $contentRepository = 'default', bool $quiet = false): void { $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); $contentRepositoryMaintainer = $this->contentRepositoryRegistry->buildService($contentRepositoryId, new ContentRepositoryMaintainerFactory()); $progressCallback = null; if (!$quiet) { - $this->outputLine('Catchup projection "%s" of Content Repository "%s" ...', [$projection, $contentRepositoryId->value]); + $this->outputLine('Reactivate projection "%s" of Content Repository "%s" ...', [$projection, $contentRepositoryId->value]); // render memory consumption and time remaining $this->output->getProgressBar()->setFormat('debug'); $this->output->progressStart(); $progressCallback = fn () => $this->output->progressAdvance(); } - $result = $contentRepositoryMaintainer->catchupProjection(SubscriptionId::fromString($projection), progressCallback: $progressCallback); + $result = $contentRepositoryMaintainer->reactivateSubscription(SubscriptionId::fromString($projection), progressCallback: $progressCallback); if (!$quiet) { $this->output->progressFinish(); From a0c9f904ac9deaf1ea803b07f3736ac238837a55 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 27 Nov 2024 13:12:11 +0100 Subject: [PATCH 15/24] TASK: Dont crash on status when the event store is not setup --- .../Classes/Service/ContentRepositoryMaintainer.php | 10 ++++++++-- .../ContentRepository/ContentRepositoryStatus.php | 9 +++++++-- .../Classes/Command/CrCommandController.php | 13 +++++++++++-- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php b/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php index 4b79aa012e2..261509a8576 100644 --- a/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php +++ b/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php @@ -23,6 +23,7 @@ use Neos\EventStore\Model\Event\StreamName; use Neos\EventStore\Model\EventStream\EventStreamFilter; use Neos\EventStore\Model\EventStream\VirtualStreamName; +use Doctrine\DBAL\Exception as DBALException; /** * Set up and manage a content repository @@ -98,11 +99,16 @@ public function setUp(): Error|null public function status(): ContentRepositoryStatus { - $lastEventEnvelope = current(iterator_to_array($this->eventStore->load(VirtualStreamName::all())->backwards()->limit(1))) ?: null; + try { + $lastEventEnvelope = current(iterator_to_array($this->eventStore->load(VirtualStreamName::all())->backwards()->limit(1))) ?: null; + $sequenceNumber = $lastEventEnvelope?->sequenceNumber ?? SequenceNumber::none(); + } catch (DBALException) { + $sequenceNumber = null; + } return ContentRepositoryStatus::create( $this->eventStore->status(), - $lastEventEnvelope?->sequenceNumber ?? SequenceNumber::none(), + $sequenceNumber, $this->subscriptionEngine->subscriptionStatus() ); } diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/ContentRepository/ContentRepositoryStatus.php b/Neos.ContentRepository.Core/Classes/SharedModel/ContentRepository/ContentRepositoryStatus.php index d54faac3a52..3c2cd61e244 100644 --- a/Neos.ContentRepository.Core/Classes/SharedModel/ContentRepository/ContentRepositoryStatus.php +++ b/Neos.ContentRepository.Core/Classes/SharedModel/ContentRepository/ContentRepositoryStatus.php @@ -26,9 +26,14 @@ */ final readonly class ContentRepositoryStatus { + /** + * @param EventStoreStatus $eventStoreStatus + * @param SequenceNumber|null $eventStorePosition The position of the event store. NULL if an error occurred, see error state of $eventStoreStatus + * @param SubscriptionStatusCollection $subscriptionStatus + */ private function __construct( public EventStoreStatus $eventStoreStatus, - public SequenceNumber $eventStorePosition, + public SequenceNumber|null $eventStorePosition, public SubscriptionStatusCollection $subscriptionStatus, ) { } @@ -38,7 +43,7 @@ private function __construct( */ public static function create( EventStoreStatus $eventStoreStatus, - SequenceNumber $eventStorePosition, + SequenceNumber|null $eventStorePosition, SubscriptionStatusCollection $subscriptionStatus, ): self { return new self( diff --git a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php index e4eb21b1c75..17589ac1c0a 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php +++ b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php @@ -74,7 +74,11 @@ public function statusCommand(string $contentRepository = 'default', bool $verbo StatusType::SETUP_REQUIRED => 'Setup required!', StatusType::ERROR => 'ERROR', }); - $this->outputLine(' Position: %d', [$crStatus->eventStorePosition->value]); + if ($crStatus->eventStorePosition) { + $this->outputLine(' Position: %d', [$crStatus->eventStorePosition->value]); + } else { + $this->outputLine(' Position: Loading failed!'); + } $hasErrors |= $crStatus->eventStoreStatus->type === StatusType::ERROR; if ($verbose && $crStatus->eventStoreStatus->details !== '') { $this->outputFormatted($crStatus->eventStoreStatus->details, [], 2); @@ -117,7 +121,12 @@ public function statusCommand(string $contentRepository = 'default', bool $verbo SubscriptionStatus::DETACHED => 'DETACHED', SubscriptionStatus::ERROR => 'ERROR', }); - $this->outputLine(' at position %d', [$status->subscriptionPosition->value]); + if ($crStatus->eventStorePosition?->value > $status->subscriptionPosition->value) { + // projection is behind + $this->outputLine(' at position %d', [$status->subscriptionPosition->value]); + } else { + $this->outputLine(' at position %d', [$status->subscriptionPosition->value]); + } $hasErrors |= $status->subscriptionStatus === SubscriptionStatus::ERROR; $reactivationRequired |= $status->subscriptionStatus === SubscriptionStatus::ERROR; $bootingRequired |= $status->subscriptionStatus === SubscriptionStatus::BOOTING; From 8c079d9657dbccf190585df81dbff005a1a1e2b6 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 27 Nov 2024 13:48:09 +0100 Subject: [PATCH 16/24] TASK: Split projection replay into separate SubscriptionCommandController this allows us to keep the namings short and precise instead of introducing `cr:subscriptionreplayall` see also: https://neos-project.slack.com/archives/C04PYL8H3/p1732629147379509 --- .../Service/ContentRepositoryMaintainer.php | 36 ++-- .../Features/Bootstrap/CRTestSuiteTrait.php | 2 +- .../Classes/Command/CrCommandController.php | 168 +++++----------- .../Command/SubscriptionCommandController.php | 180 ++++++++++++++++++ .../Processors/ProjectionReplayProcessor.php | 2 +- 5 files changed, 248 insertions(+), 140 deletions(-) create mode 100644 Neos.ContentRepositoryRegistry/Classes/Command/SubscriptionCommandController.php diff --git a/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php b/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php index 261509a8576..2bc514668a1 100644 --- a/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php +++ b/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php @@ -30,10 +30,10 @@ * * Initialisation / Tear down * -------------------------- - * The method {@see setUp} sets up the content repository like event store and projection database tables. + * The method {@see setUp} sets up the content repository like event store and subscription database tables. * It is non-destructive. * - * Resetting a content repository with {@see prune} method will purge the event stream and reset all projection states. + * Resetting a content repository with {@see prune} method will purge the event stream and reset all subscription states. * * Staus information * ----------------- @@ -43,28 +43,28 @@ * The event store status is available via {@see ContentRepositoryStatus::$eventStoreStatus}, and the subscription status * via {@see ContentRepositoryStatus::$subscriptionStatus}. Further documentation in {@see SubscriptionStatusCollection}. * - * Projection subscriptions - * ------------------------ + * Subscriptions (mainly projections) + * ---------------------------------- * - * This maintainer offers also the public API to interact with the projection catchup. In the happy path, - * no interaction is necessary, as {@see ContentRepository::handle()} triggers the projections after applying the events. + * This maintainer offers also the public API to interact with the subscription catchup. In the happy path, + * no interaction is necessary, as {@see ContentRepository::handle()} triggers the subscriptions after applying the events. * * Special cases: * * *Replay* * - * For initialising on a new database - which contains events already - a replay will make sure that the projections - * are emptied and reapply the events. This can be triggered via {@see replayProjection} or {@see replayAllProjections} + * For initialising on a new database - which contains events already - a replay will make sure that the subscriptions + * are emptied and reapply the events. This can be triggered via {@see replaySubscription} or {@see replayAllSubscriptions} * - * And after registering a new projection a setup as well as a replay of this projection is also required. + * And after registering a new subscription a setup as well as a replay of this subscription is also required. * * *Reactivate* * - * In case a projection is detached but is reinstalled a reactivation is needed via {@see reactivateSubscription} + * In case a subscription is detached but is reinstalled a reactivation is needed via {@see reactivateSubscription} * - * Also in case a projection runs into the error status, its code needs to be fixed, and it can also be attempted to be reactivated. + * Also in case a subscription runs into the error status, its code needs to be fixed, and it can also be attempted to be reactivated. * - * Note that in both cases a projection replay would also work, but with the difference that the projection is reset as well. + * Note that in both cases a subscription replay would also work, but with the difference that the subscription is reset as well. * * @api */ @@ -113,7 +113,7 @@ public function status(): ContentRepositoryStatus ); } - public function replayProjection(SubscriptionId $subscriptionId, \Closure|null $progressCallback = null): Error|null + public function replaySubscription(SubscriptionId $subscriptionId, \Closure|null $progressCallback = null): Error|null { $resetResult = $this->subscriptionEngine->reset(SubscriptionEngineCriteria::create([$subscriptionId])); if ($resetResult->errors !== null) { @@ -126,7 +126,7 @@ public function replayProjection(SubscriptionId $subscriptionId, \Closure|null $ return null; } - public function replayAllProjections(\Closure|null $progressCallback = null): Error|null + public function replayAllSubscriptions(\Closure|null $progressCallback = null): Error|null { $resetResult = $this->subscriptionEngine->reset(); if ($resetResult->errors !== null) { @@ -140,11 +140,11 @@ public function replayAllProjections(\Closure|null $progressCallback = null): Er } /** - * Reactivate a projection + * Reactivate a subscription * - * The explicit catchup is only needed for projections in the error or detached status with an advanced position. + * The explicit catchup is only needed for subscriptions in the error or detached status with an advanced position. * Running a full replay would work but might be overkill, instead this reactivation will just attempt - * catchup the projection back to active from its current position. + * catchup the subscription back to active from its current position. */ public function reactivateSubscription(SubscriptionId $subscriptionId, \Closure|null $progressCallback = null): Error|null { @@ -159,7 +159,7 @@ public function reactivateSubscription(SubscriptionId $subscriptionId, \Closure| } /** - * WARNING: Removes all events from the content repository and resets the projections + * WARNING: Removes all events from the content repository and resets the subscriptions * This operation cannot be undone. */ public function prune(): Error|null diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php index 777d5a9c1f3..c06fa32af4c 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php @@ -257,7 +257,7 @@ abstract protected function getContentRepositoryService( public function iReplayTheProjection(string $projectionName): void { $contentRepositoryMaintainer = $this->getContentRepositoryService(new ContentRepositoryMaintainerFactory()); - $result = $contentRepositoryMaintainer->replayProjection(SubscriptionId::fromString($projectionName)); + $result = $contentRepositoryMaintainer->replaySubscription(SubscriptionId::fromString($projectionName)); Assert::assertNull($result); } diff --git a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php index 17589ac1c0a..30af699ad72 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php +++ b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php @@ -8,7 +8,6 @@ use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\Subscription\DetachedSubscriptionStatus; use Neos\ContentRepository\Core\Subscription\ProjectionSubscriptionStatus; -use Neos\ContentRepository\Core\Subscription\SubscriptionId; use Neos\ContentRepository\Core\Subscription\SubscriptionStatus; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\EventStore\Model\EventStore\StatusType; @@ -16,6 +15,23 @@ use Neos\Flow\Cli\CommandController; use Symfony\Component\Console\Output\Output; +/** + * Set up a content repository + * + * *Initialisation* + * + * The command "./flow cr:setup" sets up the content repository like event store and subscription database tables. + * It is non-destructive. + * + * Note that a reset is not implemented here but for the Neos CMS use-case provided via "./flow site:pruneAll" + * + * *Staus information* + * + * The status of the content repository e.g. if a setup is required or if all subscriptions are active and their position + * can be examined with "./flow cr:status" + * + * See also {@see ContentRepositoryMaintainer} for more information. + */ final class CrCommandController extends CommandController { #[Flow\Inject()] @@ -145,10 +161,10 @@ public function statusCommand(string $contentRepository = 'default', bool $verbo $this->outputLine('Setup required, please run ./flow cr:setup'); } if ($bootingRequired) { - $this->outputLine('Replay needed for BOOTING projections, please run ./flow cr:projectionreplay [subscription-id]'); + $this->outputLine('Replay needed for BOOTING projections, please run ./flow subscription:replay [subscription-id]'); } if ($reactivationRequired) { - $this->outputLine('Reactivation of ERROR or DETACHED projection required, please run ./flow cr:reactivatesubscription [subscription-id]'); + $this->outputLine('Reactivation of ERROR or DETACHED projection required, please run ./flow subscription:reactivate [subscription-id]'); } } if ($hasErrors) { @@ -159,51 +175,35 @@ public function statusCommand(string $contentRepository = 'default', bool $verbo /** * Replays the specified projection of a Content Repository by resetting its state and performing a full catchup. * - * @param string $projection Identifier of the projection to replay like it was configured (e.g. "contentGraph", "Vendor.Package:YourProjection") + * @param string $projection Identifier of the projection to replay * @param string $contentRepository Identifier of the Content Repository instance to operate on * @param bool $force Replay the projection without confirmation. This may take some time! * @param bool $quiet If set only fatal errors are rendered to the output (must be used with --force flag to avoid user input) + * @internal + * @deprecated with Neos 9 Beta 17, please use ./flow subscription:replay instead */ public function projectionReplayCommand(string $projection, string $contentRepository = 'default', bool $force = false, bool $quiet = false): void { - if ($quiet) { - $this->output->getOutput()->setVerbosity(Output::VERBOSITY_QUIET); - } - if (!$force && $quiet) { - $this->outputLine('Cannot run in quiet mode without --force. Please acknowledge that this command will reset and replay this projection. This may take some time.'); - $this->quit(1); - } - - if (!$force && !$this->output->askConfirmation(sprintf('> This will replay the projection "%s" in "%s", which may take some time. Are you sure to proceed? (y/n) ', $projection, $contentRepository), false)) { - $this->outputLine('Abort.'); - return; - } - - $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); - $contentRepositoryMaintainer = $this->contentRepositoryRegistry->buildService($contentRepositoryId, new ContentRepositoryMaintainerFactory()); - - $progressCallback = null; - if (!$quiet) { - $this->outputLine('Replaying events for projection "%s" of Content Repository "%s" ...', [$projection, $contentRepositoryId->value]); - // render memory consumption and time remaining - $this->output->getProgressBar()->setFormat('debug'); - $this->output->progressStart(); - $progressCallback = fn () => $this->output->progressAdvance(); - } - - $result = $contentRepositoryMaintainer->replayProjection(SubscriptionId::fromString($projection), progressCallback: $progressCallback); - - if (!$quiet) { - $this->output->progressFinish(); - $this->outputLine(); - } - - if ($result !== null) { - $this->outputLine('%s', [$result->getMessage()]); + $this->outputLine('Please use ./flow subscription:replay instead!'); + $subscriptionId = match($projection) { + 'doctrineDbalContentGraph', + 'Neos\ContentGraph\DoctrineDbalAdapter\DoctrineDbalContentGraphProjection' => 'contentGraph', + 'documentUriPathProjection' => 'Neos.Neos:DocumentUriPathProjection', + 'change' => 'Neos.Neos:PendingChangesProjection', + default => null + }; + if ($subscriptionId === null) { + $this->outputLine('Invalid --projection specified. Could not map legacy argument.'); $this->quit(1); - } elseif (!$quiet) { - $this->outputLine('Done.'); } + $this->forward( + 'replay', + SubscriptionCommandController::class, + array_merge( + ['subscription' => $subscriptionId], + compact('contentRepository', 'force', 'quiet') + ) + ); } /** @@ -212,88 +212,16 @@ public function projectionReplayCommand(string $projection, string $contentRepos * @param string $contentRepository Identifier of the Content Repository instance to operate on * @param bool $force Replay the projection without confirmation. This may take some time! * @param bool $quiet If set only fatal errors are rendered to the output (must be used with --force flag to avoid user input) + * @internal + * @deprecated with Neos 9 Beta 17, please use ./flow subscription:replayall instead */ public function projectionReplayAllCommand(string $contentRepository = 'default', bool $force = false, bool $quiet = false): void { - if ($quiet) { - $this->output->getOutput()->setVerbosity(Output::VERBOSITY_QUIET); - } - - if (!$force && $quiet) { - $this->outputLine('Cannot run in quiet mode without --force. Please acknowledge that this command will reset and replay this projection. This may take some time.'); - $this->quit(1); - } - - if (!$force && !$this->output->askConfirmation(sprintf('> This will replay all projections in "%s", which may take some time. Are you sure to proceed? (y/n) ', $contentRepository), false)) { - $this->outputLine('Abort.'); - return; - } - - $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); - $contentRepositoryMaintainer = $this->contentRepositoryRegistry->buildService($contentRepositoryId, new ContentRepositoryMaintainerFactory()); - - $progressCallback = null; - if (!$quiet) { - $this->outputLine('Replaying events for all projections of Content Repository "%s" ...', [$contentRepositoryId->value]); - // render memory consumption and time remaining - // todo maybe reintroduce pretty output: https://github.com/neos/neos-development-collection/pull/5010 but without using highestSequenceNumber - $this->output->getProgressBar()->setFormat('debug'); - $this->output->progressStart(); - $progressCallback = fn () => $this->output->progressAdvance(); - } - - $result = $contentRepositoryMaintainer->replayAllProjections(progressCallback: $progressCallback); - - if (!$quiet) { - $this->output->progressFinish(); - $this->outputLine(); - } - - if ($result !== null) { - $this->outputLine('%s', [$result->getMessage()]); - $this->quit(1); - } elseif (!$quiet) { - $this->outputLine('Done.'); - } - } - - /** - * Reactivate a projection - * - * The explicit catchup is only needed for projections in the error or detached status with an advanced position. - * Running a full replay would work but might be overkill, instead this reactivation will just attempt - * catchup the projection back to active from its current position. - * - * @param string $projection Identifier of the projection to reactivate like it was configured (e.g. "contentGraph", "Vendor.Package:YourProjection") - * @param string $contentRepository Identifier of the Content Repository instance to operate on - * @param bool $quiet If set only fatal errors are rendered to the output (must be used with --force flag to avoid user input) - */ - public function reactivateSubscriptionCommand(string $projection, string $contentRepository = 'default', bool $quiet = false): void - { - $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); - $contentRepositoryMaintainer = $this->contentRepositoryRegistry->buildService($contentRepositoryId, new ContentRepositoryMaintainerFactory()); - - $progressCallback = null; - if (!$quiet) { - $this->outputLine('Reactivate projection "%s" of Content Repository "%s" ...', [$projection, $contentRepositoryId->value]); - // render memory consumption and time remaining - $this->output->getProgressBar()->setFormat('debug'); - $this->output->progressStart(); - $progressCallback = fn () => $this->output->progressAdvance(); - } - - $result = $contentRepositoryMaintainer->reactivateSubscription(SubscriptionId::fromString($projection), progressCallback: $progressCallback); - - if (!$quiet) { - $this->output->progressFinish(); - $this->outputLine(); - } - - if ($result !== null) { - $this->outputLine('%s', [$result->getMessage()]); - $this->quit(1); - } elseif (!$quiet) { - $this->outputLine('Done.'); - } + $this->outputLine('Please use ./flow subscription:replayall instead!'); + $this->forward( + 'replayall', + SubscriptionCommandController::class, + compact('contentRepository', 'force', 'quiet') + ); } } diff --git a/Neos.ContentRepositoryRegistry/Classes/Command/SubscriptionCommandController.php b/Neos.ContentRepositoryRegistry/Classes/Command/SubscriptionCommandController.php new file mode 100644 index 00000000000..4da391f52c7 --- /dev/null +++ b/Neos.ContentRepositoryRegistry/Classes/Command/SubscriptionCommandController.php @@ -0,0 +1,180 @@ +output->getOutput()->setVerbosity(Output::VERBOSITY_QUIET); + } + if (!$force && $quiet) { + $this->outputLine('Cannot run in quiet mode without --force. Please acknowledge that this command will reset and replay this subscription. This may take some time.'); + $this->quit(1); + } + + if (!$force && !$this->output->askConfirmation(sprintf('> This will replay the subscription "%s" in "%s", which may take some time. Are you sure to proceed? (y/n) ', $subscription, $contentRepository), false)) { + $this->outputLine('Abort.'); + return; + } + + $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); + $contentRepositoryMaintainer = $this->contentRepositoryRegistry->buildService($contentRepositoryId, new ContentRepositoryMaintainerFactory()); + + $progressCallback = null; + if (!$quiet) { + $this->outputLine('Replaying events for subscription "%s" of Content Repository "%s" ...', [$subscription, $contentRepositoryId->value]); + // render memory consumption and time remaining + $this->output->getProgressBar()->setFormat('debug'); + $this->output->progressStart(); + $progressCallback = fn () => $this->output->progressAdvance(); + } + + $result = $contentRepositoryMaintainer->replaySubscription(SubscriptionId::fromString($subscription), progressCallback: $progressCallback); + + if (!$quiet) { + $this->output->progressFinish(); + $this->outputLine(); + } + + if ($result !== null) { + $this->outputLine('%s', [$result->getMessage()]); + $this->quit(1); + } elseif (!$quiet) { + $this->outputLine('Done.'); + } + } + + /** + * Replays all projections of the specified Content Repository by resetting their states and performing a full catchup + * + * @param string $contentRepository Identifier of the Content Repository instance to operate on + * @param bool $force Replay all subscriptions without confirmation. This may take some time! + * @param bool $quiet If set only fatal errors are rendered to the output (must be used with --force flag to avoid user input) + */ + public function replayAllCommand(string $contentRepository = 'default', bool $force = false, bool $quiet = false): void + { + if ($quiet) { + $this->output->getOutput()->setVerbosity(Output::VERBOSITY_QUIET); + } + + if (!$force && $quiet) { + $this->outputLine('Cannot run in quiet mode without --force. Please acknowledge that this command will reset and replay all subscriptions. This may take some time.'); + $this->quit(1); + } + + if (!$force && !$this->output->askConfirmation(sprintf('> This will replay all projections in "%s", which may take some time. Are you sure to proceed? (y/n) ', $contentRepository), false)) { + $this->outputLine('Abort.'); + return; + } + + $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); + $contentRepositoryMaintainer = $this->contentRepositoryRegistry->buildService($contentRepositoryId, new ContentRepositoryMaintainerFactory()); + + $progressCallback = null; + if (!$quiet) { + $this->outputLine('Replaying events for all projections of Content Repository "%s" ...', [$contentRepositoryId->value]); + // render memory consumption and time remaining + // todo maybe reintroduce pretty output: https://github.com/neos/neos-development-collection/pull/5010 but without using highestSequenceNumber + $this->output->getProgressBar()->setFormat('debug'); + $this->output->progressStart(); + $progressCallback = fn () => $this->output->progressAdvance(); + } + + $result = $contentRepositoryMaintainer->replayAllSubscriptions(progressCallback: $progressCallback); + + if (!$quiet) { + $this->output->progressFinish(); + $this->outputLine(); + } + + if ($result !== null) { + $this->outputLine('%s', [$result->getMessage()]); + $this->quit(1); + } elseif (!$quiet) { + $this->outputLine('Done.'); + } + } + + /** + * Reactivate a subscription + * + * The explicit catchup is only needed for projections in the error or detached status with an advanced position. + * Running a full replay would work but might be overkill, instead this reactivation will just attempt + * catchup the subscription back to active from its current position. + * + * @param string $subscription Identifier of the subscription to reactivate like it was configured (e.g. "contentGraph", "Vendor.Package:YourProjection") + * @param string $contentRepository Identifier of the Content Repository instance to operate on + * @param bool $quiet If set only fatal errors are rendered to the output (must be used with --force flag to avoid user input) + */ + public function reactivateCommand(string $subscription, string $contentRepository = 'default', bool $quiet = false): void + { + $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); + $contentRepositoryMaintainer = $this->contentRepositoryRegistry->buildService($contentRepositoryId, new ContentRepositoryMaintainerFactory()); + + $progressCallback = null; + if (!$quiet) { + $this->outputLine('Reactivate subscription "%s" of Content Repository "%s" ...', [$subscription, $contentRepositoryId->value]); + // render memory consumption and time remaining + $this->output->getProgressBar()->setFormat('debug'); + $this->output->progressStart(); + $progressCallback = fn () => $this->output->progressAdvance(); + } + + $result = $contentRepositoryMaintainer->reactivateSubscription(SubscriptionId::fromString($subscription), progressCallback: $progressCallback); + + if (!$quiet) { + $this->output->progressFinish(); + $this->outputLine(); + } + + if ($result !== null) { + $this->outputLine('%s', [$result->getMessage()]); + $this->quit(1); + } elseif (!$quiet) { + $this->outputLine('Done.'); + } + } +} diff --git a/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionReplayProcessor.php b/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionReplayProcessor.php index b97a37eb27d..bb44d1f9643 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionReplayProcessor.php +++ b/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionReplayProcessor.php @@ -19,6 +19,6 @@ public function __construct( public function run(ProcessingContext $context): void { - $this->contentRepositoryMaintainer->replayAllProjections(); + $this->contentRepositoryMaintainer->replayAllSubscriptions(); } } From 4c65d81917b706f3448bf8119eebb516000bc28b Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 27 Nov 2024 14:13:25 +0100 Subject: [PATCH 17/24] TASK: Status also shows new subscriptions even if they are not persistent yet --- .../AbstractSubscriptionEngineTestCase.php | 4 +-- .../SubscriptionNewStatusTest.php | 26 ++++++++++++------- .../Service/ContentRepositoryMaintainer.php | 15 ++++++++--- .../Engine/SubscriptionEngine.php | 21 +++++++++++++-- 4 files changed, 49 insertions(+), 17 deletions(-) diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/AbstractSubscriptionEngineTestCase.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/AbstractSubscriptionEngineTestCase.php index bd9a8ce4de2..9aeb1c43b60 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/AbstractSubscriptionEngineTestCase.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/AbstractSubscriptionEngineTestCase.php @@ -19,7 +19,7 @@ use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\Subscription\DetachedSubscriptionStatus; use Neos\ContentRepository\Core\Subscription\Engine\SubscriptionEngine; -use Neos\ContentRepository\Core\Subscription\Store\SubscriptionCriteria; +use Neos\ContentRepository\Core\Subscription\Engine\SubscriptionEngineCriteria; use Neos\ContentRepository\Core\Subscription\ProjectionSubscriptionStatus; use Neos\ContentRepository\Core\Subscription\SubscriptionId; use Neos\ContentRepository\Core\Subscription\SubscriptionStatus; @@ -137,7 +137,7 @@ final protected function resetDatabase(Connection $connection, ContentRepository final protected function subscriptionStatus(string $subscriptionId): ProjectionSubscriptionStatus|DetachedSubscriptionStatus|null { - return $this->subscriptionEngine->subscriptionStatus(SubscriptionCriteria::create(ids: [SubscriptionId::fromString($subscriptionId)]))->first(); + return $this->subscriptionEngine->subscriptionStatus(SubscriptionEngineCriteria::create(ids: [SubscriptionId::fromString($subscriptionId)]))->first(); } final protected function commitExampleContentStreamEvent(): void diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionNewStatusTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionNewStatusTest.php index ed2d57fe2b1..86c8ae2e0d5 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionNewStatusTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionNewStatusTest.php @@ -43,7 +43,8 @@ public function newProjectionIsFoundWhenConfigurationIsAdded() $newFakeProjection = $this->getMockBuilder(ProjectionInterface::class)->disableAutoReturnValueGeneration()->getMock(); $newFakeProjection->method('getState')->willReturn(new class implements ProjectionStateInterface {}); - $newFakeProjection->expects(self::exactly(3))->method('status')->willReturnOnConsecutiveCalls( + $newFakeProjection->expects(self::exactly(4))->method('status')->willReturnOnConsecutiveCalls( + ProjectionStatus::setupRequired('Set me up'), ProjectionStatus::setupRequired('Set me up'), ProjectionStatus::ok(), ProjectionStatus::ok(), @@ -65,21 +66,26 @@ public function newProjectionIsFoundWhenConfigurationIsAdded() $this->getObject(ContentRepositoryRegistry::class)->resetFactoryInstance($this->contentRepository->id); $this->setupContentRepositoryDependencies($this->contentRepository->id); - // todo status doesnt find this projection yet? - self::assertNull($this->subscriptionStatus('Vendor.Package:NewFakeProjection')); + $expectedNewState = ProjectionSubscriptionStatus::create( + subscriptionId: SubscriptionId::fromString('Vendor.Package:NewFakeProjection'), + subscriptionStatus: SubscriptionStatus::NEW, + subscriptionPosition: SequenceNumber::none(), + subscriptionError: null, + setupStatus: ProjectionStatus::setupRequired('Set me up') + ); + + // status predicts the NEW state already (without creating it in the db) + self::assertEquals( + $expectedNewState, + $this->subscriptionStatus('Vendor.Package:NewFakeProjection') + ); // do something that finds new subscriptions, trigger a setup on a specific projection: $result = $this->subscriptionEngine->setup(SubscriptionEngineCriteria::create([SubscriptionId::fromString('contentGraph')])); self::assertNull($result->errors); self::assertEquals( - ProjectionSubscriptionStatus::create( - subscriptionId: SubscriptionId::fromString('Vendor.Package:NewFakeProjection'), - subscriptionStatus: SubscriptionStatus::NEW, - subscriptionPosition: SequenceNumber::none(), - subscriptionError: null, - setupStatus: ProjectionStatus::setupRequired('Set me up') - ), + $expectedNewState, $this->subscriptionStatus('Vendor.Package:NewFakeProjection') ); diff --git a/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php b/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php index 2bc514668a1..5a36a287eac 100644 --- a/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php +++ b/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php @@ -12,8 +12,8 @@ use Neos\ContentRepository\Core\Subscription\Engine\Errors; use Neos\ContentRepository\Core\Subscription\Engine\SubscriptionEngine; use Neos\ContentRepository\Core\Subscription\Engine\SubscriptionEngineCriteria; -use Neos\ContentRepository\Core\Subscription\Store\SubscriptionCriteria; use Neos\ContentRepository\Core\Subscription\SubscriptionId; +use Neos\ContentRepository\Core\Subscription\SubscriptionStatus; use Neos\ContentRepository\Core\Subscription\SubscriptionStatusCollection; use Neos\Error\Messages\Error; use Neos\EventStore\EventStoreInterface; @@ -115,6 +115,13 @@ public function status(): ContentRepositoryStatus public function replaySubscription(SubscriptionId $subscriptionId, \Closure|null $progressCallback = null): Error|null { + $subscriptionStatus = $this->subscriptionEngine->subscriptionStatus(SubscriptionEngineCriteria::create([$subscriptionId]))->first(); + if ($subscriptionStatus === null) { + return new Error(sprintf('Subscription "%s" is not registered.', $subscriptionId->value)); + } + if ($subscriptionStatus->subscriptionStatus === SubscriptionStatus::NEW) { + return new Error(sprintf('Subscription "%s" is not setup and cannot be replayed.', $subscriptionId->value)); + } $resetResult = $this->subscriptionEngine->reset(SubscriptionEngineCriteria::create([$subscriptionId])); if ($resetResult->errors !== null) { return self::createErrorForReason('reset', $resetResult->errors); @@ -148,11 +155,13 @@ public function replayAllSubscriptions(\Closure|null $progressCallback = null): */ public function reactivateSubscription(SubscriptionId $subscriptionId, \Closure|null $progressCallback = null): Error|null { - $subscriptionStatus = $this->subscriptionEngine->subscriptionStatus(SubscriptionCriteria::create([$subscriptionId]))->first(); - + $subscriptionStatus = $this->subscriptionEngine->subscriptionStatus(SubscriptionEngineCriteria::create([$subscriptionId]))->first(); if ($subscriptionStatus === null) { return new Error(sprintf('Subscription "%s" is not registered.', $subscriptionId->value)); } + if ($subscriptionStatus->subscriptionStatus === SubscriptionStatus::NEW) { + return new Error(sprintf('Subscription "%s" is not setup and cannot be reactivated.', $subscriptionId->value)); + } // todo implement https://github.com/patchlevel/event-sourcing/blob/b8591c56b21b049f46bead8e7ab424fd2afe9917/src/Subscription/Engine/DefaultSubscriptionEngine.php#L624 return null; diff --git a/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionEngine.php b/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionEngine.php index d6a0ce79379..e9616c204bc 100644 --- a/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionEngine.php +++ b/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionEngine.php @@ -111,11 +111,11 @@ public function reset(SubscriptionEngineCriteria|null $criteria = null): Result return $errors === [] ? Result::success() : Result::failed(Errors::fromArray($errors)); } - public function subscriptionStatus(SubscriptionCriteria|null $criteria = null): SubscriptionStatusCollection + public function subscriptionStatus(SubscriptionEngineCriteria|null $criteria = null): SubscriptionStatusCollection { $statuses = []; try { - $subscriptions = $this->subscriptionStore->findByCriteria($criteria ?? SubscriptionCriteria::noConstraints()); + $subscriptions = $this->subscriptionStore->findByCriteria(SubscriptionCriteria::create(ids: $criteria?->ids)); } catch (TableNotFoundException) { // the schema is not setup - thus there are no subscribers return SubscriptionStatusCollection::createEmpty(); @@ -138,6 +138,23 @@ public function subscriptionStatus(SubscriptionCriteria|null $criteria = null): setupStatus: $subscriber->projection->status(), ); } + foreach ($this->subscribers as $subscriber) { + if ($subscriptions->contain($subscriber->id)) { + continue; + } + if ($criteria?->ids?->contain($subscriber->id) === false) { + // this might be a NEW subscription but we dont return it as status is filtered. + continue; + } + // this NEW state is not persisted yet + $statuses[] = ProjectionSubscriptionStatus::create( + subscriptionId: $subscriber->id, + subscriptionStatus: SubscriptionStatus::NEW, + subscriptionPosition: SequenceNumber::none(), + subscriptionError: null, + setupStatus: $subscriber->projection->status(), + ); + } return SubscriptionStatusCollection::fromArray($statuses); } From 8c9c0e8cfc241e91b84eafb4e54b3ed115d41923 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 27 Nov 2024 14:17:28 +0100 Subject: [PATCH 18/24] TASK: Refine todos skipBooting was removed via 0ac8751e843a5a88103bbac5a5615238374a083d --- .../Classes/Service/ContentRepositoryMaintainer.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php b/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php index 5a36a287eac..4adb2301ca9 100644 --- a/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php +++ b/Neos.ContentRepository.Core/Classes/Service/ContentRepositoryMaintainer.php @@ -88,7 +88,8 @@ public function setUp(): Error|null return self::createErrorForReason('setup', $setupResult->errors); } if ($eventStoreIsEmpty) { - // todo reintroduce skipBooting flag, and also notify if the flag is not set, e.g. because there are events + // note: possibly introduce $skipBooting flag instead + // see https://github.com/patchlevel/event-sourcing/blob/b8591c56b21b049f46bead8e7ab424fd2afe9917/src/Subscription/Engine/DefaultSubscriptionEngine.php#L42 $bootResult = $this->subscriptionEngine->boot(); if ($bootResult->errors !== null) { return self::createErrorForReason('initial catchup', $bootResult->errors); @@ -184,7 +185,7 @@ public function prune(): Error|null if ($resetResult->errors !== null) { return self::createErrorForReason('reset', $resetResult->errors); } - // todo reintroduce skipBooting flag to reset + // note: possibly introduce $skipBooting flag like for setup $bootResult = $this->subscriptionEngine->boot(); if ($bootResult->errors !== null) { return self::createErrorForReason('booting', $bootResult->errors); @@ -194,7 +195,6 @@ public function prune(): Error|null private static function createErrorForReason(string $method, Errors $errors): Error { - // todo log throwable via flow???, but we are here in the CORE ... $message = []; $message[] = sprintf('%s produced the following error%s', $method, $errors->count() === 1 ? '' : 's'); foreach ($errors as $error) { From 4424483e3fe3e3b79a7cc7cb80d73a5445631a41 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 27 Nov 2024 14:27:35 +0100 Subject: [PATCH 19/24] TASK: Declare SubscriptionEngine and friends as internal only things in `SubscriptionStatusCollection` are API --- .../Classes/Subscription/DetachedSubscriptionStatus.php | 4 ++-- .../Classes/Subscription/Engine/Error.php | 2 +- .../Classes/Subscription/Engine/Errors.php | 2 +- .../Classes/Subscription/Engine/ProcessedResult.php | 2 +- .../Classes/Subscription/Engine/Result.php | 2 +- .../Classes/Subscription/Engine/SubscriptionEngine.php | 2 +- .../Subscription/Engine/SubscriptionEngineCriteria.php | 2 +- .../Classes/Subscription/Engine/SubscriptionManager.php | 4 +++- .../Classes/Subscription/Exception/CatchUpFailed.php | 1 + .../SubscriptionEngineAlreadyProcessingException.php | 3 --- .../Classes/Subscription/ProjectionSubscriptionStatus.php | 4 ++-- .../Classes/Subscription/Store/SubscriptionCriteria.php | 2 +- .../Classes/Subscription/Store/SubscriptionStoreInterface.php | 2 +- .../Classes/Subscription/Subscriber/ProjectionSubscriber.php | 2 +- .../Classes/Subscription/Subscriber/Subscribers.php | 2 +- .../Classes/Subscription/Subscription.php | 2 +- .../Classes/Subscription/SubscriptionError.php | 2 +- .../Classes/Subscription/SubscriptionId.php | 2 +- .../Classes/Subscription/SubscriptionIds.php | 2 +- .../Classes/Subscription/SubscriptionStatus.php | 2 +- .../Classes/Subscription/SubscriptionStatusFilter.php | 2 +- .../Classes/Subscription/Subscriptions.php | 2 +- 22 files changed, 25 insertions(+), 25 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/Subscription/DetachedSubscriptionStatus.php b/Neos.ContentRepository.Core/Classes/Subscription/DetachedSubscriptionStatus.php index fd61ddf9f7a..7ab2ad36ac0 100644 --- a/Neos.ContentRepository.Core/Classes/Subscription/DetachedSubscriptionStatus.php +++ b/Neos.ContentRepository.Core/Classes/Subscription/DetachedSubscriptionStatus.php @@ -10,7 +10,7 @@ * Note that the SubscriptionStatus might not be actually Detached yet, as for the marking of detached, * setup or catchup has to be run. * - * @api + * @api part of the subscription status */ final readonly class DetachedSubscriptionStatus { @@ -22,7 +22,7 @@ private function __construct( } /** - * @internal + * @internal implementation detail of the catchup */ public static function create( SubscriptionId $subscriptionId, diff --git a/Neos.ContentRepository.Core/Classes/Subscription/Engine/Error.php b/Neos.ContentRepository.Core/Classes/Subscription/Engine/Error.php index 699dac846f3..546f82c5d3c 100644 --- a/Neos.ContentRepository.Core/Classes/Subscription/Engine/Error.php +++ b/Neos.ContentRepository.Core/Classes/Subscription/Engine/Error.php @@ -7,7 +7,7 @@ use Neos\ContentRepository\Core\Subscription\SubscriptionId; /** - * @internal + * @internal implementation detail of the catchup */ final class Error { diff --git a/Neos.ContentRepository.Core/Classes/Subscription/Engine/Errors.php b/Neos.ContentRepository.Core/Classes/Subscription/Engine/Errors.php index bf7dd3668b9..11b9fffc3cf 100644 --- a/Neos.ContentRepository.Core/Classes/Subscription/Engine/Errors.php +++ b/Neos.ContentRepository.Core/Classes/Subscription/Engine/Errors.php @@ -6,7 +6,7 @@ /** * @implements \IteratorAggregate - * @api + * @internal implementation detail of the catchup */ final readonly class Errors implements \IteratorAggregate, \Countable { diff --git a/Neos.ContentRepository.Core/Classes/Subscription/Engine/ProcessedResult.php b/Neos.ContentRepository.Core/Classes/Subscription/Engine/ProcessedResult.php index 61f1a07e840..e90e6975fa2 100644 --- a/Neos.ContentRepository.Core/Classes/Subscription/Engine/ProcessedResult.php +++ b/Neos.ContentRepository.Core/Classes/Subscription/Engine/ProcessedResult.php @@ -5,7 +5,7 @@ namespace Neos\ContentRepository\Core\Subscription\Engine; /** - * @api + * @internal implementation detail of the catchup */ final readonly class ProcessedResult { diff --git a/Neos.ContentRepository.Core/Classes/Subscription/Engine/Result.php b/Neos.ContentRepository.Core/Classes/Subscription/Engine/Result.php index dc8da2ef08b..dab71033f97 100644 --- a/Neos.ContentRepository.Core/Classes/Subscription/Engine/Result.php +++ b/Neos.ContentRepository.Core/Classes/Subscription/Engine/Result.php @@ -5,7 +5,7 @@ namespace Neos\ContentRepository\Core\Subscription\Engine; /** - * @api + * @internal implementation detail of the catchup */ final readonly class Result { diff --git a/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionEngine.php b/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionEngine.php index e9616c204bc..25cf464e0bc 100644 --- a/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionEngine.php +++ b/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionEngine.php @@ -33,7 +33,7 @@ * All functionality is low level and well encapsulated and abstracted by the {@see ContentRepositoryMaintainer} * It presents the only API way to interact with catchup and offers more maintenance tasks. * - * @internal implementation detail of {@see ContentRepository::handle()} and the {@see ContentRepositoryMaintainer} + * @internal implementation detail of the catchup. See {@see ContentRepository::handle()} and {@see ContentRepositoryMaintainer} */ final class SubscriptionEngine { diff --git a/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionEngineCriteria.php b/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionEngineCriteria.php index 3cb3946aa17..068f5a15a98 100644 --- a/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionEngineCriteria.php +++ b/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionEngineCriteria.php @@ -8,7 +8,7 @@ use Neos\ContentRepository\Core\Subscription\SubscriptionIds; /** - * @internal + * @internal implementation detail of the catchup */ final class SubscriptionEngineCriteria { diff --git a/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionManager.php b/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionManager.php index 4fd33df2388..2daa7e9db3f 100644 --- a/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionManager.php +++ b/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionManager.php @@ -9,7 +9,9 @@ use Neos\ContentRepository\Core\Subscription\Subscription; use Neos\ContentRepository\Core\Subscription\Subscriptions; -/** @internal */ +/** + * @internal implementation detail of the catchup + */ final class SubscriptionManager { /** @var \SplObjectStorage */ diff --git a/Neos.ContentRepository.Core/Classes/Subscription/Exception/CatchUpFailed.php b/Neos.ContentRepository.Core/Classes/Subscription/Exception/CatchUpFailed.php index c18aaf9c3ba..2cee008bda8 100644 --- a/Neos.ContentRepository.Core/Classes/Subscription/Exception/CatchUpFailed.php +++ b/Neos.ContentRepository.Core/Classes/Subscription/Exception/CatchUpFailed.php @@ -6,6 +6,7 @@ /** * Only thrown if there is no way to recover the started catchup. The transaction will be rolled back. + * * @api */ final class CatchUpFailed extends \RuntimeException diff --git a/Neos.ContentRepository.Core/Classes/Subscription/Exception/SubscriptionEngineAlreadyProcessingException.php b/Neos.ContentRepository.Core/Classes/Subscription/Exception/SubscriptionEngineAlreadyProcessingException.php index 9631724adfb..51ab9bcbf2f 100644 --- a/Neos.ContentRepository.Core/Classes/Subscription/Exception/SubscriptionEngineAlreadyProcessingException.php +++ b/Neos.ContentRepository.Core/Classes/Subscription/Exception/SubscriptionEngineAlreadyProcessingException.php @@ -9,9 +9,6 @@ */ final class SubscriptionEngineAlreadyProcessingException extends \RuntimeException { - /** - * @internal - */ public function __construct() { parent::__construct('Subscription engine is already processing'); diff --git a/Neos.ContentRepository.Core/Classes/Subscription/ProjectionSubscriptionStatus.php b/Neos.ContentRepository.Core/Classes/Subscription/ProjectionSubscriptionStatus.php index 0924c717b73..3e443da6171 100644 --- a/Neos.ContentRepository.Core/Classes/Subscription/ProjectionSubscriptionStatus.php +++ b/Neos.ContentRepository.Core/Classes/Subscription/ProjectionSubscriptionStatus.php @@ -8,7 +8,7 @@ use Neos\EventStore\Model\Event\SequenceNumber; /** - * @api + * @api part of the subscription status */ final readonly class ProjectionSubscriptionStatus { @@ -22,7 +22,7 @@ private function __construct( } /** - * @internal + * @internal implementation detail of the catchup */ public static function create( SubscriptionId $subscriptionId, diff --git a/Neos.ContentRepository.Core/Classes/Subscription/Store/SubscriptionCriteria.php b/Neos.ContentRepository.Core/Classes/Subscription/Store/SubscriptionCriteria.php index 8eca4a8ed79..5bb7c30b2ea 100644 --- a/Neos.ContentRepository.Core/Classes/Subscription/Store/SubscriptionCriteria.php +++ b/Neos.ContentRepository.Core/Classes/Subscription/Store/SubscriptionCriteria.php @@ -11,7 +11,7 @@ use Neos\ContentRepository\Core\Subscription\SubscriptionStatusFilter; /** - * @api + * @internal implementation detail of the catchup */ final readonly class SubscriptionCriteria { diff --git a/Neos.ContentRepository.Core/Classes/Subscription/Store/SubscriptionStoreInterface.php b/Neos.ContentRepository.Core/Classes/Subscription/Store/SubscriptionStoreInterface.php index 4b6a827f8e6..0127769fde8 100644 --- a/Neos.ContentRepository.Core/Classes/Subscription/Store/SubscriptionStoreInterface.php +++ b/Neos.ContentRepository.Core/Classes/Subscription/Store/SubscriptionStoreInterface.php @@ -8,7 +8,7 @@ use Neos\ContentRepository\Core\Subscription\Subscriptions; /** - * @api + * @internal only API for custom content repository integrations */ interface SubscriptionStoreInterface { diff --git a/Neos.ContentRepository.Core/Classes/Subscription/Subscriber/ProjectionSubscriber.php b/Neos.ContentRepository.Core/Classes/Subscription/Subscriber/ProjectionSubscriber.php index 117b52265bc..7e15a8fdcba 100644 --- a/Neos.ContentRepository.Core/Classes/Subscription/Subscriber/ProjectionSubscriber.php +++ b/Neos.ContentRepository.Core/Classes/Subscription/Subscriber/ProjectionSubscriber.php @@ -13,7 +13,7 @@ use Neos\EventStore\Model\EventEnvelope; /** - * @internal + * @internal implementation detail of the catchup */ final class ProjectionSubscriber { diff --git a/Neos.ContentRepository.Core/Classes/Subscription/Subscriber/Subscribers.php b/Neos.ContentRepository.Core/Classes/Subscription/Subscriber/Subscribers.php index 17288fc927a..eba25e39a1a 100644 --- a/Neos.ContentRepository.Core/Classes/Subscription/Subscriber/Subscribers.php +++ b/Neos.ContentRepository.Core/Classes/Subscription/Subscriber/Subscribers.php @@ -14,7 +14,7 @@ * Like a possible "ListeningSubscriber" to only listen to events without the capabilities of a full-blown projection. * * @implements \IteratorAggregate - * @internal + * @internal implementation detail of the catchup */ final class Subscribers implements \IteratorAggregate, \Countable, \JsonSerializable { diff --git a/Neos.ContentRepository.Core/Classes/Subscription/Subscription.php b/Neos.ContentRepository.Core/Classes/Subscription/Subscription.php index 7a6c22ab82f..902f7210db7 100644 --- a/Neos.ContentRepository.Core/Classes/Subscription/Subscription.php +++ b/Neos.ContentRepository.Core/Classes/Subscription/Subscription.php @@ -11,7 +11,7 @@ /** * Note: This class is mutable by design! * - * @internal + * @internal implementation detail of the catchup */ final class Subscription { diff --git a/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionError.php b/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionError.php index 5c53135dc66..1adc7e2cfe1 100644 --- a/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionError.php +++ b/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionError.php @@ -5,7 +5,7 @@ namespace Neos\ContentRepository\Core\Subscription; /** - * @api + * @api part of the subscription status */ final class SubscriptionError { diff --git a/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionId.php b/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionId.php index 777814c275e..668c85f0b78 100644 --- a/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionId.php +++ b/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionId.php @@ -5,7 +5,7 @@ namespace Neos\ContentRepository\Core\Subscription; /** - * @api + * @api identifier for a registered subscription */ final class SubscriptionId { diff --git a/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionIds.php b/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionIds.php index 7b672d887ec..9e81ae56ef2 100644 --- a/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionIds.php +++ b/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionIds.php @@ -6,7 +6,7 @@ /** * @implements \IteratorAggregate - * @api + * @internal implementation detail of the catchup */ final class SubscriptionIds implements \IteratorAggregate, \Countable, \JsonSerializable { diff --git a/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionStatus.php b/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionStatus.php index f1b29c7e572..487f742fbab 100644 --- a/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionStatus.php +++ b/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionStatus.php @@ -5,7 +5,7 @@ namespace Neos\ContentRepository\Core\Subscription; /** - * @api + * @api part of the subscription status */ enum SubscriptionStatus : string { diff --git a/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionStatusFilter.php b/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionStatusFilter.php index adc3fa4e51b..e531c180329 100644 --- a/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionStatusFilter.php +++ b/Neos.ContentRepository.Core/Classes/Subscription/SubscriptionStatusFilter.php @@ -6,7 +6,7 @@ /** * @implements \IteratorAggregate - * @api + * @internal implementation detail of the catchup */ final class SubscriptionStatusFilter implements \IteratorAggregate { diff --git a/Neos.ContentRepository.Core/Classes/Subscription/Subscriptions.php b/Neos.ContentRepository.Core/Classes/Subscription/Subscriptions.php index 03a09abe0f1..3bba94d0018 100644 --- a/Neos.ContentRepository.Core/Classes/Subscription/Subscriptions.php +++ b/Neos.ContentRepository.Core/Classes/Subscription/Subscriptions.php @@ -8,7 +8,7 @@ /** * @implements \IteratorAggregate - * @api + * @internal implementation detail of the catchup */ final class Subscriptions implements \IteratorAggregate, \Countable, \JsonSerializable { From baa5e4aa280935c806d32bc02ebf3beb38544f29 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 27 Nov 2024 14:28:24 +0100 Subject: [PATCH 20/24] TASK: Add error code to `SubscriptionEngineAlreadyProcessingException` --- .../Classes/Subscription/Engine/SubscriptionEngine.php | 2 +- .../SubscriptionEngineAlreadyProcessingException.php | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionEngine.php b/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionEngine.php index 25cf464e0bc..7f8e1f43aa1 100644 --- a/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionEngine.php +++ b/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionEngine.php @@ -378,7 +378,7 @@ function (Subscriptions $subscriptions) use ($subscriptionStatus, $progressClosu private function processExclusively(\Closure $closure): mixed { if ($this->processing) { - throw new SubscriptionEngineAlreadyProcessingException(); + throw new SubscriptionEngineAlreadyProcessingException('Subscription engine is already processing', 1732714075); } $this->processing = true; try { diff --git a/Neos.ContentRepository.Core/Classes/Subscription/Exception/SubscriptionEngineAlreadyProcessingException.php b/Neos.ContentRepository.Core/Classes/Subscription/Exception/SubscriptionEngineAlreadyProcessingException.php index 51ab9bcbf2f..4747a062f31 100644 --- a/Neos.ContentRepository.Core/Classes/Subscription/Exception/SubscriptionEngineAlreadyProcessingException.php +++ b/Neos.ContentRepository.Core/Classes/Subscription/Exception/SubscriptionEngineAlreadyProcessingException.php @@ -9,8 +9,4 @@ */ final class SubscriptionEngineAlreadyProcessingException extends \RuntimeException { - public function __construct() - { - parent::__construct('Subscription engine is already processing'); - } } From 66e54bceba557256880d7bfde1b6539ab32e1c37 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 27 Nov 2024 14:37:41 +0100 Subject: [PATCH 21/24] TASK: Allow cr registry to implement internal subscription store because its framework --- phpstan-baseline.neon | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index aab9e3e26d4..2477370866c 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,5 +1,20 @@ parameters: ignoreErrors: + - + message: "#^The internal method \"Neos\\\\ContentRepository\\\\Core\\\\Subscription\\\\SubscriptionIds\\:\\:toStringArray\" is called\\.$#" + count: 1 + path: Neos.ContentRepositoryRegistry/Classes/Factory/SubscriptionStore/DoctrineSubscriptionStore.php + + - + message: "#^The internal method \"Neos\\\\ContentRepository\\\\Core\\\\Subscription\\\\SubscriptionStatusFilter\\:\\:isEmpty\" is called\\.$#" + count: 1 + path: Neos.ContentRepositoryRegistry/Classes/Factory/SubscriptionStore/DoctrineSubscriptionStore.php + + - + message: "#^The internal method \"Neos\\\\ContentRepository\\\\Core\\\\Subscription\\\\SubscriptionStatusFilter\\:\\:toStringArray\" is called\\.$#" + count: 1 + path: Neos.ContentRepositoryRegistry/Classes/Factory/SubscriptionStore/DoctrineSubscriptionStore.php + - message: "#^Method Neos\\\\Neos\\\\Controller\\\\Backend\\\\MenuHelper\\:\\:buildModuleList\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 From 51d39e59c5ebc6bbcd4604fad1b06590b698192b Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 27 Nov 2024 14:59:20 +0100 Subject: [PATCH 22/24] TASK: Rename to `SubscriptionReplayProcessor` --- ...ionReplayProcessor.php => SubscriptionReplayProcessor.php} | 2 +- Neos.Neos/Classes/Domain/Service/SiteImportService.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename Neos.ContentRepositoryRegistry/Classes/Processors/{ProjectionReplayProcessor.php => SubscriptionReplayProcessor.php} (87%) diff --git a/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionReplayProcessor.php b/Neos.ContentRepositoryRegistry/Classes/Processors/SubscriptionReplayProcessor.php similarity index 87% rename from Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionReplayProcessor.php rename to Neos.ContentRepositoryRegistry/Classes/Processors/SubscriptionReplayProcessor.php index bb44d1f9643..83ce99eaca0 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Processors/ProjectionReplayProcessor.php +++ b/Neos.ContentRepositoryRegistry/Classes/Processors/SubscriptionReplayProcessor.php @@ -10,7 +10,7 @@ /** * @internal */ -final readonly class ProjectionReplayProcessor implements ProcessorInterface +final readonly class SubscriptionReplayProcessor implements ProcessorInterface { public function __construct( private ContentRepositoryMaintainer $contentRepositoryMaintainer, diff --git a/Neos.Neos/Classes/Domain/Service/SiteImportService.php b/Neos.Neos/Classes/Domain/Service/SiteImportService.php index 2c8d5aca54a..ebe76134d19 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteImportService.php +++ b/Neos.Neos/Classes/Domain/Service/SiteImportService.php @@ -30,7 +30,7 @@ use Neos\ContentRepository\Export\Processors\AssetRepositoryImportProcessor; use Neos\ContentRepository\Export\Severity; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; -use Neos\ContentRepositoryRegistry\Processors\ProjectionReplayProcessor; +use Neos\ContentRepositoryRegistry\Processors\SubscriptionReplayProcessor; use Neos\EventStore\Model\EventStore\StatusType; use Neos\Flow\Annotations as Flow; use Neos\Flow\Persistence\Doctrine\Service as DoctrineService; @@ -85,7 +85,7 @@ public function importFromPath(ContentRepositoryId $contentRepositoryId, string 'Import assets' => new AssetRepositoryImportProcessor($this->assetRepository, $this->resourceRepository, $this->resourceManager, $this->persistenceManager), // WARNING! We do a replay here even though it will redo the live workspace creation. But otherwise the catchup hooks cannot determine that they need to be skipped as it seems like a regular catchup // In case we allow to import events into other root workspaces, or don't expect live to be empty (see Import events), this would need to be adjusted, as otherwise existing data will be replayed - 'Replay all projections' => new ProjectionReplayProcessor($contentRepositoryMaintainer), + 'Replay all subscriptions' => new SubscriptionReplayProcessor($contentRepositoryMaintainer), ]); foreach ($processors as $processorLabel => $processor) { From b2c1a29423004c6170e9aaf773c206d7341af618 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 27 Nov 2024 15:04:56 +0100 Subject: [PATCH 23/24] TASK: Improve legacy projectionReplayCommand stub --- .../Classes/Command/CrCommandController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php index 30af699ad72..c9ab42e077b 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php +++ b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php @@ -184,7 +184,6 @@ public function statusCommand(string $contentRepository = 'default', bool $verbo */ public function projectionReplayCommand(string $projection, string $contentRepository = 'default', bool $force = false, bool $quiet = false): void { - $this->outputLine('Please use ./flow subscription:replay instead!'); $subscriptionId = match($projection) { 'doctrineDbalContentGraph', 'Neos\ContentGraph\DoctrineDbalAdapter\DoctrineDbalContentGraphProjection' => 'contentGraph', @@ -193,9 +192,10 @@ public function projectionReplayCommand(string $projection, string $contentRepos default => null }; if ($subscriptionId === null) { - $this->outputLine('Invalid --projection specified. Could not map legacy argument.'); + $this->outputLine('Invalid --projection specified. Please use ./flow subscription:replay [contentGraph|Neos.Neos:DocumentUriPathProjection|...] directly.'); $this->quit(1); } + $this->outputLine('Please use ./flow subscription:replay %s instead!', [$subscriptionId]); $this->forward( 'replay', SubscriptionCommandController::class, From d84c2a4d4d2eaa01ccc15db06fcce7e3437217cf Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 27 Nov 2024 19:13:25 +0100 Subject: [PATCH 24/24] TASK: Add test for Subscription & Cr Commands (and thus CRMaintainer) --- .../AbstractSubscriptionEngineTestCase.php | 3 + .../SubscriptionGetStatusTest.php | 12 +- .../ContentRepositoryStatus.php | 2 +- .../Classes/Command/CrCommandController.php | 6 +- ...sitoryMaintenanceCommandControllerTest.php | 142 ++++++++++++++++++ 5 files changed, 161 insertions(+), 4 deletions(-) create mode 100644 Neos.ContentRepositoryRegistry/Tests/Functional/ContentRepositoryMaintenanceCommandControllerTest.php diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/AbstractSubscriptionEngineTestCase.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/AbstractSubscriptionEngineTestCase.php index 9aeb1c43b60..ca9c4de33eb 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/AbstractSubscriptionEngineTestCase.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/AbstractSubscriptionEngineTestCase.php @@ -36,6 +36,9 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +/** + * @internal, only for tests of the Neos.* namespace + */ abstract class AbstractSubscriptionEngineTestCase extends TestCase // we don't use Flows functional test case as it would reset the database afterwards { protected ContentRepository $contentRepository; diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionGetStatusTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionGetStatusTest.php index c8be53f9d98..0a624d4c08b 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionGetStatusTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionGetStatusTest.php @@ -6,12 +6,15 @@ use Doctrine\DBAL\Connection; use Neos\ContentRepository\Core\Projection\ProjectionStatus; +use Neos\ContentRepository\Core\Service\ContentRepositoryMaintainerFactory; use Neos\ContentRepository\Core\Subscription\Engine\SubscriptionEngineCriteria; use Neos\ContentRepository\Core\Subscription\ProjectionSubscriptionStatus; use Neos\ContentRepository\Core\Subscription\SubscriptionStatusCollection; use Neos\ContentRepository\Core\Subscription\SubscriptionId; use Neos\ContentRepository\Core\Subscription\SubscriptionStatus; +use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\EventStore\Model\Event\SequenceNumber; +use Neos\EventStore\Model\EventStore\StatusType; final class SubscriptionGetStatusTest extends AbstractSubscriptionEngineTestCase { @@ -25,8 +28,13 @@ public function statusOnEmptyDatabase() keepSchema: false ); - $actualStatuses = $this->subscriptionEngine->subscriptionStatus(); - self::assertTrue($actualStatuses->isEmpty()); + $crMaintainer = $this->getObject(ContentRepositoryRegistry::class)->buildService($this->contentRepository->id, new ContentRepositoryMaintainerFactory()); + + $status = $crMaintainer->status(); + + self::assertEquals(StatusType::SETUP_REQUIRED, $status->eventStoreStatus->type); + self::assertNull($status->eventStorePosition); + self::assertTrue($status->subscriptionStatus->isEmpty()); self::assertNull( $this->subscriptionStatus('contentGraph') diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/ContentRepository/ContentRepositoryStatus.php b/Neos.ContentRepository.Core/Classes/SharedModel/ContentRepository/ContentRepositoryStatus.php index 3c2cd61e244..db546bf9f9f 100644 --- a/Neos.ContentRepository.Core/Classes/SharedModel/ContentRepository/ContentRepositoryStatus.php +++ b/Neos.ContentRepository.Core/Classes/SharedModel/ContentRepository/ContentRepositoryStatus.php @@ -28,7 +28,7 @@ { /** * @param EventStoreStatus $eventStoreStatus - * @param SequenceNumber|null $eventStorePosition The position of the event store. NULL if an error occurred, see error state of $eventStoreStatus + * @param SequenceNumber|null $eventStorePosition The position of the event store. NULL if an error occurred in which case a setup must likely be done, see $eventStoreStatus * @param SubscriptionStatusCollection $subscriptionStatus */ private function __construct( diff --git a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php index c9ab42e077b..c51957ed96c 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php +++ b/Neos.ContentRepositoryRegistry/Classes/Command/CrCommandController.php @@ -47,10 +47,14 @@ final class CrCommandController extends CommandController * To check if the content repository needs to be setup look into cr:status. * That command will also display information what is about to be migrated. * + * @param bool $quiet If set, no output is generated. This is useful if only the exit code (0 = all OK, 1 = errors or warnings) is of interest * @param string $contentRepository Identifier of the Content Repository to set up */ - public function setupCommand(string $contentRepository = 'default'): void + public function setupCommand(string $contentRepository = 'default', bool $quiet = false): void { + if ($quiet) { + $this->output->getOutput()->setVerbosity(Output::VERBOSITY_QUIET); + } $contentRepositoryId = ContentRepositoryId::fromString($contentRepository); $contentRepositoryMaintainer = $this->contentRepositoryRegistry->buildService($contentRepositoryId, new ContentRepositoryMaintainerFactory()); diff --git a/Neos.ContentRepositoryRegistry/Tests/Functional/ContentRepositoryMaintenanceCommandControllerTest.php b/Neos.ContentRepositoryRegistry/Tests/Functional/ContentRepositoryMaintenanceCommandControllerTest.php new file mode 100644 index 00000000000..cf8de0c0b88 --- /dev/null +++ b/Neos.ContentRepositoryRegistry/Tests/Functional/ContentRepositoryMaintenanceCommandControllerTest.php @@ -0,0 +1,142 @@ +crController = $this->getObject(CrCommandController::class); + $this->subscriptionController = $this->getObject(SubscriptionCommandController::class); + + $this->response = new Response(); + $this->bufferedOutput = new BufferedOutput(); + + ObjectAccess::setProperty($this->crController, 'response', $this->response, true); + ObjectAccess::getProperty($this->crController, 'output', true)->setOutput($this->bufferedOutput); + + ObjectAccess::setProperty($this->subscriptionController, 'response', $this->response, true); + ObjectAccess::getProperty($this->subscriptionController, 'output', true)->setOutput($this->bufferedOutput); + } + + /** @test */ + public function setupOnEmptyEventStore(): void + { + $this->fakeProjection->expects(self::once())->method('setUp'); + $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); + + $this->crController->setupCommand(contentRepository: $this->contentRepository->id->value, quiet: true); + self::assertEmpty($this->bufferedOutput->fetch()); + + // projections are marked active because the event store is empty + $this->expectOkayStatus('contentGraph', SubscriptionStatus::ACTIVE, SequenceNumber::none()); + $this->expectOkayStatus('Vendor.Package:FakeProjection', SubscriptionStatus::ACTIVE, SequenceNumber::none()); + $this->expectOkayStatus('Vendor.Package:SecondFakeProjection', SubscriptionStatus::ACTIVE, SequenceNumber::none()); + + $this->crController->statusCommand(contentRepository: $this->contentRepository->id->value, quiet: true); + self::assertEmpty($this->bufferedOutput->fetch()); + } + + /** @test */ + public function setupOnModifiedEventStore(): void + { + $this->eventStore->setup(); + $this->commitExampleContentStreamEvent(); + + $this->fakeProjection->expects(self::once())->method('setUp'); + $this->fakeProjection->expects(self::once())->method('apply'); + $this->fakeProjection->expects(self::once())->method('resetState'); + $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); + + $this->crController->setupCommand(contentRepository: $this->contentRepository->id->value, quiet: true); + self::assertEmpty($this->bufferedOutput->fetch()); + + $this->expectOkayStatus('contentGraph', SubscriptionStatus::BOOTING, SequenceNumber::none()); + $this->expectOkayStatus('Vendor.Package:FakeProjection', SubscriptionStatus::BOOTING, SequenceNumber::none()); + $this->expectOkayStatus('Vendor.Package:SecondFakeProjection', SubscriptionStatus::BOOTING, SequenceNumber::none()); + + $this->crController->statusCommand(contentRepository: $this->contentRepository->id->value, quiet: true); + self::assertEmpty($this->bufferedOutput->fetch()); + + $this->subscriptionController->replayCommand(subscription: 'contentGraph', contentRepository: $this->contentRepository->id->value, force: true, quiet: true); + self::assertEmpty($this->bufferedOutput->fetch()); + + $this->expectOkayStatus('contentGraph', SubscriptionStatus::ACTIVE, SequenceNumber::fromInteger(1)); + $this->expectOkayStatus('Vendor.Package:FakeProjection', SubscriptionStatus::BOOTING, SequenceNumber::none()); + $this->expectOkayStatus('Vendor.Package:SecondFakeProjection', SubscriptionStatus::BOOTING, SequenceNumber::none()); + + $this->subscriptionController->replayAllCommand(contentRepository: $this->contentRepository->id->value, force: true, quiet: true); + + $this->expectOkayStatus('contentGraph', SubscriptionStatus::ACTIVE, SequenceNumber::fromInteger(1)); + $this->expectOkayStatus('Vendor.Package:FakeProjection', SubscriptionStatus::ACTIVE, SequenceNumber::fromInteger(1)); + $this->expectOkayStatus('Vendor.Package:SecondFakeProjection', SubscriptionStatus::ACTIVE, SequenceNumber::fromInteger(1)); + } + + /** @test */ + public function projectionInError(): void + { + $this->eventStore->setup(); + $this->fakeProjection->expects(self::any())->method('setUp'); + $this->fakeProjection->expects(self::any())->method('apply'); + $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); + + $this->crController->setupCommand(contentRepository: $this->contentRepository->id->value, quiet: true); + self::assertEmpty($this->bufferedOutput->fetch()); + + $this->secondFakeProjection->injectSaboteur(fn () => throw new \RuntimeException('This projection is kaputt.')); + + try { + $this->contentRepository->handle(CreateRootWorkspace::create( + WorkspaceName::forLive(), + ContentStreamId::create() + )); + } catch (\RuntimeException) { + } + + self::assertEquals( + SubscriptionStatus::ERROR, + $this->subscriptionStatus('Vendor.Package:SecondFakeProjection')?->subscriptionStatus + ); + + try { + $this->crController->statusCommand(contentRepository: $this->contentRepository->id->value, quiet: true); + } catch (StopCommandException) { + } + // exit error code because one projection has a failure + self::assertEquals(1, $this->response->getExitCode()); + self::assertEmpty($this->bufferedOutput->fetch()); + + // repair projection + $this->secondFakeProjection->killSaboteur(); + $this->subscriptionController->reactivateCommand(subscription: 'Vendor.Package:SecondFakeProjection', contentRepository: $this->contentRepository->id->value, quiet: true); + + $this->expectOkayStatus('Vendor.Package:SecondFakeProjection', SubscriptionStatus::ACTIVE, SequenceNumber::fromInteger(2)); + } +}