Cache total subscribers count, add tests

[MAILPOET-4416]
This commit is contained in:
Jan Jakes
2022-08-11 15:14:22 +02:00
committed by Veljko V
parent 7ce53adc33
commit 24cca1ba04
7 changed files with 186 additions and 11 deletions

View File

@@ -219,6 +219,7 @@ class WP {
$this->updateFirstNameIfMissing();
$this->insertUsersToSegment();
$this->removeOrphanedSubscribers();
$this->subscribersRepository->invalidateTotalSubscribersCache();
return true;
}

View File

@@ -188,6 +188,7 @@ class WooCommerce {
$this->updateGlobalStatus();
}
$this->subscribersRepository->invalidateTotalSubscribersCache();
return $lastCheckedOrderId;
}

View File

@@ -512,6 +512,8 @@ class Import {
}
}
if (empty($createdOrUpdatedSubscribers)) return null;
$this->subscriberRepository->invalidateTotalSubscribersCache();
$createdOrUpdatedSubscribersIds = array_column($createdOrUpdatedSubscribers, 'id');
if ($subscribersCustomFields) {
$this->createOrUpdateCustomFields(
@@ -588,7 +590,9 @@ class Import {
* @return array
*/
public function synchronizeWPUsers(array $wpUsers): array {
return array_map([$this->wpSegment, 'synchronizeUser'], $wpUsers);
$users = array_map([$this->wpSegment, 'synchronizeUser'], $wpUsers);
$this->subscriberRepository->invalidateTotalSubscribersCache();
return $users;
}
public function addSubscribersToSegments(array $subscribersIds, array $segmentsIds): void {

View File

@@ -7,6 +7,7 @@ use MailPoet\Entities\SegmentEntity;
use MailPoet\Entities\SubscriberCustomFieldEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Entities\SubscriberSegmentEntity;
use MailPoet\Util\License\Features\Subscribers;
use MailPoet\WP\Functions as WPFunctions;
use MailPoetVendor\Carbon\Carbon;
use MailPoetVendor\Carbon\CarbonImmutable;
@@ -41,10 +42,7 @@ class SubscribersRepository extends Repository {
return SubscriberEntity::class;
}
/**
* @return int
*/
public function getTotalSubscribers() {
public function getTotalSubscribers(): int {
$query = $this->entityManager
->createQueryBuilder()
->select('count(n.id)')
@@ -59,6 +57,10 @@ class SubscribersRepository extends Repository {
return (int)$query->getSingleScalarResult();
}
public function invalidateTotalSubscribersCache(): void {
$this->wp->deleteTransient(Subscribers::SUBSCRIBERS_COUNT_CACHE_KEY);
}
public function findBySegment(int $segmentId): array {
return $this->entityManager
->createQueryBuilder()
@@ -116,6 +118,7 @@ class SubscribersRepository extends Repository {
->setParameter('ids', $ids)
->getQuery()->execute();
$this->invalidateTotalSubscribersCache();
return count($ids);
}
@@ -135,6 +138,7 @@ class SubscribersRepository extends Repository {
->setParameter('ids', $ids)
->getQuery()->execute();
$this->invalidateTotalSubscribersCache();
return count($ids);
}
@@ -171,6 +175,7 @@ class SubscribersRepository extends Repository {
->getQuery()->execute();
});
$this->invalidateTotalSubscribersCache();
return $count;
}
@@ -282,6 +287,7 @@ class SubscribersRepository extends Repository {
->setParameter('ids', $ids)
->getQuery()->execute();
$this->invalidateTotalSubscribersCache();
return count($ids);
}

View File

@@ -5,6 +5,7 @@ namespace MailPoet\Util\License\Features;
use MailPoet\Services\Bridge;
use MailPoet\Settings\SettingsController;
use MailPoet\Subscribers\SubscribersRepository;
use MailPoet\WP\Functions as WPFunctions;
class Subscribers {
const SUBSCRIBERS_OLD_LIMIT = 2000;
@@ -18,6 +19,9 @@ class Subscribers {
const PREMIUM_EMAIL_VOLUME_LIMIT_SETTING_KEY = 'premium.premium_key_state.data.email_volume_limit';
const PREMIUM_EMAILS_SENT_SETTING_KEY = 'premium.premium_key_state.data.emails_sent';
const PREMIUM_SUPPORT_SETTING_KEY = 'premium.premium_key_state.data.support_tier';
const SUBSCRIBERS_COUNT_CACHE_KEY = 'mailpoet_subscribers_count';
const SUBSCRIBERS_COUNT_CACHE_EXPIRATION_MINUTES = 60;
const SUBSCRIBERS_COUNT_CACHE_MIN_VALUE = 1000;
/** @var SettingsController */
private $settings;
@@ -25,15 +29,20 @@ class Subscribers {
/** @var SubscribersRepository */
private $subscribersRepository;
/** @var WPFunctions */
private $wp;
public function __construct(
SettingsController $settings,
SubscribersRepository $subscribersRepository
SubscribersRepository $subscribersRepository,
WPFunctions $wp
) {
$this->settings = $settings;
$this->subscribersRepository = $subscribersRepository;
$this->wp = $wp;
}
public function check() {
public function check(): bool {
$limit = $this->getSubscribersLimit();
if ($limit === false) return false;
$subscribersCount = $this->getSubscribersCount();
@@ -49,11 +58,21 @@ class Subscribers {
return $emailsSent > $emailVolumeLimit;
}
public function getSubscribersCount() {
return $this->subscribersRepository->getTotalSubscribers();
public function getSubscribersCount(): int {
$count = $this->wp->getTransient(self::SUBSCRIBERS_COUNT_CACHE_KEY);
if (is_numeric($count)) {
return (int)$count;
}
$count = $this->subscribersRepository->getTotalSubscribers();
// cache only when number of subscribers exceeds minimum value
if ($count > self::SUBSCRIBERS_COUNT_CACHE_MIN_VALUE) {
$this->wp->setTransient(self::SUBSCRIBERS_COUNT_CACHE_KEY, $count, self::SUBSCRIBERS_COUNT_CACHE_EXPIRATION_MINUTES * 60);
}
return $count;
}
public function hasValidApiKey() {
public function hasValidApiKey(): bool {
return $this->hasValidMssKey() || $this->hasValidPremiumKey();
}

View File

@@ -0,0 +1,137 @@
<?php declare(strict_types = 1);
namespace MailPoet\Subscribers;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\WP\Functions as WPFunctions;
use MailPoet\Util\License\Features\Subscribers;
use Codeception\Stub;
class SubscribersTest extends \MailPoetTest {
/** @var Subscribers */
private $subscribers;
/** @var SubscribersRepository */
private $subscribersRepository;
/** @var WPFunctions */
private $wp;
public function _before() {
parent::_before();
$this->subscribers = $this->diContainer->get(Subscribers::class);
$this->subscribersRepository = $this->diContainer->get(SubscribersRepository::class);
$this->wp = $this->diContainer->get(WPFunctions::class);
$this->wp->deleteTransient(Subscribers::SUBSCRIBERS_COUNT_CACHE_KEY);
}
public function testItComputesSubscribersCount() {
// no subscribers
$count = $this->subscribers->getSubscribersCount();
expect($count)->same(0);
// create some subscribers (unconfirmed, subscribed, and inactive should be counted)
$this->createSubscriber('unconfirmed@fake.loc', SubscriberEntity::STATUS_UNCONFIRMED);
$this->createSubscriber('subscribed@fake.loc', SubscriberEntity::STATUS_SUBSCRIBED);
$this->createSubscriber('inactive@fake.loc', SubscriberEntity::STATUS_INACTIVE);
// check count
$count = $this->subscribers->getSubscribersCount();
expect($count)->same(3);
// add more subscribers (bounced, unsubscribed, and trashed should not be counted)
$this->createSubscriber('bounced@fake.loc', SubscriberEntity::STATUS_BOUNCED);
$this->createSubscriber('unsubscribed@fake.loc', SubscriberEntity::STATUS_UNSUBSCRIBED);
$trashed = $this->createSubscriber('trashed@fake.loc', SubscriberEntity::STATUS_SUBSCRIBED);
$trashed->setDeletedAt(new \DateTimeImmutable());
$this->subscribersRepository->flush();
// check count
$count = $this->subscribers->getSubscribersCount();
expect($count)->same(3);
}
public function testItDoesntCacheSubscribersCountForLowValues() {
// no subscribers
$count = $this->subscribers->getSubscribersCount();
expect($count)->same(0);
// add subscriber
$this->createSubscriber('one@fake.loc', SubscriberEntity::STATUS_SUBSCRIBED);
$count = $this->subscribers->getSubscribersCount();
expect($count)->same(1);
// add another subscriber (count updates without cache purging)
$this->createSubscriber('two@fake.loc', SubscriberEntity::STATUS_SUBSCRIBED);
$count = $this->subscribers->getSubscribersCount();
expect($count)->same(2);
}
public function testItCachesSubscribersCountForHighValues() {
$subscribers = $this->getServiceWithOverrides(Subscribers::class, [
'subscribersRepository' => Stub::make(SubscribersRepository::class, [
'getTotalSubscribers' => 123456,
]),
]);
$count = $subscribers->getSubscribersCount();
expect($count)->same(123456);
$subscribers = $this->getServiceWithOverrides(Subscribers::class, [
'subscribersRepository' => Stub::make(SubscribersRepository::class, [
'getTotalSubscribers' => 999999,
]),
]);
// check count (cached value)
$count = $subscribers->getSubscribersCount();
expect($count)->same(123456);
// check count (uncached value)
$this->wp->deleteTransient(Subscribers::SUBSCRIBERS_COUNT_CACHE_KEY);
$count = $subscribers->getSubscribersCount();
expect($count)->same(999999);
}
public function testItInvalidatesSubscribersCountCache() {
$subscribers = $this->getServiceWithOverrides(Subscribers::class, [
'subscribersRepository' => Stub::make(SubscribersRepository::class, [
'getTotalSubscribers' => 123456,
]),
]);
$subscribers->getSubscribersCount();
$subscribers = $this->getServiceWithOverrides(Subscribers::class, [
'subscribersRepository' => Stub::make(SubscribersRepository::class, [
'getTotalSubscribers' => 999999,
]),
]);
// check count (cached value)
$count = $subscribers->getSubscribersCount();
expect($count)->same(123456);
// modify timestamp, check count (-> uncached value)
$this->wp->updateOption(
'_transient_timeout_' . Subscribers::SUBSCRIBERS_COUNT_CACHE_KEY,
$this->wp->currentTime('timestamp') - 1
);
$count = $subscribers->getSubscribersCount();
expect($count)->same(999999);
}
public function _after() {
parent::_after();
$this->truncateEntity(SubscriberEntity::class);
$this->wp->deleteTransient(Subscribers::SUBSCRIBERS_COUNT_CACHE_KEY);
}
private function createSubscriber(string $email, string $status): SubscriberEntity {
$subscriber = new SubscriberEntity();
$subscriber->setEmail($email);
$subscriber->setStatus($status);
$this->subscribersRepository->persist($subscriber);
$this->subscribersRepository->flush();
return $subscriber;
}
}

View File

@@ -6,6 +6,7 @@ use Codeception\Util\Stub;
use MailPoet\Settings\SettingsController;
use MailPoet\Subscribers\SubscribersRepository;
use MailPoet\Util\License\Features\Subscribers as SubscribersFeature;
use MailPoet\WP\Functions as WPFunctions;
class SubscribersTest extends \MailPoetUnitTest {
public function testCheckReturnsTrueIfOldUserReachedLimit() {
@@ -178,12 +179,18 @@ class SubscribersTest extends \MailPoetUnitTest {
if ($name === SubscribersFeature::PREMIUM_SUPPORT_SETTING_KEY) return isset($specs['support_tier']) ? $specs['support_tier'] : 'free';
},
]);
$subscribersRepository = Stub::make(SubscribersRepository::class, [
'getTotalSubscribers' => function() use($specs) {
return $specs['subscribers_count'];
},
]);
return new SubscribersFeature($settings, $subscribersRepository);
$wpFunctions = Stub::make(WPFunctions::class, [
'getTransient' => false,
'setTransient' => false,
]);
return new SubscribersFeature($settings, $subscribersRepository, $wpFunctions);
}
}