DaemonRun.php
128 lines
| 1 | <?php declare(strict_types = 1); |
| 2 | |
| 3 | namespace MailPoet\Cron\ActionScheduler\Actions; |
| 4 | |
| 5 | if (!defined('ABSPATH')) exit; |
| 6 | |
| 7 | |
| 8 | use MailPoet\Cron\ActionScheduler\ActionScheduler; |
| 9 | use MailPoet\Cron\ActionScheduler\RemoteExecutorHandler; |
| 10 | use MailPoet\Cron\CronHelper; |
| 11 | use MailPoet\Cron\Daemon; |
| 12 | use MailPoet\Cron\Triggers\WordPress; |
| 13 | use MailPoet\Logging\LoggerFactory; |
| 14 | use MailPoet\WP\Functions as WPFunctions; |
| 15 | |
| 16 | class DaemonRun { |
| 17 | const NAME = 'mailpoet/cron/daemon-run'; |
| 18 | const EXECUTION_LIMIT_MARGIN = 10; // 10 seconds |
| 19 | const SHORT_DURATION_THRESHOLD = 2; // 2 seconds |
| 20 | |
| 21 | /** @var WPFunctions */ |
| 22 | private $wp; |
| 23 | |
| 24 | /** @var Daemon */ |
| 25 | private $daemon; |
| 26 | |
| 27 | /** @var WordPress */ |
| 28 | private $wordpressTrigger; |
| 29 | |
| 30 | /** @var CronHelper */ |
| 31 | private $cronHelper; |
| 32 | |
| 33 | /** @var RemoteExecutorHandler */ |
| 34 | private $remoteExecutorHandler; |
| 35 | |
| 36 | /** @var ActionScheduler */ |
| 37 | private $actionScheduler; |
| 38 | |
| 39 | /** @var LoggerFactory */ |
| 40 | private $loggerFactory; |
| 41 | |
| 42 | /** |
| 43 | * Default 20 seconds |
| 44 | * @var float |
| 45 | */ |
| 46 | private $remainingExecutionLimit = 20; |
| 47 | |
| 48 | /** @var int */ |
| 49 | private $lastRunDuration = 0; |
| 50 | |
| 51 | public function __construct( |
| 52 | WPFunctions $wp, |
| 53 | Daemon $daemon, |
| 54 | WordPress $wordpressTrigger, |
| 55 | CronHelper $cronHelper, |
| 56 | RemoteExecutorHandler $remoteExecutorHandler, |
| 57 | ActionScheduler $actionScheduler, |
| 58 | LoggerFactory $loggerFactory |
| 59 | ) { |
| 60 | $this->wp = $wp; |
| 61 | $this->daemon = $daemon; |
| 62 | $this->wordpressTrigger = $wordpressTrigger; |
| 63 | $this->cronHelper = $cronHelper; |
| 64 | $this->remoteExecutorHandler = $remoteExecutorHandler; |
| 65 | $this->actionScheduler = $actionScheduler; |
| 66 | $this->loggerFactory = $loggerFactory; |
| 67 | } |
| 68 | |
| 69 | public function init(): void { |
| 70 | $this->wp->addAction(self::NAME, [$this, 'process']); |
| 71 | $this->wp->addFilter('action_scheduler_maximum_execution_time_likely_to_be_exceeded', [$this, 'storeRemainingExecutionLimit'], 10, 5); |
| 72 | } |
| 73 | |
| 74 | /** |
| 75 | * Run daemon that processes scheduled tasks for limited time |
| 76 | */ |
| 77 | public function process(): void { |
| 78 | $this->wp->addAction('action_scheduler_after_process_queue', [$this, 'afterProcess']); |
| 79 | $this->wp->addAction('mailpoet_cron_get_execution_limit', [$this, 'getDaemonExecutionLimit']); |
| 80 | $this->lastRunDuration = 0; |
| 81 | $startTime = time(); |
| 82 | $this->daemon->run($this->cronHelper->createDaemon($this->cronHelper->createToken())); |
| 83 | $this->lastRunDuration = time() - $startTime; |
| 84 | } |
| 85 | |
| 86 | /** |
| 87 | * Callback for setting the remaining execution time for the cron daemon (MailPoet\Cron\Daemon) |
| 88 | */ |
| 89 | public function getDaemonExecutionLimit(): float { |
| 90 | return $this->remainingExecutionLimit; |
| 91 | } |
| 92 | |
| 93 | /** |
| 94 | * After Action Scheduler finishes its work we need to check if there is more work and in case there is we trigger additional runner. |
| 95 | */ |
| 96 | public function afterProcess(): void { |
| 97 | $hasJobsToDo = $this->wordpressTrigger->checkExecutionRequirements(); |
| 98 | if (!$hasJobsToDo) { |
| 99 | return; |
| 100 | } |
| 101 | // The $lastDurationWasTooShort check prevents scheduling the next immediate action in case the last run was suspiciously short. |
| 102 | // If there was still some execution time left, the daemon should have been continued. |
| 103 | $lastDurationWasTooShort = ($this->lastRunDuration < self::SHORT_DURATION_THRESHOLD) && ($this->remainingExecutionLimit > 0); |
| 104 | if ($lastDurationWasTooShort) { |
| 105 | $this->loggerFactory->getLogger(LoggerFactory::TOPIC_CRON)->info('Daemon run ended too early!', [ |
| 106 | 'duration' => $this->lastRunDuration, |
| 107 | 'remainingLimit' => $this->remainingExecutionLimit, |
| 108 | ]); |
| 109 | return; |
| 110 | } |
| 111 | $this->actionScheduler->scheduleImmediateSingleAction(self::NAME); |
| 112 | // Chaining async requests can crash MySQL. A brief sleep call in PHP prevents that. |
| 113 | // @see https://github.com/woocommerce/action-scheduler/blob/6633378283d33746eec7314586783f58deee5375/classes/ActionScheduler_AsyncRequest_QueueRunner.php#L91-L96 |
| 114 | sleep(2); |
| 115 | $this->remoteExecutorHandler->triggerExecutor(); |
| 116 | } |
| 117 | |
| 118 | /** |
| 119 | * This method is hooked into action_scheduler_maximum_execution_time_likely_to_be_exceeded |
| 120 | * It checks how much execution time is left for the daemon to run |
| 121 | */ |
| 122 | public function storeRemainingExecutionLimit($likelyExceeded, $runner, $processedActions, $executionTime, $maxExecutionTime): bool { |
| 123 | $newLimit = ($maxExecutionTime - $executionTime) - self::EXECUTION_LIMIT_MARGIN; |
| 124 | $this->remainingExecutionLimit = max($newLimit, 0); |
| 125 | return (bool)$likelyExceeded; |
| 126 | } |
| 127 | } |
| 128 |