init
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Cron\ActionScheduler;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\WP\Functions as WPFunctions;
|
||||
|
||||
class ActionScheduler {
|
||||
public const GROUP_ID = 'mailpoet-cron';
|
||||
|
||||
/** @var WPFunctions */
|
||||
private $wp;
|
||||
|
||||
public function __construct(
|
||||
WPFunctions $wp
|
||||
) {
|
||||
$this->wp = $wp;
|
||||
}
|
||||
|
||||
public function scheduleRecurringAction(int $timestamp, int $interval_in_seconds, string $hook, array $args = [], bool $unique = true): int {
|
||||
$result = as_schedule_recurring_action($timestamp, $interval_in_seconds, $hook, $args, self::GROUP_ID, $unique);
|
||||
return is_int($result) ? $result : 0;
|
||||
}
|
||||
|
||||
public function scheduleImmediateSingleAction(string $hook, array $args = [], bool $unique = true): int {
|
||||
$result = as_schedule_single_action($this->wp->currentTime('timestamp', true), $hook, $args, self::GROUP_ID, $unique);
|
||||
return is_int($result) ? $result : 0;
|
||||
}
|
||||
|
||||
public function unscheduleAction(string $hook, array $args = []): ?int {
|
||||
$id = as_unschedule_action($hook, $args, self::GROUP_ID);
|
||||
return $id !== null ? intval($id) : null;
|
||||
}
|
||||
|
||||
public function unscheduleAllCronActions(): void {
|
||||
// Passing only group to unschedule all by group
|
||||
as_unschedule_all_actions('', [], self::GROUP_ID);
|
||||
}
|
||||
|
||||
public function hasScheduledAction(string $hook, array $args = []): bool {
|
||||
return as_has_scheduled_action($hook, $args, self::GROUP_ID);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Cron\ActionScheduler\Actions;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Cron\ActionScheduler\ActionScheduler;
|
||||
use MailPoet\Cron\ActionScheduler\RemoteExecutorHandler;
|
||||
use MailPoet\Cron\CronHelper;
|
||||
use MailPoet\Cron\Daemon;
|
||||
use MailPoet\Cron\Triggers\WordPress;
|
||||
use MailPoet\Logging\LoggerFactory;
|
||||
use MailPoet\WP\Functions as WPFunctions;
|
||||
|
||||
class DaemonRun {
|
||||
const NAME = 'mailpoet/cron/daemon-run';
|
||||
const EXECUTION_LIMIT_MARGIN = 10; // 10 seconds
|
||||
const SHORT_DURATION_THRESHOLD = 2; // 2 seconds
|
||||
|
||||
/** @var WPFunctions */
|
||||
private $wp;
|
||||
|
||||
/** @var Daemon */
|
||||
private $daemon;
|
||||
|
||||
/** @var WordPress */
|
||||
private $wordpressTrigger;
|
||||
|
||||
/** @var CronHelper */
|
||||
private $cronHelper;
|
||||
|
||||
/** @var RemoteExecutorHandler */
|
||||
private $remoteExecutorHandler;
|
||||
|
||||
/** @var ActionScheduler */
|
||||
private $actionScheduler;
|
||||
|
||||
/** @var LoggerFactory */
|
||||
private $loggerFactory;
|
||||
|
||||
/**
|
||||
* Default 20 seconds
|
||||
* @var float
|
||||
*/
|
||||
private $remainingExecutionLimit = 20;
|
||||
|
||||
/** @var int */
|
||||
private $lastRunDuration = 0;
|
||||
|
||||
public function __construct(
|
||||
WPFunctions $wp,
|
||||
Daemon $daemon,
|
||||
WordPress $wordpressTrigger,
|
||||
CronHelper $cronHelper,
|
||||
RemoteExecutorHandler $remoteExecutorHandler,
|
||||
ActionScheduler $actionScheduler,
|
||||
LoggerFactory $loggerFactory
|
||||
) {
|
||||
$this->wp = $wp;
|
||||
$this->daemon = $daemon;
|
||||
$this->wordpressTrigger = $wordpressTrigger;
|
||||
$this->cronHelper = $cronHelper;
|
||||
$this->remoteExecutorHandler = $remoteExecutorHandler;
|
||||
$this->actionScheduler = $actionScheduler;
|
||||
$this->loggerFactory = $loggerFactory;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
$this->wp->addAction(self::NAME, [$this, 'process']);
|
||||
$this->wp->addFilter('action_scheduler_maximum_execution_time_likely_to_be_exceeded', [$this, 'storeRemainingExecutionLimit'], 10, 5);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run daemon that processes scheduled tasks for limited time
|
||||
*/
|
||||
public function process(): void {
|
||||
$this->wp->addAction('action_scheduler_after_process_queue', [$this, 'afterProcess']);
|
||||
$this->wp->addAction('mailpoet_cron_get_execution_limit', [$this, 'getDaemonExecutionLimit']);
|
||||
$this->lastRunDuration = 0;
|
||||
$startTime = time();
|
||||
$this->daemon->run($this->cronHelper->createDaemon($this->cronHelper->createToken()));
|
||||
$this->lastRunDuration = time() - $startTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for setting the remaining execution time for the cron daemon (MailPoet\Cron\Daemon)
|
||||
*/
|
||||
public function getDaemonExecutionLimit(): float {
|
||||
return $this->remainingExecutionLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* After Action Scheduler finishes its work we need to check if there is more work and in case there is we trigger additional runner.
|
||||
*/
|
||||
public function afterProcess(): void {
|
||||
$hasJobsToDo = $this->wordpressTrigger->checkExecutionRequirements();
|
||||
if (!$hasJobsToDo) {
|
||||
return;
|
||||
}
|
||||
// The $lastDurationWasTooShort check prevents scheduling the next immediate action in case the last run was suspiciously short.
|
||||
// If there was still some execution time left, the daemon should have been continued.
|
||||
$lastDurationWasTooShort = ($this->lastRunDuration < self::SHORT_DURATION_THRESHOLD) && ($this->remainingExecutionLimit > 0);
|
||||
if ($lastDurationWasTooShort) {
|
||||
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_CRON)->info('Daemon run ended too early!', [
|
||||
'duration' => $this->lastRunDuration,
|
||||
'remainingLimit' => $this->remainingExecutionLimit,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
$this->actionScheduler->scheduleImmediateSingleAction(self::NAME);
|
||||
// Chaining async requests can crash MySQL. A brief sleep call in PHP prevents that.
|
||||
// @see https://github.com/woocommerce/action-scheduler/blob/6633378283d33746eec7314586783f58deee5375/classes/ActionScheduler_AsyncRequest_QueueRunner.php#L91-L96
|
||||
sleep(2);
|
||||
$this->remoteExecutorHandler->triggerExecutor();
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is hooked into action_scheduler_maximum_execution_time_likely_to_be_exceeded
|
||||
* It checks how much execution time is left for the daemon to run
|
||||
*/
|
||||
public function storeRemainingExecutionLimit($likelyExceeded, $runner, $processedActions, $executionTime, $maxExecutionTime): bool {
|
||||
$newLimit = ($maxExecutionTime - $executionTime) - self::EXECUTION_LIMIT_MARGIN;
|
||||
$this->remainingExecutionLimit = max($newLimit, 0);
|
||||
return (bool)$likelyExceeded;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Cron\ActionScheduler\Actions;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Cron\ActionScheduler\ActionScheduler;
|
||||
use MailPoet\Cron\ActionScheduler\RemoteExecutorHandler;
|
||||
use MailPoet\Cron\Triggers\WordPress;
|
||||
use MailPoet\WP\Functions as WPFunctions;
|
||||
|
||||
class DaemonTrigger {
|
||||
const NAME = 'mailpoet/cron/daemon-trigger';
|
||||
const TRIGGER_RUN_INTERVAL = 120; // 2 minutes
|
||||
|
||||
/** @var WPFunctions */
|
||||
private $wp;
|
||||
|
||||
/** @var WordPress */
|
||||
private $wordpressTrigger;
|
||||
|
||||
/** @var RemoteExecutorHandler */
|
||||
private $remoteExecutorHandler;
|
||||
|
||||
/** @var ActionScheduler */
|
||||
private $actionScheduler;
|
||||
|
||||
public function __construct(
|
||||
WPFunctions $wp,
|
||||
WordPress $wordpressTrigger,
|
||||
RemoteExecutorHandler $remoteExecutorHandler,
|
||||
ActionScheduler $actionScheduler
|
||||
) {
|
||||
$this->wp = $wp;
|
||||
$this->wordpressTrigger = $wordpressTrigger;
|
||||
$this->remoteExecutorHandler = $remoteExecutorHandler;
|
||||
$this->actionScheduler = $actionScheduler;
|
||||
}
|
||||
|
||||
public function init() {
|
||||
$this->wp->addAction(self::NAME, [$this, 'process']);
|
||||
if (!$this->actionScheduler->hasScheduledAction(self::NAME)) {
|
||||
$this->actionScheduler->scheduleRecurringAction($this->wp->currentTime('timestamp', true), self::TRIGGER_RUN_INTERVAL, self::NAME);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* It checks if there are scheduled tasks to execute.
|
||||
* In case there are tasks to do, it schedules a daemon-run action.
|
||||
*/
|
||||
public function process(): void {
|
||||
$hasJobsToDo = $this->wordpressTrigger->checkExecutionRequirements();
|
||||
if (!$hasJobsToDo) {
|
||||
$this->actionScheduler->unscheduleAction(DaemonRun::NAME);
|
||||
return;
|
||||
}
|
||||
if ($this->actionScheduler->hasScheduledAction(DaemonRun::NAME)) {
|
||||
return;
|
||||
}
|
||||
// Schedule immediate action for execution of the daemon
|
||||
$this->actionScheduler->scheduleImmediateSingleAction(DaemonRun::NAME);
|
||||
$this->remoteExecutorHandler->triggerExecutor();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Cron\ActionScheduler;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Util\Helpers;
|
||||
use MailPoet\WP\Functions as WPFunctions;
|
||||
|
||||
class RemoteExecutorHandler {
|
||||
const AJAX_ACTION_NAME = 'mailpoet-cron-action-scheduler-run';
|
||||
|
||||
/** @var WPFunctions */
|
||||
private $wp;
|
||||
|
||||
public function __construct(
|
||||
WPFunctions $wp
|
||||
) {
|
||||
$this->wp = $wp;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
$this->wp->addAction('wp_ajax_nopriv_' . self::AJAX_ACTION_NAME, [$this, 'runActionScheduler'], 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to spawn Action Scheduler runner via ajax request
|
||||
* @see https://actionscheduler.org/perf/#increasing-initialisation-rate-of-runners
|
||||
*/
|
||||
public function triggerExecutor(): void {
|
||||
$this->wp->addFilter('https_local_ssl_verify', '__return_false', 100);
|
||||
$this->wp->wpRemotePost($this->wp->adminUrl('admin-ajax.php'), [
|
||||
'method' => 'POST',
|
||||
'timeout' => 5,
|
||||
'redirection' => 5,
|
||||
'httpversion' => '1.0',
|
||||
'blocking' => false,
|
||||
'headers' => [],
|
||||
'body' => [
|
||||
'action' => self::AJAX_ACTION_NAME,
|
||||
],
|
||||
'cookies' => [],
|
||||
]);
|
||||
}
|
||||
|
||||
public function runActionScheduler(): void {
|
||||
try {
|
||||
$this->wp->addFilter('action_scheduler_queue_runner_concurrent_batches', [$this, 'ensureConcurrency']);
|
||||
\ActionScheduler_QueueRunner::instance()->run();
|
||||
wp_die();
|
||||
} catch (\Exception $e) {
|
||||
$mySqlGoneAwayMessage = Helpers::mySqlGoneAwayExceptionHandler($e);
|
||||
if ($mySqlGoneAwayMessage) {
|
||||
throw new \Exception($mySqlGoneAwayMessage, 0, $e);
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When triggering new runner at the end of a runner execution
|
||||
* we need to make sure the concurrency allows more one runner.
|
||||
*/
|
||||
public function ensureConcurrency(int $concurrency): int {
|
||||
return ($concurrency) < 2 ? 2 : $concurrency;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
Reference in New Issue
Block a user