diff --git a/mailpoet/lib/Automation/Integrations/MailPoet/Hooks/CreateAutomationRunHook.php b/mailpoet/lib/Automation/Integrations/MailPoet/Hooks/CreateAutomationRunHook.php index bd218042d2..fb6d6b974f 100644 --- a/mailpoet/lib/Automation/Integrations/MailPoet/Hooks/CreateAutomationRunHook.php +++ b/mailpoet/lib/Automation/Integrations/MailPoet/Hooks/CreateAutomationRunHook.php @@ -5,23 +5,20 @@ namespace MailPoet\Automation\Integrations\MailPoet\Hooks; use MailPoet\Automation\Engine\Data\StepRunArgs; use MailPoet\Automation\Engine\Hooks; use MailPoet\Automation\Engine\Storage\AutomationRunStorage; -use MailPoet\Automation\Engine\WordPress; use MailPoet\Automation\Integrations\MailPoet\Subjects\SubscriberSubject; +use MailPoet\Util\Security; +use MailPoet\WP\Functions as WPFunctions; class CreateAutomationRunHook { - - - /** @var WordPress */ - private $wp; - - private $automationRunStorage; + private AutomationRunStorage $automationRunStorage; + private WPFunctions $wp; public function __construct( - WordPress $wp, - AutomationRunStorage $automationRunStorage + AutomationRunStorage $automationRunStorage, + WPFunctions $wp ) { - $this->wp = $wp; $this->automationRunStorage = $automationRunStorage; + $this->wp = $wp; } public function init(): void { @@ -39,11 +36,29 @@ class CreateAutomationRunHook { return true; } - $subscriberSubject = $args->getAutomationRun()->getSubjects(SubscriberSubject::KEY); + $subscriberSubject = array_values($args->getAutomationRun()->getSubjects(SubscriberSubject::KEY))[0] ?? null; if (!$subscriberSubject) { return true; } - return $this->automationRunStorage->getCountByAutomationAndSubject($automation, current($subscriberSubject)) === 0; + // Use locking mechanism to minimize the risk of race conditions. + // WP transients don't provide atomic operations, so we can't guarantee + // race-condition safety with a 100% certainty, but we can significantly + // minimize the risk by generating and re-checking a unique lock value. + $key = sprintf('mailpoet:run-once-per-subscriber:[%s][%s]', $automation->getId(), $subscriberSubject->getHash()); + + // 1. If lock already exists, do not create automation run. + $value = $this->wp->getTransient($key); + if ($value) { + return false; + } + + // 2. If lock does not exist, create it with a unique value. + $value = Security::generateRandomString(16); + $this->wp->setTransient($key, $value, MINUTE_IN_SECONDS); + + // 3. If no automation run exist, ensure that the lock wasn't updated by another process. + $count = $this->automationRunStorage->getCountByAutomationAndSubject($automation, $subscriberSubject); + return $count === 0 && $this->wp->getTransient($key) === $value; } }