init
This commit is contained in:
+66
@@ -0,0 +1,66 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Util\DataInconsistency;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\UnexpectedValueException;
|
||||
|
||||
class DataInconsistencyController {
|
||||
const ORPHANED_SENDING_TASKS = 'orphaned_sending_tasks';
|
||||
const ORPHANED_SENDING_TASK_SUBSCRIBERS = 'orphaned_sending_task_subscribers';
|
||||
const SENDING_QUEUE_WITHOUT_NEWSLETTER = 'sending_queue_without_newsletter';
|
||||
const ORPHANED_SUBSCRIPTIONS = 'orphaned_subscriptions';
|
||||
const ORPHANED_LINKS = 'orphaned_links';
|
||||
const ORPHANED_NEWSLETTER_POSTS = 'orphaned_newsletter_posts';
|
||||
|
||||
const SUPPORTED_INCONSISTENCY_CHECKS = [
|
||||
self::ORPHANED_SENDING_TASKS,
|
||||
self::ORPHANED_SENDING_TASK_SUBSCRIBERS,
|
||||
self::SENDING_QUEUE_WITHOUT_NEWSLETTER,
|
||||
self::ORPHANED_SUBSCRIPTIONS,
|
||||
self::ORPHANED_LINKS,
|
||||
self::ORPHANED_NEWSLETTER_POSTS,
|
||||
];
|
||||
|
||||
private DataInconsistencyRepository $repository;
|
||||
|
||||
public function __construct(
|
||||
DataInconsistencyRepository $repository
|
||||
) {
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
public function getInconsistentDataStatus(): array {
|
||||
$result = [
|
||||
self::ORPHANED_SENDING_TASKS => $this->repository->getOrphanedSendingTasksCount(),
|
||||
self::ORPHANED_SENDING_TASK_SUBSCRIBERS => $this->repository->getOrphanedScheduledTasksSubscribersCount(),
|
||||
self::SENDING_QUEUE_WITHOUT_NEWSLETTER => $this->repository->getSendingQueuesWithoutNewsletterCount(),
|
||||
self::ORPHANED_SUBSCRIPTIONS => $this->repository->getOrphanedSubscriptionsCount(),
|
||||
self::ORPHANED_LINKS => $this->repository->getOrphanedNewsletterLinksCount(),
|
||||
self::ORPHANED_NEWSLETTER_POSTS => $this->repository->getOrphanedNewsletterPostsCount(),
|
||||
];
|
||||
$result['total'] = array_sum($result);
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function fixInconsistentData(string $inconsistency): void {
|
||||
if (!in_array($inconsistency, self::SUPPORTED_INCONSISTENCY_CHECKS, true)) {
|
||||
throw new UnexpectedValueException(__('Unsupported data inconsistency check.', 'mailpoet'));
|
||||
}
|
||||
if ($inconsistency === self::ORPHANED_SENDING_TASKS) {
|
||||
$this->repository->cleanupOrphanedSendingTasks();
|
||||
} elseif ($inconsistency === self::ORPHANED_SENDING_TASK_SUBSCRIBERS) {
|
||||
$this->repository->cleanupOrphanedScheduledTaskSubscribers();
|
||||
} elseif ($inconsistency === self::SENDING_QUEUE_WITHOUT_NEWSLETTER) {
|
||||
$this->repository->cleanupSendingQueuesWithoutNewsletter();
|
||||
} elseif ($inconsistency === self::ORPHANED_SUBSCRIPTIONS) {
|
||||
$this->repository->cleanupOrphanedSubscriptions();
|
||||
} elseif ($inconsistency === self::ORPHANED_LINKS) {
|
||||
$this->repository->cleanupOrphanedNewsletterLinks();
|
||||
} elseif ($inconsistency === self::ORPHANED_NEWSLETTER_POSTS) {
|
||||
$this->repository->cleanupOrphanedNewsletterPosts();
|
||||
}
|
||||
}
|
||||
}
|
||||
+256
@@ -0,0 +1,256 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Util\DataInconsistency;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Cron\Workers\SendingQueue\SendingQueue;
|
||||
use MailPoet\Entities\NewsletterEntity;
|
||||
use MailPoet\Entities\NewsletterLinkEntity;
|
||||
use MailPoet\Entities\NewsletterPostEntity;
|
||||
use MailPoet\Entities\ScheduledTaskEntity;
|
||||
use MailPoet\Entities\ScheduledTaskSubscriberEntity;
|
||||
use MailPoet\Entities\SegmentEntity;
|
||||
use MailPoet\Entities\SendingQueueEntity;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Entities\SubscriberSegmentEntity;
|
||||
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
|
||||
use MailPoetVendor\Doctrine\DBAL\ParameterType;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
use MailPoetVendor\Doctrine\ORM\Query;
|
||||
use MailPoetVendor\Doctrine\ORM\QueryBuilder;
|
||||
|
||||
class DataInconsistencyRepository {
|
||||
const DELETE_ROWS_LIMIT = 10000;
|
||||
|
||||
private EntityManager $entityManager;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
public function getOrphanedSendingTasksCount(): int {
|
||||
$builder = $this->entityManager->createQueryBuilder()
|
||||
->select('count(st.id)');
|
||||
return (int)$this->buildOrphanedSendingTasksQuery($builder)->getSingleScalarResult();
|
||||
}
|
||||
|
||||
public function getOrphanedScheduledTasksSubscribersCount(): int {
|
||||
$this->createOrphanedScheduledTaskSubscribersTemporaryTables();
|
||||
$count = $this->getOrphanedScheduledTasksSubscribersCountFromTemporaryTables();
|
||||
$this->dropOrphanedScheduledTaskSubscribersTemporaryTables();
|
||||
return $count;
|
||||
}
|
||||
|
||||
private function getOrphanedScheduledTasksSubscribersCountFromTemporaryTables(): int {
|
||||
$connection = $this->entityManager->getConnection();
|
||||
$stsTable = $this->entityManager->getClassMetadata(ScheduledTaskSubscriberEntity::class)->getTableName();
|
||||
/** @var string $count */
|
||||
$count = $connection->executeQuery("
|
||||
SELECT COUNT(*) FROM $stsTable sts WHERE sts.task_id IN (SELECT task_id FROM orphaned_task_ids)
|
||||
")->fetchOne();
|
||||
return intval($count);
|
||||
}
|
||||
|
||||
public function getSendingQueuesWithoutNewsletterCount(): int {
|
||||
$sqTable = $this->entityManager->getClassMetadata(SendingQueueEntity::class)->getTableName();
|
||||
$newsletterTable = $this->entityManager->getClassMetadata(NewsletterEntity::class)->getTableName();
|
||||
/** @var string $count */
|
||||
$count = $this->entityManager->getConnection()->executeQuery("
|
||||
SELECT count(*) FROM $sqTable sq
|
||||
LEFT JOIN $newsletterTable n ON n.`id` = sq.`newsletter_id`
|
||||
WHERE n.`id` IS NULL
|
||||
")->fetchOne();
|
||||
return intval($count);
|
||||
}
|
||||
|
||||
public function getOrphanedSubscriptionsCount(): int {
|
||||
$subscriberTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
$segmentTable = $this->entityManager->getClassMetadata(SegmentEntity::class)->getTableName();
|
||||
$subscriberSegmentTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
|
||||
/** @var string $count */
|
||||
$count = $this->entityManager->getConnection()->executeQuery("
|
||||
SELECT count(distinct ss.`id`) FROM $subscriberSegmentTable ss
|
||||
LEFT JOIN $segmentTable seg ON seg.`id` = ss.`segment_id`
|
||||
LEFT JOIN $subscriberTable sub ON sub.`id` = ss.`subscriber_id`
|
||||
WHERE seg.`id` IS NULL OR sub.`id` IS NULL
|
||||
")->fetchOne();
|
||||
return intval($count);
|
||||
}
|
||||
|
||||
public function getOrphanedNewsletterLinksCount(): int {
|
||||
$newsletterTable = $this->entityManager->getClassMetadata(NewsletterEntity::class)->getTableName();
|
||||
$sendingQueueTable = $this->entityManager->getClassMetadata(SendingQueueEntity::class)->getTableName();
|
||||
$newsletterLinkTable = $this->entityManager->getClassMetadata(NewsletterLinkEntity::class)->getTableName();
|
||||
/** @var string $count */
|
||||
$count = $this->entityManager->getConnection()->executeQuery("
|
||||
SELECT count(distinct nl.`id`) FROM $newsletterLinkTable nl
|
||||
LEFT JOIN $newsletterTable n ON n.`id` = nl.`newsletter_id`
|
||||
LEFT JOIN $sendingQueueTable sq ON sq.`id` = nl.`queue_id`
|
||||
WHERE n.`id` IS NULL OR sq.`id` IS NULL
|
||||
")->fetchOne();
|
||||
return intval($count);
|
||||
}
|
||||
|
||||
public function getOrphanedNewsletterPostsCount(): int {
|
||||
$newsletterTable = $this->entityManager->getClassMetadata(NewsletterEntity::class)->getTableName();
|
||||
$newsletterPostTable = $this->entityManager->getClassMetadata(NewsletterPostEntity::class)->getTableName();
|
||||
/** @var string $count */
|
||||
$count = $this->entityManager->getConnection()->executeQuery("
|
||||
SELECT count(distinct np.`id`) FROM $newsletterPostTable np
|
||||
LEFT JOIN $newsletterTable n ON n.`id` = np.`newsletter_id`
|
||||
WHERE n.`id` IS NULL
|
||||
")->fetchOne();
|
||||
return intval($count);
|
||||
}
|
||||
|
||||
public function cleanupOrphanedSendingTasks(): int {
|
||||
/** @var array<int, array{id: string}> $ids */
|
||||
$ids = $this->buildOrphanedSendingTasksQuery(
|
||||
$this->entityManager->createQueryBuilder()
|
||||
->select('st.id')
|
||||
)->getResult();
|
||||
|
||||
if (!$ids) {
|
||||
return 0;
|
||||
}
|
||||
$ids = array_column($ids, 'id');
|
||||
// delete the orphaned tasks
|
||||
$qb = $this->entityManager->createQueryBuilder();
|
||||
$countDeletedTasks = $qb->delete(ScheduledTaskEntity::class, 'st')
|
||||
->where($qb->expr()->in('st.id', ':ids'))
|
||||
->setParameter('ids', $ids)
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
// delete the scheduled tasks subscribers
|
||||
$stsTable = $this->entityManager->getClassMetadata(ScheduledTaskSubscriberEntity::class)->getTableName();
|
||||
$this->entityManager->getConnection()->executeStatement(
|
||||
"DELETE sts_top FROM $stsTable sts_top
|
||||
JOIN (
|
||||
SELECT sts.`task_id`, sts.`subscriber_id` FROM $stsTable sts
|
||||
WHERE `task_id` IN (:ids)
|
||||
LIMIT :limit
|
||||
) as to_delete ON sts_top.`task_id` = to_delete.`task_id` AND sts_top.`subscriber_id` = to_delete.`subscriber_id`",
|
||||
['limit' => self::DELETE_ROWS_LIMIT, 'ids' => $ids],
|
||||
['limit' => ParameterType::INTEGER, 'ids' => ArrayParameterType::INTEGER]
|
||||
);
|
||||
|
||||
|
||||
$qb = $this->entityManager->createQueryBuilder();
|
||||
$qb->delete(ScheduledTaskSubscriberEntity::class, 'sts')
|
||||
->where($qb->expr()->in('sts.task', ':ids'))
|
||||
->setParameter('ids', $ids)
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
return $countDeletedTasks;
|
||||
}
|
||||
|
||||
public function cleanupOrphanedScheduledTaskSubscribers(): int {
|
||||
$stsTable = $this->entityManager->getClassMetadata(ScheduledTaskSubscriberEntity::class)->getTableName();
|
||||
$deletedCount = 0;
|
||||
|
||||
$this->createOrphanedScheduledTaskSubscribersTemporaryTables();
|
||||
do {
|
||||
$deletedCount += (int)$this->entityManager->getConnection()->executeStatement(
|
||||
"
|
||||
DELETE sts_top FROM $stsTable sts_top
|
||||
JOIN (
|
||||
SELECT task_id, subscriber_id
|
||||
FROM $stsTable
|
||||
WHERE task_id IN (SELECT task_id FROM orphaned_task_ids)
|
||||
LIMIT :limit
|
||||
) AS to_delete ON sts_top.task_id = to_delete.task_id AND sts_top.subscriber_id = to_delete.subscriber_id
|
||||
",
|
||||
['limit' => self::DELETE_ROWS_LIMIT],
|
||||
['limit' => ParameterType::INTEGER]
|
||||
);
|
||||
} while ($this->getOrphanedScheduledTasksSubscribersCountFromTemporaryTables() > 0);
|
||||
$this->dropOrphanedScheduledTaskSubscribersTemporaryTables();
|
||||
return $deletedCount;
|
||||
}
|
||||
|
||||
public function cleanupSendingQueuesWithoutNewsletter(): int {
|
||||
$sqTable = $this->entityManager->getClassMetadata(SendingQueueEntity::class)->getTableName();
|
||||
$newsletterTable = $this->entityManager->getClassMetadata(NewsletterEntity::class)->getTableName();
|
||||
$deletedQueuesCount = (int)$this->entityManager->getConnection()->executeStatement("
|
||||
DELETE sq FROM $sqTable sq
|
||||
LEFT JOIN $newsletterTable n ON n.`id` = sq.`newsletter_id`
|
||||
WHERE n.`id` IS NULL
|
||||
");
|
||||
|
||||
$this->cleanupOrphanedSendingTasks();
|
||||
return $deletedQueuesCount;
|
||||
}
|
||||
|
||||
public function cleanupOrphanedSubscriptions(): int {
|
||||
$subscriberTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
$segmentTable = $this->entityManager->getClassMetadata(SegmentEntity::class)->getTableName();
|
||||
$subscriberSegmentTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
|
||||
return (int)$this->entityManager->getConnection()->executeStatement("
|
||||
DELETE ss FROM $subscriberSegmentTable ss
|
||||
LEFT JOIN $segmentTable seg ON seg.`id` = ss.`segment_id`
|
||||
LEFT JOIN $subscriberTable sub ON sub.`id` = ss.`subscriber_id`
|
||||
WHERE seg.`id` IS NULL OR sub.`id` IS NULL
|
||||
");
|
||||
}
|
||||
|
||||
public function cleanupOrphanedNewsletterLinks(): int {
|
||||
$newsletterTable = $this->entityManager->getClassMetadata(NewsletterEntity::class)->getTableName();
|
||||
$sendingQueueTable = $this->entityManager->getClassMetadata(SendingQueueEntity::class)->getTableName();
|
||||
$newsletterLinkTable = $this->entityManager->getClassMetadata(NewsletterLinkEntity::class)->getTableName();
|
||||
return (int)$this->entityManager->getConnection()->executeStatement("
|
||||
DELETE nl FROM $newsletterLinkTable nl
|
||||
LEFT JOIN $newsletterTable n ON n.`id` = nl.`newsletter_id`
|
||||
LEFT JOIN $sendingQueueTable sq ON sq.`id` = nl.`queue_id`
|
||||
WHERE n.`id` IS NULL OR sq.`id` IS NULL
|
||||
");
|
||||
}
|
||||
|
||||
public function cleanupOrphanedNewsletterPosts(): int {
|
||||
$newsletterTable = $this->entityManager->getClassMetadata(NewsletterEntity::class)->getTableName();
|
||||
$newsletterPostTable = $this->entityManager->getClassMetadata(NewsletterPostEntity::class)->getTableName();
|
||||
return (int)$this->entityManager->getConnection()->executeStatement("
|
||||
DELETE np FROM $newsletterPostTable np
|
||||
LEFT JOIN $newsletterTable n ON n.`id` = np.`newsletter_id`
|
||||
WHERE n.`id` IS NULL
|
||||
");
|
||||
}
|
||||
|
||||
private function buildOrphanedSendingTasksQuery(QueryBuilder $queryBuilder): Query {
|
||||
return $queryBuilder
|
||||
->from(ScheduledTaskEntity::class, 'st')
|
||||
->leftJoin('st.sendingQueue', 'sq')
|
||||
->where('sq.id IS NULL')
|
||||
->andWhere('st.type = :type')
|
||||
->setParameter('type', SendingQueue::TASK_TYPE)
|
||||
->getQuery();
|
||||
}
|
||||
|
||||
private function createOrphanedScheduledTaskSubscribersTemporaryTables(): void {
|
||||
$connection = $this->entityManager->getConnection();
|
||||
$stTable = $this->entityManager->getClassMetadata(ScheduledTaskEntity::class)->getTableName();
|
||||
$stsTable = $this->entityManager->getClassMetadata(ScheduledTaskSubscriberEntity::class)->getTableName();
|
||||
|
||||
// 1. Get the DISTINCT task IDs so that the subsequent JOIN is more efficient.
|
||||
$connection->executeStatement("
|
||||
CREATE TEMPORARY TABLE IF NOT EXISTS task_ids
|
||||
SELECT DISTINCT task_id FROM $stsTable
|
||||
");
|
||||
|
||||
// 2. Get the orphaned task IDs.
|
||||
$connection->executeStatement("
|
||||
CREATE TEMPORARY TABLE IF NOT EXISTS orphaned_task_ids
|
||||
SELECT task_id FROM task_ids LEFT JOIN $stTable st ON st.id = task_ids.task_id WHERE st.id IS NULL
|
||||
");
|
||||
}
|
||||
|
||||
private function dropOrphanedScheduledTaskSubscribersTemporaryTables(): void {
|
||||
$this->entityManager->getConnection()->executeStatement("DROP TEMPORARY TABLE IF EXISTS task_ids");
|
||||
$this->entityManager->getConnection()->executeStatement("DROP TEMPORARY TABLE IF EXISTS orphaned_task_ids");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
Reference in New Issue
Block a user