From 55a297f60a60ffd5ceac6408cfecea908b5bdd04 Mon Sep 17 00:00:00 2001 From: Dmytro Asieiev <32021063+dmiseev@users.noreply.github.com> Date: Wed, 11 Sep 2024 13:30:31 +0300 Subject: [PATCH] CC-33982: P&S optimisation (#11033) P&S optimisation --- composer.json | 4 +- dependency.json | 2 +- .../Transfer/synchronization.transfer.xml | 10 + .../Business/Search/SynchronizationSearch.php | 15 ++ .../Storage/SynchronizationStorage.php | 15 ++ .../SynchronizationInterface.php | 7 + .../SynchronizationBusinessFactory.php | 33 +++ .../Business/SynchronizationFacade.php | 27 +++ .../SynchronizationFacadeInterface.php | 27 +++ .../InMemoryMessageSynchronizer.php | 187 +++++++++++++++ .../MessageSynchronizerInterface.php | 25 ++ .../DirectSynchronizationConsolePlugin.php | 55 +++++ .../AddSynchronizationMessageToBufferTest.php | 116 ++++++++++ ...hSynchronizationMessagesFromBufferTest.php | 217 ++++++++++++++++++ .../SynchronizationBusinessTester.php | 74 +++++- .../Zed/Synchronization/codeception.yml | 6 +- 16 files changed, 816 insertions(+), 4 deletions(-) create mode 100644 src/Spryker/Zed/Synchronization/Business/Synchronizer/InMemoryMessageSynchronizer.php create mode 100644 src/Spryker/Zed/Synchronization/Business/Synchronizer/MessageSynchronizerInterface.php create mode 100644 src/Spryker/Zed/Synchronization/Communication/Plugin/Console/DirectSynchronizationConsolePlugin.php create mode 100644 tests/SprykerTest/Zed/Synchronization/Business/Facade/AddSynchronizationMessageToBufferTest.php create mode 100644 tests/SprykerTest/Zed/Synchronization/Business/Facade/FlushSynchronizationMessagesFromBufferTest.php diff --git a/composer.json b/composer.json index 194e373..ba7a9b3 100644 --- a/composer.json +++ b/composer.json @@ -7,6 +7,7 @@ "php": ">=8.1", "spryker/elastica": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", "spryker/kernel": "^3.49.0", + "spryker/log": "^3.0.0", "spryker/propel-orm": "^1.16.0", "spryker/queue": "^1.0.0", "spryker/search": "^8.3.0", @@ -14,7 +15,7 @@ "spryker/store": "^1.19.0", "spryker/symfony": "^3.1.0", "spryker/synchronization-extension": "^1.3.0", - "spryker/transfer": "^3.25.0", + "spryker/transfer": "^3.27.0", "spryker/util-encoding": "^2.0.0" }, "require-dev": { @@ -27,6 +28,7 @@ "spryker/cms-page-search": "*", "spryker/cms-storage": "*", "spryker/code-sniffer": "*", + "spryker/container": "*", "spryker/glossary-storage": "*", "spryker/navigation-storage": "*", "spryker/price-product-storage": "*", diff --git a/dependency.json b/dependency.json index 22c114f..1932e90 100644 --- a/dependency.json +++ b/dependency.json @@ -1,5 +1,5 @@ { "include": { - "spryker/transfer": "Provides transfer objects definition with `::get*OrFail()` functionality." + "spryker/transfer": "Provides transfer objects definition with strict types." } } diff --git a/src/Spryker/Shared/Synchronization/Transfer/synchronization.transfer.xml b/src/Spryker/Shared/Synchronization/Transfer/synchronization.transfer.xml index c181864..45318b2 100644 --- a/src/Spryker/Shared/Synchronization/Transfer/synchronization.transfer.xml +++ b/src/Spryker/Shared/Synchronization/Transfer/synchronization.transfer.xml @@ -51,4 +51,14 @@ + + + + + + + + + + diff --git a/src/Spryker/Zed/Synchronization/Business/Search/SynchronizationSearch.php b/src/Spryker/Zed/Synchronization/Business/Search/SynchronizationSearch.php index 2836a6a..62ea08b 100644 --- a/src/Spryker/Zed/Synchronization/Business/Search/SynchronizationSearch.php +++ b/src/Spryker/Zed/Synchronization/Business/Search/SynchronizationSearch.php @@ -46,6 +46,11 @@ class SynchronizationSearch implements SynchronizationInterface */ protected const STORE = 'store'; + /** + * @var string + */ + protected const DESTINATION_TYPE = 'search'; + /** * @var \Spryker\Zed\Synchronization\Dependency\Client\SynchronizationToSearchClientInterface */ @@ -249,6 +254,16 @@ public function deleteBulk(array $data): void $this->searchClient->deleteBulk($searchDocumentTransfers); } + /** + * @param string $destinationType + * + * @return bool + */ + public function isDestinationTypeApplicable(string $destinationType): bool + { + return $destinationType === static::DESTINATION_TYPE; + } + /** * @param array $data * diff --git a/src/Spryker/Zed/Synchronization/Business/Storage/SynchronizationStorage.php b/src/Spryker/Zed/Synchronization/Business/Storage/SynchronizationStorage.php index 53902ec..345cc59 100644 --- a/src/Spryker/Zed/Synchronization/Business/Storage/SynchronizationStorage.php +++ b/src/Spryker/Zed/Synchronization/Business/Storage/SynchronizationStorage.php @@ -24,6 +24,11 @@ class SynchronizationStorage implements SynchronizationInterface */ public const VALUE = 'value'; + /** + * @var string + */ + protected const DESTINATION_TYPE = 'storage'; + /** * @var \Spryker\Zed\Synchronization\Dependency\Client\SynchronizationToStorageClientInterface */ @@ -174,4 +179,14 @@ public function deleteBulk(array $data): void $this->storageClient->deleteMulti($keysToDelete); } + + /** + * @param string $destinationType + * + * @return bool + */ + public function isDestinationTypeApplicable(string $destinationType): bool + { + return $destinationType === static::DESTINATION_TYPE; + } } diff --git a/src/Spryker/Zed/Synchronization/Business/Synchronization/SynchronizationInterface.php b/src/Spryker/Zed/Synchronization/Business/Synchronization/SynchronizationInterface.php index 5def9ec..4b756bf 100644 --- a/src/Spryker/Zed/Synchronization/Business/Synchronization/SynchronizationInterface.php +++ b/src/Spryker/Zed/Synchronization/Business/Synchronization/SynchronizationInterface.php @@ -38,4 +38,11 @@ public function delete(array $data, $queueName); * @return void */ public function deleteBulk(array $data): void; + + /** + * @param string $destinationType + * + * @return bool + */ + public function isDestinationTypeApplicable(string $destinationType): bool; } diff --git a/src/Spryker/Zed/Synchronization/Business/SynchronizationBusinessFactory.php b/src/Spryker/Zed/Synchronization/Business/SynchronizationBusinessFactory.php index 74d7c9b..27b605f 100644 --- a/src/Spryker/Zed/Synchronization/Business/SynchronizationBusinessFactory.php +++ b/src/Spryker/Zed/Synchronization/Business/SynchronizationBusinessFactory.php @@ -19,6 +19,8 @@ use Spryker\Zed\Synchronization\Business\Message\QueueMessageProcessorInterface; use Spryker\Zed\Synchronization\Business\Search\SynchronizationSearch; use Spryker\Zed\Synchronization\Business\Storage\SynchronizationStorage; +use Spryker\Zed\Synchronization\Business\Synchronizer\InMemoryMessageSynchronizer; +use Spryker\Zed\Synchronization\Business\Synchronizer\MessageSynchronizerInterface; use Spryker\Zed\Synchronization\Business\Validation\OutdatedValidator; use Spryker\Zed\Synchronization\Dependency\Facade\SynchronizationToStoreFacadeInterface; use Spryker\Zed\Synchronization\SynchronizationDependencyProvider; @@ -29,6 +31,11 @@ */ class SynchronizationBusinessFactory extends AbstractBusinessFactory { + /** + * @var \Spryker\Zed\Synchronization\Business\Synchronizer\MessageSynchronizerInterface|null + */ + protected static $inMemoryMessageSynchronizer; + /** * @return \Spryker\Zed\Synchronization\Business\Synchronization\SynchronizationInterface */ @@ -153,6 +160,32 @@ protected function createQueueMessageCreator() ); } + /** + * @return \Spryker\Zed\Synchronization\Business\Synchronizer\MessageSynchronizerInterface + */ + public function createInMemoryMessageSynchronizer(): MessageSynchronizerInterface + { + if (!static::$inMemoryMessageSynchronizer) { + static::$inMemoryMessageSynchronizer = new InMemoryMessageSynchronizer( + $this->getQueueClient(), + $this->createSynchronizationWriters(), + ); + } + + return static::$inMemoryMessageSynchronizer; + } + + /** + * @return list<\Spryker\Zed\Synchronization\Business\Synchronization\SynchronizationInterface> + */ + public function createSynchronizationWriters(): array + { + return [ + $this->createStorageManager(), + $this->createSearchManager(), + ]; + } + /** * @return \Spryker\Zed\Synchronization\Dependency\Client\SynchronizationToStorageClientInterface */ diff --git a/src/Spryker/Zed/Synchronization/Business/SynchronizationFacade.php b/src/Spryker/Zed/Synchronization/Business/SynchronizationFacade.php index 9df606b..5d85e75 100644 --- a/src/Spryker/Zed/Synchronization/Business/SynchronizationFacade.php +++ b/src/Spryker/Zed/Synchronization/Business/SynchronizationFacade.php @@ -7,6 +7,7 @@ namespace Spryker\Zed\Synchronization\Business; +use Generated\Shared\Transfer\SynchronizationMessageTransfer; use Spryker\Zed\Kernel\Business\AbstractFacade; /** @@ -148,4 +149,30 @@ public function getAvailableResourceNames(): array { return $this->getFactory()->createExporterPluginResolver()->getAvailableResourceNames(); } + + /** + * {@inheritDoc} + * + * @api + * + * @param \Generated\Shared\Transfer\SynchronizationMessageTransfer $synchronizationMessage + * + * @return void + */ + public function addSynchronizationMessageToBuffer(SynchronizationMessageTransfer $synchronizationMessage): void + { + $this->getFactory()->createInMemoryMessageSynchronizer()->addSynchronizationMessage($synchronizationMessage); + } + + /** + * {@inheritDoc} + * + * @api + * + * @return void + */ + public function flushSynchronizationMessagesFromBuffer(): void + { + $this->getFactory()->createInMemoryMessageSynchronizer()->flushSynchronizationMessages(); + } } diff --git a/src/Spryker/Zed/Synchronization/Business/SynchronizationFacadeInterface.php b/src/Spryker/Zed/Synchronization/Business/SynchronizationFacadeInterface.php index 5d118a4..2db70a5 100644 --- a/src/Spryker/Zed/Synchronization/Business/SynchronizationFacadeInterface.php +++ b/src/Spryker/Zed/Synchronization/Business/SynchronizationFacadeInterface.php @@ -7,6 +7,8 @@ namespace Spryker\Zed\Synchronization\Business; +use Generated\Shared\Transfer\SynchronizationMessageTransfer; + interface SynchronizationFacadeInterface { /** @@ -135,4 +137,29 @@ public function executeResolvedPluginsBySourcesWithIds(array $resources, array $ * @return array */ public function getAvailableResourceNames(): array; + + /** + * Specification: + * - Adds a message to buffer storage. + * - The message will be synchronized to storage/search when {@link flushSynchronizationMessagesFromBuffer()} is called. + * + * @api + * + * @param \Generated\Shared\Transfer\SynchronizationMessageTransfer $synchronizationMessage + * + * @return void + */ + public function addSynchronizationMessageToBuffer(SynchronizationMessageTransfer $synchronizationMessage): void; + + /** + * Specification: + * - Syncs the buffered messages to storage/search. + * - Marks the messages as failed if error occurs. + * - Sends failed messages to queue as a fallback. + * + * @api + * + * @return void + */ + public function flushSynchronizationMessagesFromBuffer(): void; } diff --git a/src/Spryker/Zed/Synchronization/Business/Synchronizer/InMemoryMessageSynchronizer.php b/src/Spryker/Zed/Synchronization/Business/Synchronizer/InMemoryMessageSynchronizer.php new file mode 100644 index 0000000..1d1591b --- /dev/null +++ b/src/Spryker/Zed/Synchronization/Business/Synchronizer/InMemoryMessageSynchronizer.php @@ -0,0 +1,187 @@ +>>> + */ + protected static array $messages = []; + + /** + * @var \Spryker\Zed\Synchronization\Dependency\Client\SynchronizationToQueueClientInterface + */ + protected SynchronizationToQueueClientInterface $queueClient; + + /** + * @var list<\Spryker\Zed\Synchronization\Business\Synchronization\SynchronizationInterface> + */ + protected array $synchronizationWriters; + + /** + * @param \Spryker\Zed\Synchronization\Dependency\Client\SynchronizationToQueueClientInterface $queueClient + * @param list<\Spryker\Zed\Synchronization\Business\Synchronization\SynchronizationInterface> $synchronizationWriters + */ + public function __construct(SynchronizationToQueueClientInterface $queueClient, array $synchronizationWriters) + { + $this->queueClient = $queueClient; + $this->synchronizationWriters = $synchronizationWriters; + } + + /** + * @param \Generated\Shared\Transfer\SynchronizationMessageTransfer $synchronizationMessage + * + * @return void + */ + public function addSynchronizationMessage(SynchronizationMessageTransfer $synchronizationMessage): void + { + $operationType = $synchronizationMessage->getOperationType(); + $fallbackQueueName = $synchronizationMessage->getFallbackQueueName(); + $destinationType = $synchronizationMessage->getSyncDestinationType(); + + if (!in_array($operationType, [static::TYPE_WRITE, static::TYPE_DELETE], true)) { + return; + } + + static::$messages[$destinationType][$fallbackQueueName][$operationType][] = $synchronizationMessage; + } + + /** + * @return void + */ + public function flushSynchronizationMessages(): void + { + if (!static::$messages) { + return; + } + + foreach (static::$messages as $destinationType => $queues) { + foreach ($queues as $fallbackQueueName => $synchronizationMessagesGroupedByOperationType) { + try { + $this->synchronizeBulkMessages($destinationType, $synchronizationMessagesGroupedByOperationType); + } catch (Throwable $exception) { + $this->getLogger()->error( + sprintf('Exception occurred: %s. Message will be rerouted to the queue: %s', $exception->getMessage(), $fallbackQueueName), + ['exception' => $exception], + ); + $this->sendFailedMessagesToQueue($fallbackQueueName, $synchronizationMessagesGroupedByOperationType); + } + } + } + } + + /** + * @param string $destinationType + * @param array> $synchronizationMessagesGroupedByOperationType + * + * @return void + */ + protected function synchronizeBulkMessages( + string $destinationType, + array $synchronizationMessagesGroupedByOperationType + ): void { + $synchronizationWriter = $this->getSynchronizationWriter($destinationType); + + foreach ($synchronizationMessagesGroupedByOperationType as $operation => $messages) { + if ($operation === static::TYPE_WRITE) { + $synchronizationWriter->writeBulk($this->extractMessageData($messages)); + + continue; + } + + if ($operation === static::TYPE_DELETE) { + $synchronizationWriter->deleteBulk($this->extractMessageData($messages)); + } + } + } + + /** + * @param string $destinationType + * + * @throws \Exception + * + * @return \Spryker\Zed\Synchronization\Business\Synchronization\SynchronizationInterface + */ + protected function getSynchronizationWriter(string $destinationType): SynchronizationInterface + { + foreach ($this->synchronizationWriters as $synchronizationWriter) { + if ($synchronizationWriter->isDestinationTypeApplicable($destinationType)) { + return $synchronizationWriter; + } + } + + throw new Exception(sprintf( + 'Synchronization for destination type "%s" not found.', + $destinationType, + )); + } + + /** + * @param string $destinationQueue + * @param array> $synchronizationMessagesGroupedByOperationType + * + * @return void + */ + protected function sendFailedMessagesToQueue(string $destinationQueue, array $synchronizationMessagesGroupedByOperationType): void + { + foreach ($synchronizationMessagesGroupedByOperationType as $messages) { + $queueMessageTransfers = $this->extractQueueMessageTransfers($messages); + $this->queueClient->sendMessages($destinationQueue, $queueMessageTransfers); + } + } + + /** + * @param list<\Generated\Shared\Transfer\SynchronizationMessageTransfer> $synchronizationMessages + * + * @return list<\Generated\Shared\Transfer\QueueSendMessageTransfer> + */ + protected function extractQueueMessageTransfers(array $synchronizationMessages): array + { + $queueMessageTransfers = []; + foreach ($synchronizationMessages as $synchronizationMessageTransfer) { + $queueMessageTransfers[] = $synchronizationMessageTransfer->getFallbackQueueMessage(); + } + + return $queueMessageTransfers; + } + + /** + * @param list<\Generated\Shared\Transfer\SynchronizationMessageTransfer> $synchronizationMessages + * + * @return array + */ + protected function extractMessageData(array $synchronizationMessages): array + { + $messageData = []; + foreach ($synchronizationMessages as $synchronizationMessageTransfer) { + $messageData[] = $synchronizationMessageTransfer->getData(); + } + + return $messageData; + } +} diff --git a/src/Spryker/Zed/Synchronization/Business/Synchronizer/MessageSynchronizerInterface.php b/src/Spryker/Zed/Synchronization/Business/Synchronizer/MessageSynchronizerInterface.php new file mode 100644 index 0000000..95326e6 --- /dev/null +++ b/src/Spryker/Zed/Synchronization/Business/Synchronizer/MessageSynchronizerInterface.php @@ -0,0 +1,25 @@ + + */ + public static function getSubscribedEvents(): array + { + return [ + ConsoleEvents::TERMINATE => ['onConsoleTerminate'], + ]; + } + + /** + * {@inheritDoc} + * - Syncs the buffered messages to storage/search. + * - Marks the messages as failed if error occurs. + * - Sends failed messages to queue as a fallback. + * + * @api + * + * @param \Symfony\Component\Console\Event\ConsoleTerminateEvent $event + * + * @return void + */ + public function onConsoleTerminate(ConsoleTerminateEvent $event): void + { + $this->getFacade()->flushSynchronizationMessagesFromBuffer(); + } +} diff --git a/tests/SprykerTest/Zed/Synchronization/Business/Facade/AddSynchronizationMessageToBufferTest.php b/tests/SprykerTest/Zed/Synchronization/Business/Facade/AddSynchronizationMessageToBufferTest.php new file mode 100644 index 0000000..8db0735 --- /dev/null +++ b/tests/SprykerTest/Zed/Synchronization/Business/Facade/AddSynchronizationMessageToBufferTest.php @@ -0,0 +1,116 @@ +tester->clearStaticVariable(InMemoryMessageSynchronizer::class, 'messages'); + } + + /** + * @return void + */ + public function testShouldAddSyncMessageToInMemoryStorage(): void + { + // Arrange + $message = $this->tester->createFakeSynchronizationMessage('storage', 'sync.storage.product', 'write'); + $synchronizationMessageTransfer = (new SynchronizationMessageTransfer())->fromArray($message); + + // Act + $this->tester->getFacade()->addSynchronizationMessageToBuffer($synchronizationMessageTransfer); + + // Assert + $messagesInMemory = $this->tester->getStaticVariable(InMemoryMessageSynchronizer::class, 'messages'); + + $this->assertMessageKey($messagesInMemory); + $this->assertSame($message, $messagesInMemory['storage']['sync.storage.product']['write'][0]->toArray()); + } + + /** + * @return void + */ + public function testShouldAddDifferentSyncMessagesToInMemoryStorage(): void + { + // Arrange + $message1 = $this->tester->createFakeSynchronizationMessage('storage', 'sync.storage.product', 'write'); + $synchronizationMessageTransfer1 = (new SynchronizationMessageTransfer())->fromArray($message1); + + $message2 = $this->tester->createFakeSynchronizationMessage('storage', 'sync.storage.product', 'write'); + $synchronizationMessageTransfer2 = (new SynchronizationMessageTransfer())->fromArray($message2); + + $message3 = $this->tester->createFakeSynchronizationMessage('search', 'sync.search.product', 'delete'); + $synchronizationMessageTransfer3 = (new SynchronizationMessageTransfer())->fromArray($message3); + + // Act + $this->tester->getFacade()->addSynchronizationMessageToBuffer($synchronizationMessageTransfer1); + $this->tester->getFacade()->addSynchronizationMessageToBuffer($synchronizationMessageTransfer2); + $this->tester->getFacade()->addSynchronizationMessageToBuffer($synchronizationMessageTransfer3); + + // Assert + $messagesInMemory = $this->tester->getStaticVariable(InMemoryMessageSynchronizer::class, 'messages'); + + $this->assertSame($message1, $messagesInMemory['storage']['sync.storage.product']['write'][0]->toArray()); + $this->assertSame($message2, $messagesInMemory['storage']['sync.storage.product']['write'][1]->toArray()); + $this->assertSame($message3, $messagesInMemory['search']['sync.search.product']['delete'][0]->toArray()); + } + + /** + * @return void + */ + public function testShouldNotAddSyncMessageToInMemoryStorageWithWrongOperationType(): void + { + // Arrange + $message = $this->tester->createFakeSynchronizationMessage('storage', 'sync.storage.product', 'reset'); + $synchronizationMessageTransfer = (new SynchronizationMessageTransfer())->fromArray($message); + + // Act + $this->tester->getFacade()->addSynchronizationMessageToBuffer($synchronizationMessageTransfer); + + // Assert + $this->assertEmpty($this->tester->getStaticVariable(InMemoryMessageSynchronizer::class, 'messages')); + } + + /** + * @param array $messagesInMemory + * + * @return void + */ + protected function assertMessageKey(array $messagesInMemory): void + { + $this->assertArrayHasKey('storage', $messagesInMemory); + $this->assertArrayHasKey('sync.storage.product', $messagesInMemory['storage']); + $this->assertArrayHasKey('write', $messagesInMemory['storage']['sync.storage.product']); + } +} diff --git a/tests/SprykerTest/Zed/Synchronization/Business/Facade/FlushSynchronizationMessagesFromBufferTest.php b/tests/SprykerTest/Zed/Synchronization/Business/Facade/FlushSynchronizationMessagesFromBufferTest.php new file mode 100644 index 0000000..630e329 --- /dev/null +++ b/tests/SprykerTest/Zed/Synchronization/Business/Facade/FlushSynchronizationMessagesFromBufferTest.php @@ -0,0 +1,217 @@ +tester->clearStaticVariable(InMemoryMessageSynchronizer::class, 'messages'); + $this->tester->clearStaticVariable(SynchronizationBusinessFactory::class, 'inMemoryMessageSynchronizer'); + } + + /** + * @return void + */ + public function testShouldWriteBulkStorageAndSearchMessages(): void + { + // Assert + $this->assertBulkMessageProcessing('write'); + + // Arrange + $synchronizationMessageTransfer = (new SynchronizationMessageTransfer()) + ->fromArray($this->tester->createFakeSynchronizationMessage('storage', 'sync.storage.product', 'write')); + + $this->tester->getFacade()->addSynchronizationMessageToBuffer($synchronizationMessageTransfer); + + $synchronizationMessageTransfer = (new SynchronizationMessageTransfer()) + ->fromArray($this->tester->createFakeSynchronizationMessage('search', 'sync.search.product', 'write')); + + $this->tester->getFacade()->addSynchronizationMessageToBuffer($synchronizationMessageTransfer); + + // Act + $this->tester->getFacade()->flushSynchronizationMessagesFromBuffer(); + } + + /** + * @return void + */ + public function testShouldDeleteBulkStorageAndSearchMessages(): void + { + // Assert + $this->assertBulkMessageProcessing('delete'); + + // Arrange + $synchronizationMessageTransfer = (new SynchronizationMessageTransfer()) + ->fromArray($this->tester->createFakeSynchronizationMessage('storage', 'sync.storage.product', 'delete')); + + $this->tester->getFacade()->addSynchronizationMessageToBuffer($synchronizationMessageTransfer); + + $synchronizationMessageTransfer = (new SynchronizationMessageTransfer()) + ->fromArray($this->tester->createFakeSynchronizationMessage('search', 'sync.search.product', 'delete')); + + $this->tester->getFacade()->addSynchronizationMessageToBuffer($synchronizationMessageTransfer); + + // Act + $this->tester->getFacade()->flushSynchronizationMessagesFromBuffer(); + } + + /** + * @return void + */ + public function testShouldDeleteAndWriteBulkStorageAndSearchMessages(): void + { + // Assert + $this->assertBulkMessageProcessing('write-delete'); + + // Arrange + $synchronizationMessageTransfer = (new SynchronizationMessageTransfer()) + ->fromArray($this->tester->createFakeSynchronizationMessage('storage', 'sync.storage.product', 'delete')); + + $this->tester->getFacade()->addSynchronizationMessageToBuffer($synchronizationMessageTransfer); + + $synchronizationMessageTransfer = (new SynchronizationMessageTransfer()) + ->fromArray($this->tester->createFakeSynchronizationMessage('storage', 'sync.storage.product', 'write')); + + $this->tester->getFacade()->addSynchronizationMessageToBuffer($synchronizationMessageTransfer); + + $synchronizationMessageTransfer = (new SynchronizationMessageTransfer()) + ->fromArray($this->tester->createFakeSynchronizationMessage('search', 'sync.search.product', 'delete')); + + $this->tester->getFacade()->addSynchronizationMessageToBuffer($synchronizationMessageTransfer); + + $synchronizationMessageTransfer = (new SynchronizationMessageTransfer()) + ->fromArray($this->tester->createFakeSynchronizationMessage('search', 'sync.search.product', 'write')); + + $this->tester->getFacade()->addSynchronizationMessageToBuffer($synchronizationMessageTransfer); + + // Act + $this->tester->getFacade()->flushSynchronizationMessagesFromBuffer(); + } + + /** + * @return void + */ + public function testShouldSkipSynchronizationWhenMessagesAreEmpty(): void + { + // Assert + $this->assertBulkMessageProcessing('skip'); + + // Act + $this->tester->getFacade()->flushSynchronizationMessagesFromBuffer(); + } + + /** + * @return void + */ + public function testShouldCatchExceptionWhenSynchronizationForDestinationTypeNotFound(): void + { + // Assert + $this->assertBulkMessageProcessing('skip', true); + + // Arrange + $synchronizationMessageTransfer = (new SynchronizationMessageTransfer()) + ->fromArray($this->tester->createFakeSynchronizationMessage('unknown', 'sync.unknown.product', 'write')); + + $this->tester->getFacade()->addSynchronizationMessageToBuffer($synchronizationMessageTransfer); + + // Act + $this->tester->getFacade()->flushSynchronizationMessagesFromBuffer(); + } + + /** + * @return void + */ + public function testShouldSkipSynchronizationForNotFoundOperationType(): void + { + // Assert + $this->assertBulkMessageProcessing('skip'); + + // Arrange + $synchronizationMessageTransfer = (new SynchronizationMessageTransfer()) + ->fromArray($this->tester->createFakeSynchronizationMessage('storage', 'sync.storage.product', 'unknown')); + + $this->tester->getFacade()->addSynchronizationMessageToBuffer($synchronizationMessageTransfer); + + // Act + $this->tester->getFacade()->flushSynchronizationMessagesFromBuffer(); + } + + /** + * @param string $operationType + * @param bool $expectSendToQueue + * + * @return void + */ + protected function assertBulkMessageProcessing( + string $operationType, + bool $expectSendToQueue = false + ): void { + $storageManagerMock = $this->createMock(SynchronizationStorage::class); + $storageManagerMock->method('isDestinationTypeApplicable') + ->willReturnCallback(function (string $destinationType) { + return $destinationType === 'storage'; + }); + + $searchManagerMock = $this->createMock(SynchronizationSearch::class); + $searchManagerMock->method('isDestinationTypeApplicable') + ->willReturnCallback(function (string $destinationType) { + return $destinationType === 'search'; + }); + + $queueClientMock = $this->createMock(SynchronizationToQueueClientInterface::class); + + $operationMethods = [ + 'write' => ['write' => true, 'delete' => false], + 'delete' => ['write' => false, 'delete' => true], + 'write-delete' => ['write' => true, 'delete' => true], + 'skip' => ['write' => false, 'delete' => false], + ]; + + $methods = $operationMethods[$operationType]; + $storageManagerMock->expects($methods['write'] ? $this->once() : $this->never())->method('writeBulk'); + $storageManagerMock->expects($methods['delete'] ? $this->once() : $this->never())->method('deleteBulk'); + $searchManagerMock->expects($methods['write'] ? $this->once() : $this->never())->method('writeBulk'); + $searchManagerMock->expects($methods['delete'] ? $this->once() : $this->never())->method('deleteBulk'); + $queueClientMock->expects($expectSendToQueue ? $this->once() : $this->never())->method('sendMessages'); + + $this->tester->mockFactoryMethod('createStorageManager', $storageManagerMock); + $this->tester->mockFactoryMethod('createSearchManager', $searchManagerMock); + $this->tester->mockFactoryMethod('getQueueClient', $queueClientMock); + } +} diff --git a/tests/SprykerTest/Zed/Synchronization/_support/SynchronizationBusinessTester.php b/tests/SprykerTest/Zed/Synchronization/_support/SynchronizationBusinessTester.php index a97ad1d..7534f41 100644 --- a/tests/SprykerTest/Zed/Synchronization/_support/SynchronizationBusinessTester.php +++ b/tests/SprykerTest/Zed/Synchronization/_support/SynchronizationBusinessTester.php @@ -8,6 +8,7 @@ namespace SprykerTest\Zed\Synchronization; use Codeception\Actor; +use ReflectionClass; /** * @method void wantToTest($text) @@ -20,11 +21,82 @@ * @method void lookForwardTo($achieveValue) * @method void comment($description) * @method \Codeception\Lib\Friend haveFriend($name, $actorClass = null) + * @method \Spryker\Zed\Synchronization\Business\SynchronizationFacadeInterface getFacade(?string $moduleName = null) * * @SuppressWarnings(PHPMD) - * @method \Spryker\Zed\Synchronization\Business\SynchronizationFacadeInterface getFacade() */ class SynchronizationBusinessTester extends Actor { use _generated\SynchronizationBusinessTesterActions; + + /** + * @param string $className + * @param string $propertyName + * + * @return void + */ + public function clearStaticVariable(string $className, string $propertyName): void + { + $reflectionResolver = new ReflectionClass($className); + $reflectionProperty = $reflectionResolver->getProperty($propertyName); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue([]); + } + + /** + * @param string $className + * @param string $propertyName + * + * @return array + */ + public function getStaticVariable(string $className, string $propertyName): array + { + $reflectionResolver = new ReflectionClass($className); + $reflectionProperty = $reflectionResolver->getProperty($propertyName); + $reflectionProperty->setAccessible(true); + + return $reflectionProperty->getValue(); + } + + /** + * @param string $destinationType + * @param string $queueName + * @param string $operationType + * + * @return array + */ + public function createFakeSynchronizationMessage( + string $destinationType, + string $queueName, + string $operationType + ): array { + return [ + 'data' => [ + 'key' => 'product_concrete:en_us:321', + 'value' => [ + 'id_product_abstract' => 223, + 'id_product_concrete' => 321, + 'name' => 'HDMI cable (1.5m)', + 'sku' => 'cable-hdmi-1-1', + '_timestamp' => 1722247340.708308, + ], + 'resource' => 'product_concrete', + 'store' => '', + 'params' => [], + ], + 'fallback_queue_name' => $queueName, + 'sync_destination_type' => $destinationType, + 'operation_type' => $operationType, + 'locale' => 'en_US', + 'resource' => 'product_concrete', + 'fallback_queue_message' => [ + 'body' => '{"write":{"key":"product_concrete:en_us:321","value":{"merchant_reference":null,"id_product_abstract":223,"id_product_concrete":321,"attributes":{"packaging_unit":"Ring"},"name":"HDMI cable (1.5m)","sku":"cable-hdmi-1-1","url":"\\/en\\/hdmi-cable-223","description":"Enjoy clear, crisp, immediate connectivity with the High-Speed HDMI Cable. This quality High-Definition Multimedia Interface (HDMI) cable allows you to connect a wide variety of devices in the realms of home entertainment, computing, gaming, and more to your HDTV, projector, or monitor. Perfect for those that interact with multiple platforms and devices, you can rely on strong performance and playback delivery when it comes to your digital experience.","meta_title":null,"meta_keywords":null,"meta_description":null,"super_attributes_definition":["packaging_unit"],"color_code":null,"_timestamp":1722247340.708308},"resource":"product_concrete","store":"","params":[]}}', + 'routing_key' => null, + 'headers' => [], + 'store_name' => null, + 'locale' => null, + 'queue_pool_name' => 'synchronizationPool', + ], + ]; + } } diff --git a/tests/SprykerTest/Zed/Synchronization/codeception.yml b/tests/SprykerTest/Zed/Synchronization/codeception.yml index 94d4f00..10bab90 100644 --- a/tests/SprykerTest/Zed/Synchronization/codeception.yml +++ b/tests/SprykerTest/Zed/Synchronization/codeception.yml @@ -20,7 +20,11 @@ suites: - Asserts - \SprykerTest\Shared\Testify\Helper\Environment - \SprykerTest\Shared\Testify\Helper\ConfigHelper - - \SprykerTest\Shared\Testify\Helper\LocatorHelper + - \SprykerTest\Shared\Testify\Helper\LocatorHelper: + projectNamespaces: ['Pyz'] - \SprykerTest\Shared\Testify\Helper\DependencyHelper - \SprykerTest\Shared\Propel\Helper\TransactionHelper - \SprykerTest\Zed\Testify\Helper\BusinessHelper + - \SprykerTest\Zed\Testify\Helper\Business\DependencyProviderHelper + - \SprykerTest\Service\Container\Helper\ContainerHelper + - \SprykerTest\Zed\Store\Helper\StoreDependencyHelper