Automations
1 year ago
KeyCheck
1 year ago
SendingQueue
6 days ago
StatsNotifications
2 months ago
AuthorizedSendingEmailsCheck.php
3 years ago
BackfillEngagementData.php
1 year ago
Bounce.php
6 days ago
BounceTaskSubscribersCleanup.php
1 month ago
BulkConfirmationEmailResend.php
2 months ago
ExportFilesCleanup.php
2 months ago
InactiveSubscribersMaintenance.php
2 weeks ago
LogCleanup.php
10 months ago
Mixpanel.php
9 months ago
NewsletterTemplateThumbnails.php
1 year ago
ReEngagementEmailsScheduler.php
1 year ago
Scheduler.php
2 months ago
SendingQueueBodyCleanup.php
2 months ago
SendingTaskSubscribersCleanup.php
2 months ago
SimpleWorker.php
1 year ago
StatisticsExport.php
2 months ago
SubscriberLimitNotificationWorker.php
2 months ago
SubscriberLinkTokens.php
1 year ago
SubscribersCountCacheRecalculation.php
2 weeks ago
SubscribersEngagementScore.php
4 days ago
SubscribersLastEngagement.php
2 months ago
SubscribersSegmentsCountSync.php
2 weeks ago
SubscribersStatsReport.php
1 year ago
Tracks.php
9 months ago
UnconfirmedSubscribersCleanup.php
2 months ago
UnsubscribeTokens.php
1 month ago
WooCommercePastOrders.php
1 year ago
WooCommerceSync.php
3 years ago
WorkersFactory.php
2 weeks ago
index.php
3 years ago
InactiveSubscribersMaintenance.php
149 lines
| 1 | <?php declare(strict_types = 1); |
| 2 | |
| 3 | namespace MailPoet\Cron\Workers; |
| 4 | |
| 5 | if (!defined('ABSPATH')) exit; |
| 6 | |
| 7 | |
| 8 | use MailPoet\Entities\ScheduledTaskEntity; |
| 9 | use MailPoet\Settings\SettingsController; |
| 10 | use MailPoet\Settings\TrackingConfig; |
| 11 | use MailPoet\Subscribers\InactiveSubscribersController; |
| 12 | use MailPoet\Subscribers\SubscribersEmailCountsController; |
| 13 | use MailPoet\Subscribers\SubscribersRepository; |
| 14 | use MailPoetVendor\Carbon\Carbon; |
| 15 | use MailPoetVendor\Doctrine\ORM\EntityManager; |
| 16 | |
| 17 | class InactiveSubscribersMaintenance extends SimpleWorker { |
| 18 | const TASK_TYPE = 'inactive_subscribers_maintenance'; |
| 19 | const BATCH_SIZE = 1000; |
| 20 | const SUPPORT_MULTIPLE_INSTANCES = false; |
| 21 | const LAST_EMAIL_COUNT_AT_SETTING = 'inactive_subscribers_maintenance_last_email_count_at'; |
| 22 | |
| 23 | /** @var SubscribersEmailCountsController */ |
| 24 | private $subscribersEmailCountsController; |
| 25 | |
| 26 | /** @var InactiveSubscribersController */ |
| 27 | private $inactiveSubscribersController; |
| 28 | |
| 29 | /** @var SubscribersRepository */ |
| 30 | private $subscribersRepository; |
| 31 | |
| 32 | /** @var SettingsController */ |
| 33 | private $settings; |
| 34 | |
| 35 | /** @var TrackingConfig */ |
| 36 | private $trackingConfig; |
| 37 | |
| 38 | /** @var EntityManager */ |
| 39 | private $entityManager; |
| 40 | |
| 41 | public function __construct( |
| 42 | SubscribersEmailCountsController $subscribersEmailCountsController, |
| 43 | InactiveSubscribersController $inactiveSubscribersController, |
| 44 | SubscribersRepository $subscribersRepository, |
| 45 | SettingsController $settings, |
| 46 | TrackingConfig $trackingConfig, |
| 47 | EntityManager $entityManager |
| 48 | ) { |
| 49 | $this->subscribersEmailCountsController = $subscribersEmailCountsController; |
| 50 | $this->inactiveSubscribersController = $inactiveSubscribersController; |
| 51 | $this->subscribersRepository = $subscribersRepository; |
| 52 | $this->settings = $settings; |
| 53 | $this->trackingConfig = $trackingConfig; |
| 54 | $this->entityManager = $entityManager; |
| 55 | parent::__construct(); |
| 56 | } |
| 57 | |
| 58 | public function processTaskStrategy(ScheduledTaskEntity $task, $timer) { |
| 59 | $meta = $task->getMeta(); |
| 60 | $meta = is_array($meta) ? $meta : []; |
| 61 | |
| 62 | // A partially counted run persists its cursor and frozen cutoff but writes the baseline only |
| 63 | // when it finishes. If the feature is disabled before it resumes, the early-return below |
| 64 | // completes the task with counting half-done. Drop the baseline in that case so the next |
| 65 | // enabled run does a full recount (which resets email_count) instead of incrementing the |
| 66 | // already-counted windows again. |
| 67 | $hasPartialEmailCountProgress = isset($meta['last_subscriber_id'], $meta['email_count_cutoff']); |
| 68 | |
| 69 | if (!$this->trackingConfig->isEmailTrackingEnabled()) { |
| 70 | if ($hasPartialEmailCountProgress) { |
| 71 | $this->settings->delete(self::LAST_EMAIL_COUNT_AT_SETTING); |
| 72 | } |
| 73 | $this->schedule(); |
| 74 | return true; |
| 75 | } |
| 76 | |
| 77 | $daysToInactive = (int)$this->settings->get('deactivate_subscriber_after_inactive_days'); |
| 78 | if ($daysToInactive === 0) { |
| 79 | if ($hasPartialEmailCountProgress) { |
| 80 | $this->settings->delete(self::LAST_EMAIL_COUNT_AT_SETTING); |
| 81 | } |
| 82 | $this->inactiveSubscribersController->reactivateInactiveSubscribers(); |
| 83 | $this->schedule(); |
| 84 | return true; |
| 85 | } |
| 86 | |
| 87 | // Freeze one cutoff for the whole run (persisted so resumes reuse it) so every window |
| 88 | // counts up to the same point and the stored baseline matches it exactly. Otherwise late |
| 89 | // cron (scheduled_at != now) or a multi-tick run would re-count or skip a sliver of tasks. |
| 90 | $countCutoff = isset($meta['email_count_cutoff']) && is_string($meta['email_count_cutoff']) |
| 91 | ? Carbon::parse($meta['email_count_cutoff']) |
| 92 | : new Carbon(); |
| 93 | $meta['email_count_cutoff'] = $countCutoff->format(\DateTimeInterface::ATOM); |
| 94 | |
| 95 | $dateFromLastRun = $this->getDateFromLastRun(); |
| 96 | $refreshCounts = $dateFromLastRun === null || $this->subscribersEmailCountsController->hasNewSendingTasksSince($dateFromLastRun, $countCutoff); |
| 97 | |
| 98 | $startId = isset($meta['last_subscriber_id']) ? (int)$meta['last_subscriber_id'] : 0; |
| 99 | |
| 100 | while (true) { |
| 101 | [$count, $endId] = $this->subscribersRepository->getNextIdWindow($startId, self::BATCH_SIZE); |
| 102 | if ($count === 0) { |
| 103 | break; |
| 104 | } |
| 105 | |
| 106 | // Count and advance the cursor atomically: the count increment is not idempotent, so a |
| 107 | // failure must not leave the count applied while the cursor still points at this window. |
| 108 | $this->entityManager->wrapInTransaction(function() use ($refreshCounts, $dateFromLastRun, $startId, $endId, $countCutoff, $task, &$meta): void { |
| 109 | if ($refreshCounts) { |
| 110 | $this->subscribersEmailCountsController->updateSubscribersEmailCounts($dateFromLastRun, $startId, $endId, $countCutoff); |
| 111 | } |
| 112 | $meta['last_subscriber_id'] = $endId + 1; |
| 113 | $task->setMeta($meta); |
| 114 | $this->scheduledTasksRepository->persist($task); |
| 115 | $this->scheduledTasksRepository->flush(); |
| 116 | }); |
| 117 | |
| 118 | // Marking is idempotent (recomputed from current state each pass), so it stays outside the |
| 119 | // transaction -- a marking failure cannot trigger a re-count of an already-counted window. |
| 120 | $this->inactiveSubscribersController->markInactiveSubscribers($daysToInactive, $startId, $endId); |
| 121 | $this->cronHelper->enforceExecutionLimit($timer); |
| 122 | $startId = $endId + 1; |
| 123 | } |
| 124 | |
| 125 | // Baseline = the frozen cutoff, matching exactly what was counted, so the next run resumes |
| 126 | // from the right point even if this run's cron was late or spanned several ticks. |
| 127 | $this->rememberLastEmailCountDate($countCutoff); |
| 128 | |
| 129 | while ($this->inactiveSubscribersController->markActiveSubscribers($daysToInactive, self::BATCH_SIZE) === self::BATCH_SIZE) { |
| 130 | $this->cronHelper->enforceExecutionLimit($timer); |
| 131 | } |
| 132 | |
| 133 | $this->schedule(); |
| 134 | return true; |
| 135 | } |
| 136 | |
| 137 | private function getDateFromLastRun(): ?\DateTimeInterface { |
| 138 | // Baseline is the last run that actually counted, not the last completed task -- runs |
| 139 | // while inactivity detection is off complete without counting and must not move it. On |
| 140 | // upgrade the migration seeds this setting from the last legacy email-count task. |
| 141 | $lastEmailCountAt = $this->settings->get(self::LAST_EMAIL_COUNT_AT_SETTING); |
| 142 | return is_string($lastEmailCountAt) && $lastEmailCountAt !== '' ? Carbon::parse($lastEmailCountAt) : null; |
| 143 | } |
| 144 | |
| 145 | private function rememberLastEmailCountDate(\DateTimeInterface $countCutoff): void { |
| 146 | $this->settings->set(self::LAST_EMAIL_COUNT_AT_SETTING, $countCutoff->format(\DateTimeInterface::ATOM)); |
| 147 | } |
| 148 | } |
| 149 |