diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionDetachedStatusTest.php b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionDetachedStatusTest.php index 8528517da0..2a45ec60c4 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionDetachedStatusTest.php +++ b/Neos.ContentRepository.BehavioralTests/Tests/Functional/Subscription/SubscriptionDetachedStatusTest.php @@ -23,7 +23,7 @@ public function resetContentRepositoryRegistry(): void } /** @test */ - public function projectionIsDetachedIfConfigurationIsRemoved() + public function projectionIsDetachedOnCatchupActive() { $this->fakeProjection->expects(self::once())->method('setUp'); $this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok()); @@ -49,7 +49,7 @@ public function projectionIsDetachedIfConfigurationIsRemoved() // $this->expectOkayStatus('Vendor.Package:FakeProjection', SubscriptionStatus::ACTIVE, SequenceNumber::none()); $this->fakeProjection->expects(self::never())->method('apply'); - // catchup or anything that finds detached subscribers + // catchup to mark detached subscribers $result = $this->subscriptionEngine->catchUpActive(); // todo result should reflect that there was an detachment? Throw error in CR? self::assertEquals(ProcessedResult::success(1), $result); @@ -88,4 +88,78 @@ public function projectionIsDetachedIfConfigurationIsRemoved() $this->subscriptionStatus('Vendor.Package:FakeProjection') ); } + + /** @test */ + 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->subscriptionService->setupEventStore(); + $this->subscriptionService->subscriptionEngine->setup(); + + $this->commitExampleContentStreamEvent(); + + $result = $this->subscriptionService->subscriptionEngine->boot(); + self::assertEquals(ProcessedResult::success(1), $result); + + $this->expectOkayStatus('Vendor.Package:FakeProjection', SubscriptionStatus::ACTIVE, SequenceNumber::fromInteger(1)); + + // "uninstall" the projection + $originalSettings = $this->getObject(ConfigurationManager::class)->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, 'Neos.ContentRepositoryRegistry'); + $mockSettings = $originalSettings; + unset($mockSettings['contentRepositories'][$this->contentRepository->id->value]['projections']['Vendor.Package:FakeProjection']); + $this->getObject(ContentRepositoryRegistry::class)->injectSettings($mockSettings); + $this->getObject(ContentRepositoryRegistry::class)->resetFactoryInstance($this->contentRepository->id); + $this->setupContentRepositoryDependencies($this->contentRepository->id); + + $this->fakeProjection->expects(self::never())->method('apply'); + // setup to find detached subscribers + $result = $this->subscriptionEngine->setup(); + // todo result should reflect that there was an detachment? + self::assertNull($result->errors); + + $expectedDetachedState = SubscriptionAndProjectionStatus::create( + subscriptionId: SubscriptionId::fromString('Vendor.Package:FakeProjection'), + subscriptionStatus: SubscriptionStatus::DETACHED, + subscriptionPosition: SequenceNumber::fromInteger(1), + subscriptionError: null, + projectionStatus: null // not calculate-able at this point! + ); + self::assertEquals( + $expectedDetachedState, + $this->subscriptionStatus('Vendor.Package:FakeProjection') + ); + + // another setup does not reattach, because there is no subscriber + $result = $this->subscriptionEngine->setup(); + self::assertNull($result->errors); + + self::assertEquals( + $expectedDetachedState, + $this->subscriptionStatus('Vendor.Package:FakeProjection') + ); + + // "reinstall" the projection + $this->getObject(ContentRepositoryRegistry::class)->injectSettings($originalSettings); + $this->getObject(ContentRepositoryRegistry::class)->resetFactoryInstance($this->contentRepository->id); + $this->setupContentRepositoryDependencies($this->contentRepository->id); + + self::assertEquals( + SubscriptionAndProjectionStatus::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! + ), + $this->subscriptionStatus('Vendor.Package:FakeProjection') + ); + + // setup does re-attach as the projection is found again + $this->subscriptionEngine->setup(); + + $this->expectOkayStatus('Vendor.Package:FakeProjection', SubscriptionStatus::BOOTING, SequenceNumber::fromInteger(1)); + } } diff --git a/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionEngine.php b/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionEngine.php index f5399d8b29..4d0b9c9bd5 100644 --- a/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionEngine.php +++ b/Neos.ContentRepository.Core/Classes/Subscription/Engine/SubscriptionEngine.php @@ -54,6 +54,7 @@ public function setup(SubscriptionEngineCriteria|null $criteria = null): Result SubscriptionStatus::NEW, SubscriptionStatus::BOOTING, SubscriptionStatus::ACTIVE, + SubscriptionStatus::DETACHED, ]))); if ($subscriptions->isEmpty()) { $this->logger?->info('Subscription Engine: No subscriptions found.'); // todo not happy? Because there must be at least the content graph?!! @@ -170,6 +171,16 @@ function (Subscriptions $subscriptions) { */ private function setupSubscription(Subscription $subscription): ?Error { + if (!$this->subscribers->contain($subscription->id)) { + // mark detached subscriptions as we cannot set up + $subscription->set( + status: SubscriptionStatus::DETACHED, + ); + $this->subscriptionManager->update($subscription); + $this->logger?->info(sprintf('Subscription Engine: Subscriber for "%s" not found and has been marked as detached.', $subscription->id->value)); + return null; + } + $subscriber = $this->subscribers->get($subscription->id); try { $subscriber->projection->setUp();