diff --git a/UPGRADE.md b/UPGRADE.md index efde4f2d8..e3e041e18 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -67,6 +67,10 @@ - Added methods `setProfile()`, `getGateway()`, `setGateway()` and `setConnection()` to `SwagMigrationAssistant\Migration\MigrationContext` - Added null checks to methods `getProfile()` and `getGateway()` in `SwagMigrationAssistant\Migration\MigrationContext` to ensure that a profile and gateway is set before usage +- [BREAKING] [#57](https://github.com/shopware/SwagMigrationAssistant/pull/57) feat!: checksum and reset via mq + - [BREAKING] Renamed method `cleanupMappingChecksums()` to `startCleanupMappingChecksums()` in `SwagMigrationAssistant\Migration\Run\RunServiceInterface` and implementation `SwagMigrationAssistant\Migration\Run\RunService` + - [BREAKING] Renamed method `cleanupMigrationData()` to `startTruncateMigrationData()` in `SwagMigrationAssistant\Migration\Run\RunServiceInterface` and implementation `SwagMigrationAssistant\Migration\Run\RunService` + # 14.0.0 - [BREAKING] MIG-1053 - Removed ability to set the `verify` flag for the guzzle API client. This is now always true by default. - [BREAKING] MIG-1053 - Refactored both Shopware 5 and Shopware 6 EnvironmentReader classes to provide more information about exceptions. diff --git a/src/Controller/StatusController.php b/src/Controller/StatusController.php index cdd95de87..677f9678e 100644 --- a/src/Controller/StatusController.php +++ b/src/Controller/StatusController.php @@ -341,13 +341,7 @@ public function resetChecksums(Request $request, Context $context): Response throw RoutingException::missingRequestParameter('connectionId'); } - $connection = $this->migrationConnectionRepo->search(new Criteria([$connectionId]), $context)->getEntities()->first(); - - if ($connection === null) { - throw MigrationException::noConnectionFound(); - } - - $this->runService->cleanupMappingChecksums($connectionId, $context); + $this->runService->startCleanupMappingChecksums($connectionId, $context); return new Response(); } @@ -360,18 +354,18 @@ public function resetChecksums(Request $request, Context $context): Response )] public function cleanupMigrationData(Context $context): Response { - $this->runService->cleanupMigrationData($context); + $this->runService->startTruncateMigrationData($context); return new Response(); } #[Route( - path: '/api/_action/migration/get-reset-status', - name: 'api.admin.migration.get-reset-status', + path: '/api/_action/migration/is-truncating-migration-data', + name: 'api.admin.migration.is-truncating-migration-data', defaults: ['_acl' => ['admin']], methods: [Request::METHOD_GET] )] - public function getResetStatus(Context $context): JsonResponse + public function isTruncatingMigrationData(Context $context): JsonResponse { $settings = $this->generalSettingRepo->search(new Criteria(), $context)->getEntities()->first(); @@ -381,4 +375,26 @@ public function getResetStatus(Context $context): JsonResponse return new JsonResponse($settings->isReset()); } + + #[Route( + path: '/api/_action/migration/is-resetting-checksums', + name: 'api.admin.migration.is-resetting-checksums', + defaults: ['_acl' => ['admin']], + methods: [Request::METHOD_GET] + )] + public function isResettingChecksums(Context $context): JsonResponse + { + $settings = $this->generalSettingRepo + ->search(new Criteria(), $context) + ->getEntities() + ->first(); + + if ($settings === null) { + return new JsonResponse(false); + } + + return new JsonResponse( + $settings->isResettingChecksums() + ); + } } diff --git a/src/Core/Migration/Migration1759000000AddIsResettingChecksumsToSetting.php b/src/Core/Migration/Migration1759000000AddIsResettingChecksumsToSetting.php new file mode 100644 index 000000000..f2ece4927 --- /dev/null +++ b/src/Core/Migration/Migration1759000000AddIsResettingChecksumsToSetting.php @@ -0,0 +1,37 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace SwagMigrationAssistant\Core\Migration; + +use Doctrine\DBAL\Connection; +use Shopware\Core\Framework\Log\Package; +use Shopware\Core\Framework\Migration\MigrationStep; + +#[Package('fundamentals@after-sales')] +class Migration1759000000AddIsResettingChecksumsToSetting extends MigrationStep +{ + public const TABLE = 'swag_migration_general_setting'; + + public const COLUMN = 'is_resetting_checksums'; + + public function getCreationTimestamp(): int + { + return 1759000000; + } + + public function update(Connection $connection): void + { + $this->addColumn( + connection: $connection, + table: self::TABLE, + column: self::COLUMN, + type: 'TINYINT(1)', + nullable: false, + default: '0' + ); + } +} diff --git a/src/DependencyInjection/migration.xml b/src/DependencyInjection/migration.xml index 9b45b2d86..a41275f96 100644 --- a/src/DependencyInjection/migration.xml +++ b/src/DependencyInjection/migration.xml @@ -185,7 +185,6 @@ - @@ -312,7 +311,7 @@ - + @@ -328,7 +327,14 @@ + + + + + + + @@ -368,7 +374,6 @@ - diff --git a/src/Exception/MigrationException.php b/src/Exception/MigrationException.php index 2f13fa472..0ac3376af 100644 --- a/src/Exception/MigrationException.php +++ b/src/Exception/MigrationException.php @@ -32,6 +32,10 @@ class MigrationException extends HttpException public const MIGRATION_IS_ALREADY_RUNNING = 'SWAG_MIGRATION__MIGRATION_IS_ALREADY_RUNNING'; + public const MIGRATION_IS_RESETTING_CHECKSUMS = 'SWAG_MIGRATION__MIGRATION_IS_RESETTING_CHECKSUMS'; + + public const MIGRATION_IS_TRUNCATING_DATA = 'SWAG_MIGRATION__MIGRATION_IS_TRUNCATING_DATA'; + public const NO_CONNECTION_IS_SELECTED = 'SWAG_MIGRATION__NO_CONNECTION_IS_SELECTED'; public const NO_CONNECTION_FOUND = 'SWAG_MIGRATION__NO_CONNECTION_FOUND'; @@ -294,6 +298,24 @@ public static function migrationIsAlreadyRunning(): self ); } + public static function checksumResetRunning(): self + { + return new MigrationIsAlreadyRunningException( + Response::HTTP_BAD_REQUEST, + self::MIGRATION_IS_RESETTING_CHECKSUMS, + 'Checksum reset is running.', + ); + } + + public static function truncatingDataRunning(): self + { + return new MigrationIsAlreadyRunningException( + Response::HTTP_BAD_REQUEST, + self::MIGRATION_IS_TRUNCATING_DATA, + 'Data truncation is running.', + ); + } + public static function noConnectionIsSelected(): self { return new self( diff --git a/src/Migration/MessageQueue/Handler/Processor/AbortingProcessor.php b/src/Migration/MessageQueue/Handler/Processor/AbortingProcessor.php index bf2a1773d..011df47bf 100644 --- a/src/Migration/MessageQueue/Handler/Processor/AbortingProcessor.php +++ b/src/Migration/MessageQueue/Handler/Processor/AbortingProcessor.php @@ -12,11 +12,10 @@ use Shopware\Core\Framework\Log\Package; use SwagMigrationAssistant\Migration\Data\SwagMigrationDataCollection; use SwagMigrationAssistant\Migration\Media\SwagMigrationMediaFileCollection; -use SwagMigrationAssistant\Migration\MessageQueue\Message\MigrationProcessMessage; +use SwagMigrationAssistant\Migration\MessageQueue\Message\ResetChecksumMessage; use SwagMigrationAssistant\Migration\MigrationContextInterface; use SwagMigrationAssistant\Migration\Run\MigrationProgress; use SwagMigrationAssistant\Migration\Run\MigrationStep; -use SwagMigrationAssistant\Migration\Run\RunServiceInterface; use SwagMigrationAssistant\Migration\Run\RunTransitionServiceInterface; use SwagMigrationAssistant\Migration\Run\SwagMigrationRunCollection; use SwagMigrationAssistant\Migration\Run\SwagMigrationRunEntity; @@ -35,7 +34,6 @@ public function __construct( EntityRepository $migrationDataRepo, EntityRepository $migrationMediaFileRepo, RunTransitionServiceInterface $runTransitionService, - private readonly RunServiceInterface $runService, private readonly MessageBusInterface $bus, ) { parent::__construct( @@ -57,12 +55,14 @@ public function process( SwagMigrationRunEntity $run, MigrationProgress $progress, ): void { - $connection = $migrationContext->getConnection(); - $this->runService->cleanupMappingChecksums($connection->getId(), $context); - - $this->runTransitionService->forceTransitionToRunStep($migrationContext->getRunUuid(), MigrationStep::CLEANUP); - $progress->setIsAborted(true); - $this->updateProgress($migrationContext->getRunUuid(), $progress, $context); - $this->bus->dispatch(new MigrationProcessMessage($context, $migrationContext->getRunUuid())); + $this->bus->dispatch(new ResetChecksumMessage( + $migrationContext->getConnection()->getId(), + $context, + $run->getId(), + $progress->getCurrentEntity(), + null, + 0, + true // abort flow flag + )); } } diff --git a/src/Migration/MessageQueue/Handler/Processor/CleanUpProcessor.php b/src/Migration/MessageQueue/Handler/Processor/CleanUpProcessor.php index 5ebe6ac0e..f6c186d0f 100644 --- a/src/Migration/MessageQueue/Handler/Processor/CleanUpProcessor.php +++ b/src/Migration/MessageQueue/Handler/Processor/CleanUpProcessor.php @@ -9,11 +9,9 @@ use Doctrine\DBAL\Connection; use Shopware\Core\Framework\Context; -use Shopware\Core\Framework\DataAbstractionLayer\Dbal\QueryBuilder; use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository; use Shopware\Core\Framework\Log\Package; use SwagMigrationAssistant\Migration\Data\SwagMigrationDataCollection; -use SwagMigrationAssistant\Migration\Data\SwagMigrationDataDefinition; use SwagMigrationAssistant\Migration\Media\SwagMigrationMediaFileCollection; use SwagMigrationAssistant\Migration\MessageQueue\Message\MigrationProcessMessage; use SwagMigrationAssistant\Migration\MigrationContextInterface; @@ -27,6 +25,8 @@ #[Package('fundamentals@after-sales')] class CleanUpProcessor extends AbstractProcessor { + public const BATCH_SIZE = 250; + /** * @param EntityRepository $migrationRunRepo * @param EntityRepository $migrationDataRepo @@ -37,7 +37,7 @@ public function __construct( EntityRepository $migrationDataRepo, EntityRepository $migrationMediaFileRepo, RunTransitionServiceInterface $runTransitionService, - private readonly Connection $dbalConnection, + private readonly Connection $connection, private readonly MessageBusInterface $bus, ) { parent::__construct( @@ -59,22 +59,51 @@ public function process( SwagMigrationRunEntity $run, MigrationProgress $progress, ): void { - $deleteCount = (int) $this->removeMigrationData(); + if ($progress->getTotal() === 0) { + $progress->setTotal($this->getMigrationDataTotal()); + $progress->setProgress(0); + } + + $deleteCount = $this->removeMigrationData(); + + if ($deleteCount > 0) { + $progress->setProgress( + $progress->getProgress() + $deleteCount + ); + } if ($deleteCount <= 0) { - $this->runTransitionService->transitionToRunStep($migrationContext->getRunUuid(), MigrationStep::INDEXING); + $this->runTransitionService->transitionToRunStep( + $migrationContext->getRunUuid(), + MigrationStep::INDEXING + ); } - $this->updateProgress($migrationContext->getRunUuid(), $progress, $context); - $this->bus->dispatch(new MigrationProcessMessage($context, $migrationContext->getRunUuid())); + $this->updateProgress( + $migrationContext->getRunUuid(), + $progress, + $context + ); + + $this->bus->dispatch(new MigrationProcessMessage( + $context, + $migrationContext->getRunUuid() + )); } - private function removeMigrationData(): int|string + private function removeMigrationData(): int { - return (new QueryBuilder($this->dbalConnection)) - ->delete(SwagMigrationDataDefinition::ENTITY_NAME) - ->andWhere('written = 1') - ->setMaxResults(1000) + return (int) $this->connection->createQueryBuilder() + ->delete('swag_migration_data') + ->setMaxResults(self::BATCH_SIZE) ->executeStatement(); } + + private function getMigrationDataTotal(): int + { + return (int) $this->connection->createQueryBuilder() + ->select('COUNT(id)')->from('swag_migration_data') + ->executeQuery() + ->fetchOne(); + } } diff --git a/src/Migration/MessageQueue/Handler/ResetChecksumHandler.php b/src/Migration/MessageQueue/Handler/ResetChecksumHandler.php new file mode 100644 index 000000000..76618154f --- /dev/null +++ b/src/Migration/MessageQueue/Handler/ResetChecksumHandler.php @@ -0,0 +1,195 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace SwagMigrationAssistant\Migration\MessageQueue\Handler; + +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\ParameterType; +use Shopware\Core\Framework\Context; +use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository; +use Shopware\Core\Framework\Log\Package; +use Shopware\Core\Framework\Uuid\Uuid; +use SwagMigrationAssistant\Migration\DataSelection\DefaultEntities; +use SwagMigrationAssistant\Migration\MessageQueue\Message\MigrationProcessMessage; +use SwagMigrationAssistant\Migration\MessageQueue\Message\ResetChecksumMessage; +use SwagMigrationAssistant\Migration\Run\MigrationProgress; +use SwagMigrationAssistant\Migration\Run\MigrationStep; +use SwagMigrationAssistant\Migration\Run\ProgressDataSetCollection; +use SwagMigrationAssistant\Migration\Run\RunTransitionServiceInterface; +use SwagMigrationAssistant\Migration\Run\SwagMigrationRunCollection; +use Symfony\Component\Messenger\Attribute\AsMessageHandler; +use Symfony\Component\Messenger\MessageBusInterface; + +/** + * @internal + */ +#[AsMessageHandler] +#[Package('fundamentals@after-sales')] +final readonly class ResetChecksumHandler +{ + public const BATCH_SIZE = 250; + + /** + * @param EntityRepository $migrationRunRepo + */ + public function __construct( + private Connection $connection, + private MessageBusInterface $messageBus, + private EntityRepository $migrationRunRepo, + private RunTransitionServiceInterface $runTransitionService, + ) { + } + + public function __invoke(ResetChecksumMessage $message): void + { + $connectionId = $message->getConnectionId(); + $totalMappings = $message->getTotalMappings(); + $progress = null; + + if ($totalMappings === null) { + $totalMappings = $this->getTotalMappingsCount($connectionId); + + if ($message->getRunId() !== null && $totalMappings > 0) { + $progress = $this->updateProgress( + $message, + 0, + $totalMappings, + $message->getContext() + ); + } + } + + $affectedRows = $this->resetChecksums($connectionId); + + if ($affectedRows === 0) { + $this->handleCompletion($message, $progress); + + return; + } + + $newProcessedCount = $message->getProcessedMappings() + $affectedRows; + + if ($message->getRunId() !== null) { + $progress = $this->updateProgress( + $message, + $newProcessedCount, + $totalMappings, + $message->getContext() + ); + } + + if ($affectedRows < self::BATCH_SIZE) { + $this->handleCompletion($message, $progress); + + return; + } + + $this->messageBus->dispatch(new ResetChecksumMessage( + $message->getConnectionId(), + $message->getContext(), + $message->getRunId(), + $message->getEntity(), + $totalMappings, + $newProcessedCount, + $message->isPartOfAbort() + )); + } + + private function handleCompletion(ResetChecksumMessage $message, ?MigrationProgress $progress): void + { + $this->clearResettingChecksumsFlag(); + + if (!$message->isPartOfAbort() || $message->getRunId() === null) { + return; + } + + $runId = $message->getRunId(); + $context = $message->getContext(); + + $this->runTransitionService->forceTransitionToRunStep( + $runId, + MigrationStep::CLEANUP + ); + + $finalProgress = new MigrationProgress( + 0, + 0, + $progress?->getDataSets() ?? new ProgressDataSetCollection(), + $message->getEntity() ?? DefaultEntities::RULE, + $progress?->getCurrentEntityProgress() ?? 0 + ); + $finalProgress->setIsAborted(true); + + $this->migrationRunRepo->upsert([ + [ + 'id' => $runId, + 'progress' => $finalProgress->jsonSerialize(), + ], + ], $context); + + $this->messageBus->dispatch(new MigrationProcessMessage( + $context, + $runId + )); + } + + private function resetChecksums(string $connectionId): int + { + return (int) $this->connection->executeStatement( + 'UPDATE swag_migration_mapping + SET checksum = NULL + WHERE checksum IS NOT NULL + AND connection_id = :connectionId + LIMIT :limit', + [ + 'connectionId' => Uuid::fromHexToBytes($connectionId), + 'limit' => self::BATCH_SIZE, + ], + [ + 'connectionId' => ParameterType::BINARY, + 'limit' => ParameterType::INTEGER, + ] + ); + } + + private function getTotalMappingsCount(string $connectionId): int + { + return (int) $this->connection->createQueryBuilder() + ->select('COUNT(m.id)') + ->from('swag_migration_mapping', 'm') + ->where('m.checksum IS NOT NULL') + ->andWhere('m.connection_id = :connectionId') + ->setParameter('connectionId', Uuid::fromHexToBytes($connectionId)) + ->executeQuery() + ->fetchOne(); + } + + private function updateProgress(ResetChecksumMessage $message, int $processed, int $total, Context $context): MigrationProgress + { + $progress = new MigrationProgress( + $processed, + $total, + new ProgressDataSetCollection(), + $message->getEntity() ?? DefaultEntities::RULE, + $processed + ); + + $this->migrationRunRepo->update([[ + 'id' => $message->getRunId(), + 'progress' => $progress->jsonSerialize(), + ]], $context); + + return $progress; + } + + private function clearResettingChecksumsFlag(): void + { + $this->connection->executeStatement( + 'UPDATE swag_migration_general_setting SET `is_resetting_checksums` = 0;' + ); + } +} diff --git a/src/Migration/MessageQueue/Handler/CleanupMigrationHandler.php b/src/Migration/MessageQueue/Handler/TruncateMigrationHandler.php similarity index 53% rename from src/Migration/MessageQueue/Handler/CleanupMigrationHandler.php rename to src/Migration/MessageQueue/Handler/TruncateMigrationHandler.php index 536118b91..79cd8978b 100644 --- a/src/Migration/MessageQueue/Handler/CleanupMigrationHandler.php +++ b/src/Migration/MessageQueue/Handler/TruncateMigrationHandler.php @@ -9,7 +9,7 @@ use Doctrine\DBAL\Connection; use Shopware\Core\Framework\Log\Package; -use SwagMigrationAssistant\Migration\MessageQueue\Message\CleanupMigrationMessage; +use SwagMigrationAssistant\Migration\MessageQueue\Message\TruncateMigrationMessage; use Symfony\Component\Messenger\Attribute\AsMessageHandler; use Symfony\Component\Messenger\MessageBusInterface; @@ -18,15 +18,17 @@ /** * @internal */ -final class CleanupMigrationHandler +final class TruncateMigrationHandler { + private const BATCH_SIZE = 250; + public function __construct( private readonly Connection $connection, private readonly MessageBusInterface $bus, ) { } - public function __invoke(CleanupMigrationMessage $message): void + public function __invoke(TruncateMigrationMessage $message): void { $currentStep = 0; $tablesToReset = [ @@ -38,16 +40,40 @@ public function __invoke(CleanupMigrationMessage $message): void 'swag_migration_connection', ]; - $step = \array_search($message->getTableName(), $tablesToReset, true); + $step = \array_search( + $message->getTableName(), + $tablesToReset, + true + ); + if ($step !== false) { $currentStep = $step; } + $affectedRows = (int) $this->connection->executeStatement( + 'DELETE FROM ' . $tablesToReset[$currentStep] . ' LIMIT ' . self::BATCH_SIZE + ); + + if ($affectedRows >= self::BATCH_SIZE) { + $this->bus->dispatch(new TruncateMigrationMessage( + $tablesToReset[$currentStep] + )); + + return; + } + $nextStep = $currentStep + 1; + if (isset($tablesToReset[$nextStep])) { - $nextMessage = new CleanupMigrationMessage($tablesToReset[$nextStep]); - $this->bus->dispatch($nextMessage); + $this->bus->dispatch(new TruncateMigrationMessage( + $tablesToReset[$nextStep] + )); + + return; } - $this->connection->executeStatement('DELETE FROM ' . $tablesToReset[$currentStep] . ';'); + + $this->connection->executeStatement( + 'UPDATE swag_migration_general_setting SET `is_reset` = 0;' + ); } } diff --git a/src/Migration/MessageQueue/Message/ResetChecksumMessage.php b/src/Migration/MessageQueue/Message/ResetChecksumMessage.php new file mode 100644 index 000000000..94af5c3be --- /dev/null +++ b/src/Migration/MessageQueue/Message/ResetChecksumMessage.php @@ -0,0 +1,62 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace SwagMigrationAssistant\Migration\MessageQueue\Message; + +use Shopware\Core\Framework\Context; +use Shopware\Core\Framework\Log\Package; +use Shopware\Core\Framework\MessageQueue\AsyncMessageInterface; + +#[Package('fundamentals@after-sales')] +readonly class ResetChecksumMessage implements AsyncMessageInterface +{ + public function __construct( + private string $connectionId, + private Context $context, + private ?string $runId = null, + private ?string $entity = null, + private ?int $totalMappings = null, + private int $processedMappings = 0, + private bool $isPartOfAbort = false, + ) { + } + + public function getConnectionId(): string + { + return $this->connectionId; + } + + public function getContext(): Context + { + return $this->context; + } + + public function getRunId(): ?string + { + return $this->runId; + } + + public function getEntity(): ?string + { + return $this->entity; + } + + public function getTotalMappings(): ?int + { + return $this->totalMappings; + } + + public function getProcessedMappings(): int + { + return $this->processedMappings; + } + + public function isPartOfAbort(): bool + { + return $this->isPartOfAbort; + } +} diff --git a/src/Migration/MessageQueue/Message/CleanupMigrationMessage.php b/src/Migration/MessageQueue/Message/TruncateMigrationMessage.php similarity index 90% rename from src/Migration/MessageQueue/Message/CleanupMigrationMessage.php rename to src/Migration/MessageQueue/Message/TruncateMigrationMessage.php index db9025c46..864521af8 100644 --- a/src/Migration/MessageQueue/Message/CleanupMigrationMessage.php +++ b/src/Migration/MessageQueue/Message/TruncateMigrationMessage.php @@ -11,7 +11,7 @@ use Shopware\Core\Framework\MessageQueue\AsyncMessageInterface; #[Package('fundamentals@after-sales')] -class CleanupMigrationMessage implements AsyncMessageInterface +class TruncateMigrationMessage implements AsyncMessageInterface { public function __construct(private readonly ?string $tableName = null) { diff --git a/src/Migration/Run/RunService.php b/src/Migration/Run/RunService.php index bff24908e..617035e01 100644 --- a/src/Migration/Run/RunService.php +++ b/src/Migration/Run/RunService.php @@ -10,17 +10,13 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\ParameterType; use Shopware\Core\Framework\Context; -use Shopware\Core\Framework\DataAbstractionLayer\Dbal\QueryBuilder; -use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition; use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository; use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria; use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter; use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter; use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\NotFilter; -use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting; use Shopware\Core\Framework\Log\Package; use Shopware\Core\Framework\Store\Services\TrackingEventClient; -use Shopware\Core\Framework\Uuid\Uuid; use Shopware\Core\System\SalesChannel\SalesChannelCollection; use Shopware\Core\System\SalesChannel\SalesChannelDefinition; use Shopware\Storefront\Theme\ThemeCollection; @@ -35,8 +31,9 @@ use SwagMigrationAssistant\Migration\Logging\Log\ThemeCompilingErrorRunLog; use SwagMigrationAssistant\Migration\Logging\LoggingServiceInterface; use SwagMigrationAssistant\Migration\Mapping\MappingServiceInterface; -use SwagMigrationAssistant\Migration\MessageQueue\Message\CleanupMigrationMessage; use SwagMigrationAssistant\Migration\MessageQueue\Message\MigrationProcessMessage; +use SwagMigrationAssistant\Migration\MessageQueue\Message\ResetChecksumMessage; +use SwagMigrationAssistant\Migration\MessageQueue\Message\TruncateMigrationMessage; use SwagMigrationAssistant\Migration\MigrationContext; use SwagMigrationAssistant\Migration\MigrationContextFactoryInterface; use SwagMigrationAssistant\Migration\MigrationContextInterface; @@ -69,7 +66,6 @@ public function __construct( private readonly EntityRepository $generalSettingRepo, private readonly ThemeService $themeService, private readonly MappingServiceInterface $mappingService, - private readonly EntityDefinition $migrationDataDefinition, private readonly Connection $dbalConnection, private readonly LoggingServiceInterface $loggingService, private readonly TrackingEventClient $trackingEventClient, @@ -86,6 +82,10 @@ public function startMigrationRun(array $dataSelectionIds, Context $context): vo throw MigrationException::migrationIsAlreadyRunning(); } + if ($this->isResettingChecksums()) { + throw MigrationException::checksumResetRunning(); + } + $connection = $this->getCurrentConnection($context); if ($connection === null) { @@ -97,8 +97,6 @@ public function startMigrationRun(array $dataSelectionIds, Context $context): vo } $connectionId = $connection->getId(); - $this->cleanupUnwrittenRunDataOfLastInactiveRun($context); - $runUuid = $this->createPlainMigrationRun($connectionId, $context); if ($runUuid === null) { @@ -177,30 +175,29 @@ public function abortMigration(Context $context): void $this->fireTrackingInformation(self::TRACKING_EVENT_MIGRATION_ABORTED, $runId, $context); } - public function cleanupMappingChecksums(string $connectionUuid, Context $context, bool $resetAll = true): void + public function startCleanupMappingChecksums(string $connectionId, Context $context): void { - $sql = <<connectionRepo->search( + new Criteria([$connectionId]), + $context, + )->getEntities()->first(); + + if ($connection === null) { + throw MigrationException::noConnectionFound(); } - $this->dbalConnection->executeStatement( - $sql, - [$connectionUuid], - [ParameterType::STRING] + $affectedRows = $this->dbalConnection->executeStatement( + 'UPDATE swag_migration_general_setting SET `is_resetting_checksums` = 1 WHERE `is_resetting_checksums` = 0;' ); + + if ($affectedRows === 0) { + throw MigrationException::checksumResetRunning(); + } + + $this->bus->dispatch(new ResetChecksumMessage( + $connectionId, + $context, + )); } public function approveFinishingMigration(Context $context): void @@ -220,14 +217,21 @@ public function approveFinishingMigration(Context $context): void $this->fireTrackingInformation(self::TRACKING_EVENT_MIGRATION_FINISHED, $run->getId(), $context); } - public function cleanupMigrationData(Context $context): void + public function startTruncateMigrationData(Context $context): void { if ($this->isMigrationRunning($context)) { throw MigrationException::migrationIsAlreadyRunning(); } - $this->dbalConnection->executeStatement('UPDATE swag_migration_general_setting SET selected_connection_id = NULL, `is_reset` = 1;'); - $this->bus->dispatch(new CleanupMigrationMessage()); + $affectedRows = $this->dbalConnection->executeStatement( + 'UPDATE swag_migration_general_setting SET selected_connection_id = NULL, `is_reset` = 1 WHERE `is_reset` = 0;' + ); + + if ($affectedRows === 0) { + throw MigrationException::truncatingDataRunning(); + } + + $this->bus->dispatch(new TruncateMigrationMessage()); } public function assignThemeToSalesChannel(string $runUuid, Context $context): void @@ -308,17 +312,11 @@ private function isMigrationRunning(Context $context): bool return $this->getActiveRun($context) !== null; } - private function getLastInactiveRun(Context $context): ?SwagMigrationRunEntity + private function isResettingChecksums(): bool { - $criteria = new Criteria(); - $criteria->addFilter(new MultiFilter(MultiFilter::CONNECTION_OR, [ - new EqualsFilter('step', MigrationStep::ABORTED->value), - new EqualsFilter('step', MigrationStep::FINISHED->value), - ])); - $criteria->addSorting(new FieldSorting('createdAt', 'DESC')); - $criteria->setLimit(1); - - return $this->migrationRunRepo->search($criteria, $context)->getEntities()->first(); + return (bool) $this->dbalConnection->fetchOne( + 'SELECT is_resetting_checksums FROM swag_migration_general_setting LIMIT 1' + ); } private function fireTrackingInformation(string $eventName, string $runUuid, Context $context): void @@ -550,21 +548,6 @@ private function getDefaultTheme(Context $context): ?string return \reset($ids); } - private function cleanupUnwrittenRunDataOfLastInactiveRun(Context $context): void - { - $lastInactiveRun = $this->getLastInactiveRun($context); - - if ($lastInactiveRun === null) { - return; - } - - $queryBuilder = new QueryBuilder($this->dbalConnection); - $queryBuilder->delete($this->migrationDataDefinition->getEntityName()) - ->andWhere('run_id = :runId') - ->setParameter('runId', Uuid::fromHexToBytes($lastInactiveRun->getId())) - ->executeStatement(); - } - private function updateUnprocessedMediaFiles(string $connectionId, string $runUuid): void { $sql = << $dataSelectionIds @@ -49,6 +51,4 @@ public function updateConnectionCredentials(Context $context, string $connection public function approveFinishingMigration(Context $context): void; public function assignThemeToSalesChannel(string $runUuid, Context $context): void; - - public function cleanupMigrationData(Context $context): void; } diff --git a/src/Migration/Setting/GeneralSettingDefinition.php b/src/Migration/Setting/GeneralSettingDefinition.php index e96767518..d075d466d 100644 --- a/src/Migration/Setting/GeneralSettingDefinition.php +++ b/src/Migration/Setting/GeneralSettingDefinition.php @@ -46,6 +46,7 @@ protected function defineFields(): FieldCollection (new IdField('id', 'id'))->addFlags(new PrimaryKey(), new Required()), new FkField('selected_connection_id', 'selectedConnectionId', SwagMigrationConnectionDefinition::class), new BoolField('is_reset', 'isReset'), + new BoolField('is_resetting_checksums', 'isResettingChecksums'), new CreatedAtField(), new UpdatedAtField(), new ManyToOneAssociationField('selectedConnection', 'selected_connection_id', SwagMigrationConnectionDefinition::class, 'id', true), diff --git a/src/Migration/Setting/GeneralSettingEntity.php b/src/Migration/Setting/GeneralSettingEntity.php index 8497e7baa..43dc70884 100644 --- a/src/Migration/Setting/GeneralSettingEntity.php +++ b/src/Migration/Setting/GeneralSettingEntity.php @@ -23,6 +23,8 @@ class GeneralSettingEntity extends Entity protected bool $isReset; + protected bool $isResettingChecksums = false; + public function getSelectedConnectionId(): ?string { return $this->selectedConnectionId; @@ -52,4 +54,14 @@ public function setIsReset(bool $isReset): void { $this->isReset = $isReset; } + + public function isResettingChecksums(): bool + { + return $this->isResettingChecksums; + } + + public function setIsResettingChecksums(bool $isResettingChecksums): void + { + $this->isResettingChecksums = $isResettingChecksums; + } } diff --git a/src/Resources/app/administration/src/core/service/api/swag-migration.api.service.ts b/src/Resources/app/administration/src/core/service/api/swag-migration.api.service.ts index 79b955a02..fdc4f0d9a 100644 --- a/src/Resources/app/administration/src/core/service/api/swag-migration.api.service.ts +++ b/src/Resources/app/administration/src/core/service/api/swag-migration.api.service.ts @@ -398,4 +398,34 @@ export default class MigrationApiService extends ApiService { headers, }); } + + async isResettingChecksums(): Promise { + // @ts-ignore + const headers = this.getBasicHeaders(); + + // @ts-ignore + return this.httpClient + .get(`_action/${this.getApiBasePath()}/is-resetting-checksums`, { + ...this.basicConfig, + headers, + }) + .then((response: AxiosResponse) => { + return ApiService.handleResponse(response); + }); + } + + async isTruncatingMigrationData(): Promise { + // @ts-ignore + const headers = this.getBasicHeaders(); + + // @ts-ignore + return this.httpClient + .get(`_action/${this.getApiBasePath()}/is-truncating-migration-data`, { + ...this.basicConfig, + headers, + }) + .then((response: AxiosResponse) => { + return ApiService.handleResponse(response); + }); + } } diff --git a/src/Resources/app/administration/src/module/swag-migration/component/card/swag-migration-shop-information/index.ts b/src/Resources/app/administration/src/module/swag-migration/component/card/swag-migration-shop-information/index.ts index fe6c3975b..865728fce 100644 --- a/src/Resources/app/administration/src/module/swag-migration/component/card/swag-migration-shop-information/index.ts +++ b/src/Resources/app/administration/src/module/swag-migration/component/card/swag-migration-shop-information/index.ts @@ -8,7 +8,7 @@ import type { TRepository, } from '../../../../../type/types'; import { MIGRATION_API_SERVICE } from '../../../../../core/service/api/swag-migration.api.service'; -import { MIGRATION_STORE_ID } from '../../../store/migration.store'; +import { MIGRATION_STORE_ID, type MigrationStore } from '../../../store/migration.store'; const { Mixin, Store } = Shopware; const { mapState } = Shopware.Component.getComponentHelper(); @@ -23,10 +23,15 @@ export const BADGE_TYPE = { DANGER: 'danger', } as const; +const MIGRATION_POLLING_INTERVAL = 2500 as const; + +type PollingType = 'checksum' | 'truncate'; + /** * @private */ export interface SwagMigrationShopInformationData { + migrationStore: MigrationStore; confirmModalIsLoading: boolean; showRemoveCredentialsConfirmModal: boolean; showResetChecksumsConfirmModal: boolean; @@ -34,6 +39,9 @@ export interface SwagMigrationShopInformationData { lastMigrationDate: string; connection: MigrationConnection | null; context: unknown; + checksumPollingIntervalId: number | null; + truncatePollingIntervalId: number | null; + isLoading: boolean; } /** @@ -52,14 +60,6 @@ export default Shopware.Component.wrapComponentConfig({ Mixin.getByName('notification'), ], - filters: { - localizedNumberFormat(value: number) { - const locale = `${this.adminLocaleLanguage}-${this.adminLocaleRegion}`; - - return Intl.NumberFormat(locale).format(value); - }, - }, - props: { connected: { type: Boolean, @@ -69,6 +69,9 @@ export default Shopware.Component.wrapComponentConfig({ data(): SwagMigrationShopInformationData { return { + migrationStore: Store.get(MIGRATION_STORE_ID), + checksumPollingIntervalId: null, + truncatePollingIntervalId: null, confirmModalIsLoading: false, showRemoveCredentialsConfirmModal: false, showResetChecksumsConfirmModal: false, @@ -76,6 +79,7 @@ export default Shopware.Component.wrapComponentConfig({ lastMigrationDate: '-', connection: null, context: Shopware.Context.api, + isLoading: false, }; }, @@ -83,6 +87,8 @@ export default Shopware.Component.wrapComponentConfig({ ...mapState( () => Store.get(MIGRATION_STORE_ID), [ + 'isResettingChecksum', + 'isTruncatingMigration', 'connectionId', 'currentConnection', 'environmentInformation', @@ -92,10 +98,6 @@ export default Shopware.Component.wrapComponentConfig({ ], ), - displayEnvironmentInformation() { - return this.environmentInformation === null ? {} : this.environmentInformation; - }, - migrationRunRepository(): TRepository<'swag_migration_run'> { return this.repositoryFactory.create('swag_migration_run'); }, @@ -104,38 +106,46 @@ export default Shopware.Component.wrapComponentConfig({ return this.repositoryFactory.create('swag_migration_connection'); }, - connectionName() { - return this.connection !== null - ? this.connection?.name - : this.$tc('swag-migration.index.shopInfoCard.noConnection'); + displayEnvironmentInformation() { + return this.environmentInformation === null ? {} : this.environmentInformation; }, - shopUrl() { - return this.displayEnvironmentInformation.sourceSystemDomain === undefined - ? '' - : this.displayEnvironmentInformation.sourceSystemDomain.replace(/^\s*https?:\/\//, ''); + isUpdating() { + return this.isResettingChecksum || this.isTruncatingMigration || this.isLoading; }, - shopUrlPrefix() { - if (this.displayEnvironmentInformation.sourceSystemDomain === undefined) { - return ''; - } + showUpdateBanner() { + return this.isResettingChecksum || this.isTruncatingMigration; + }, - const match = this.displayEnvironmentInformation.sourceSystemDomain.match(/^\s*https?:\/\//); + updateBannerTitle() { + if (this.isResettingChecksum) { + return this.$tc('swag-migration.index.shopInfoCard.updateBanner.isResettingChecksums.title'); + } - if (match === null) { - return ''; + if (this.isTruncatingMigration) { + return this.$tc('swag-migration.index.shopInfoCard.updateBanner.isTruncatingMigration.title'); } - return match[0]; + return ''; }, - sslActive() { - return this.shopUrlPrefix === 'https://'; + updateBannerMessage() { + if (this.isResettingChecksum) { + return this.$tc('swag-migration.index.shopInfoCard.updateBanner.isResettingChecksums.message'); + } + + if (this.isTruncatingMigration) { + return this.$tc('swag-migration.index.shopInfoCard.updateBanner.isTruncatingMigration.message'); + } + + return ''; }, - shopUrlPrefixClass() { - return this.sslActive ? 'swag-migration-shop-information__shop-domain-prefix--is-ssl' : ''; + connectionName() { + return this.connection !== null + ? this.connection.name + : this.$tc('swag-migration.index.shopInfoCard.noConnection'); }, connectionBadgeLabel() { @@ -158,6 +168,34 @@ export default Shopware.Component.wrapComponentConfig({ return BADGE_TYPE.DANGER; }, + shopUrl() { + return this.displayEnvironmentInformation.sourceSystemDomain === undefined + ? '' + : this.displayEnvironmentInformation.sourceSystemDomain.replace(/^\s*https?:\/\//, ''); + }, + + shopUrlPrefix() { + if (this.displayEnvironmentInformation.sourceSystemDomain === undefined) { + return ''; + } + + const match = this.displayEnvironmentInformation.sourceSystemDomain.match(/^\s*https?:\/\//); + + if (match === null) { + return ''; + } + + return match[0]; + }, + + sslActive() { + return this.shopUrlPrefix === 'https://'; + }, + + shopUrlPrefixClass() { + return this.sslActive ? 'swag-migration-shop-information__shop-domain-prefix--is-ssl' : ''; + }, + shopFirstLetter() { return this.displayEnvironmentInformation.sourceSystemName?.charAt(0) ?? 'S'; }, @@ -194,7 +232,7 @@ export default Shopware.Component.wrapComponentConfig({ }, showMoreInformation() { - return this.connection !== null && this.connection !== undefined; + return this.connection !== null; }, }, @@ -219,22 +257,29 @@ export default Shopware.Component.wrapComponentConfig({ }, methods: { - createdComponent() { - this.updateLastMigrationDate(); - }, - - openResetMigrationModal() { - this.showResetMigrationConfirmModal = true; - this.$router.push({ - name: 'swag.migration.index.resetMigration', - }); - }, - - async onCloseResetModal() { - this.showResetMigrationConfirmModal = false; - await this.$router.push({ - name: 'swag.migration.index.main', - }); + async createdComponent() { + this.isLoading = true; + + try { + const [ + isResettingChecksums, + isTruncatingMigration, + ] = await Promise.all([ + this.migrationApiService.isResettingChecksums(), + this.migrationApiService.isTruncatingMigrationData(), + this.updateLastMigrationDate(), + ]); + + if (isResettingChecksums) { + this.registerPolling('checksum'); + } + + if (isTruncatingMigration) { + this.registerPolling('truncate'); + } + } finally { + this.isLoading = false; + } }, async updateLastMigrationDate() { @@ -292,6 +337,76 @@ export default Shopware.Component.wrapComponentConfig({ }); }, + registerPolling(type: PollingType) { + this.unregisterPolling(type); + + if (type === 'checksum') { + this.migrationStore.setIsResettingChecksum(true); + this.checksumPollingIntervalId = setInterval(() => this.poll(type), MIGRATION_POLLING_INTERVAL); + } else { + this.migrationStore.setIsTruncatingMigration(true); + this.truncatePollingIntervalId = setInterval(() => this.poll(type), MIGRATION_POLLING_INTERVAL); + } + }, + + unregisterPolling(type: PollingType) { + if (type === 'checksum') { + if (this.checksumPollingIntervalId) { + clearInterval(this.checksumPollingIntervalId); + } + + this.checksumPollingIntervalId = null; + this.migrationStore.setIsResettingChecksum(false); + } else { + if (this.truncatePollingIntervalId) { + clearInterval(this.truncatePollingIntervalId); + } + + this.migrationStore.setIsTruncatingMigration(false); + this.truncatePollingIntervalId = null; + } + }, + + async poll(type: PollingType) { + const isActive = type === 'checksum' ? this.isResettingChecksum : this.isTruncatingMigration; + + if (!isActive) { + return; + } + + try { + const isStillRunning = + type === 'checksum' + ? await this.migrationApiService.isResettingChecksums() + : await this.migrationApiService.isTruncatingMigrationData(); + + if (!isStillRunning) { + this.unregisterPolling(type); + + if (type === 'truncate') { + this.migrationStore.init(true); + } + } + } catch { + this.unregisterPolling(type); + this.createNotificationError({ + title: this.$tc('global.default.error'), + message: this.$tc('swag-migration.api-error.getState'), + }); + } + }, + + openResetMigrationModal() { + this.showResetMigrationConfirmModal = true; + }, + + async onCloseResetModal() { + this.showResetMigrationConfirmModal = false; + await this.$router.push({ + name: 'swag.migration.index.main', + }); + }, + onClickEditConnectionCredentials() { this.$router.push({ name: 'swag.migration.wizard.credentials', @@ -325,6 +440,10 @@ export default Shopware.Component.wrapComponentConfig({ }); }, + onClickRefreshConnection() { + return this.migrationStore.init(true); + }, + async onClickRemoveConnectionCredentials() { this.confirmModalIsLoading = true; @@ -336,41 +455,21 @@ export default Shopware.Component.wrapComponentConfig({ async onClickResetChecksums() { this.confirmModalIsLoading = true; - return this.migrationApiService.resetChecksums(this.connectionId).then(() => { - this.showResetChecksumsConfirmModal = false; - this.confirmModalIsLoading = false; - }); + await this.migrationApiService.resetChecksums(this.connectionId); + this.registerPolling('checksum'); + + this.showResetChecksumsConfirmModal = false; + this.confirmModalIsLoading = false; }, async onClickResetMigration() { this.confirmModalIsLoading = true; - return this.migrationApiService - .cleanupMigrationData() - .catch(() => { - this.showResetMigrationConfirmModal = false; - this.confirmModalIsLoading = false; - - this.createNotificationError({ - title: this.$t( - 'swag-migration.index.shopInfoCard.resetMigrationConfirmDialog.errorNotification.title', - ), - message: this.$t( - 'swag-migration.index.shopInfoCard.resetMigrationConfirmDialog.errorNotification.message', - ), - variant: 'error', - growl: true, - }); - }) - .finally(async () => { - this.confirmModalIsLoading = false; - await this.onCloseResetModal(); - window.location.reload(); - }); - }, + await this.migrationApiService.cleanupMigrationData(); + this.registerPolling('truncate'); - onClickRefreshConnection() { - return Store.get(MIGRATION_STORE_ID).init(true); + this.showResetMigrationConfirmModal = false; + this.confirmModalIsLoading = false; }, }, }); diff --git a/src/Resources/app/administration/src/module/swag-migration/component/card/swag-migration-shop-information/swag-migration-shop-information.html.twig b/src/Resources/app/administration/src/module/swag-migration/component/card/swag-migration-shop-information/swag-migration-shop-information.html.twig index 063371661..431cd8d57 100644 --- a/src/Resources/app/administration/src/module/swag-migration/component/card/swag-migration-shop-information/swag-migration-shop-information.html.twig +++ b/src/Resources/app/administration/src/module/swag-migration/component/card/swag-migration-shop-information/swag-migration-shop-information.html.twig @@ -1,8 +1,18 @@ {% block swag_migration_shop_information %} + + {{ updateBannerMessage }} + +