| 1 |
<?php |
| 2 |
|
| 3 |
/** |
| 4 |
* Class ActionScheduler_QueueRunner |
| 5 |
*/ |
| 6 |
class ActionScheduler_QueueRunner extends ActionScheduler_Abstract_QueueRunner { |
| 7 |
const WP_CRON_HOOK = 'action_scheduler_run_queue'; |
| 8 |
|
| 9 |
const WP_CRON_SCHEDULE = 'every_minute'; |
| 10 |
|
| 11 |
/** @var ActionScheduler_AsyncRequest_QueueRunner */ |
| 12 |
protected $async_request; |
| 13 |
|
| 14 |
/** @var ActionScheduler_QueueRunner */ |
| 15 |
private static $runner = null; |
| 16 |
|
| 17 |
/** @var int */ |
| 18 |
private $processed_actions_count = 0; |
| 19 |
|
| 20 |
/** |
| 21 |
* @return ActionScheduler_QueueRunner |
| 22 |
* @codeCoverageIgnore |
| 23 |
*/ |
| 24 |
public static function instance() { |
| 25 |
if ( empty(self::$runner) ) { |
| 26 |
$class = apply_filters('action_scheduler_queue_runner_class', 'ActionScheduler_QueueRunner'); |
| 27 |
self::$runner = new $class(); |
| 28 |
} |
| 29 |
return self::$runner; |
| 30 |
} |
| 31 |
|
| 32 |
/** |
| 33 |
* ActionScheduler_QueueRunner constructor. |
| 34 |
* |
| 35 |
* @param ActionScheduler_Store $store |
| 36 |
* @param ActionScheduler_FatalErrorMonitor $monitor |
| 37 |
* @param ActionScheduler_QueueCleaner $cleaner |
| 38 |
*/ |
| 39 |
public function __construct( ActionScheduler_Store $store = null, ActionScheduler_FatalErrorMonitor $monitor = null, ActionScheduler_QueueCleaner $cleaner = null, ActionScheduler_AsyncRequest_QueueRunner $async_request = null ) { |
| 40 |
parent::__construct( $store, $monitor, $cleaner ); |
| 41 |
|
| 42 |
if ( is_null( $async_request ) ) { |
| 43 |
$async_request = new ActionScheduler_AsyncRequest_QueueRunner( $this->store ); |
| 44 |
} |
| 45 |
|
| 46 |
$this->async_request = $async_request; |
| 47 |
} |
| 48 |
|
| 49 |
/** |
| 50 |
* @codeCoverageIgnore |
| 51 |
*/ |
| 52 |
public function init() { |
| 53 |
|
| 54 |
add_filter( 'cron_schedules', array( self::instance(), 'add_wp_cron_schedule' ) ); |
| 55 |
|
| 56 |
// Check for and remove any WP Cron hook scheduled by Action Scheduler < 3.0.0, which didn't include the $context param |
| 57 |
$next_timestamp = wp_next_scheduled( self::WP_CRON_HOOK ); |
| 58 |
if ( $next_timestamp ) { |
| 59 |
wp_unschedule_event( $next_timestamp, self::WP_CRON_HOOK ); |
| 60 |
} |
| 61 |
|
| 62 |
$cron_context = array( 'WP Cron' ); |
| 63 |
|
| 64 |
if ( ! wp_next_scheduled( self::WP_CRON_HOOK, $cron_context ) ) { |
| 65 |
$schedule = apply_filters( 'action_scheduler_run_schedule', self::WP_CRON_SCHEDULE ); |
| 66 |
wp_schedule_event( time(), $schedule, self::WP_CRON_HOOK, $cron_context ); |
| 67 |
} |
| 68 |
|
| 69 |
add_action( self::WP_CRON_HOOK, array( self::instance(), 'run' ) ); |
| 70 |
$this->hook_dispatch_async_request(); |
| 71 |
} |
| 72 |
|
| 73 |
/** |
| 74 |
* Hook check for dispatching an async request. |
| 75 |
*/ |
| 76 |
public function hook_dispatch_async_request() { |
| 77 |
add_action( 'shutdown', array( $this, 'maybe_dispatch_async_request' ) ); |
| 78 |
} |
| 79 |
|
| 80 |
/** |
| 81 |
* Unhook check for dispatching an async request. |
| 82 |
*/ |
| 83 |
public function unhook_dispatch_async_request() { |
| 84 |
remove_action( 'shutdown', array( $this, 'maybe_dispatch_async_request' ) ); |
| 85 |
} |
| 86 |
|
| 87 |
/** |
| 88 |
* Check if we should dispatch an async request to process actions. |
| 89 |
* |
| 90 |
* This method is attached to 'shutdown', so is called frequently. To avoid slowing down |
| 91 |
* the site, it mitigates the work performed in each request by: |
| 92 |
* 1. checking if it's in the admin context and then |
| 93 |
* 2. haven't run on the 'shutdown' hook within the lock time (60 seconds by default) |
| 94 |
* 3. haven't exceeded the number of allowed batches. |
| 95 |
* |
| 96 |
* The order of these checks is important, because they run from a check on a value: |
| 97 |
* 1. in memory - is_admin() maps to $GLOBALS or the WP_ADMIN constant |
| 98 |
* 2. in memory - transients use autoloaded options by default |
| 99 |
* 3. from a database query - has_maximum_concurrent_batches() run the query |
| 100 |
* $this->store->get_claim_count() to find the current number of claims in the DB. |
| 101 |
* |
| 102 |
* If all of these conditions are met, then we request an async runner check whether it |
| 103 |
* should dispatch a request to process pending actions. |
| 104 |
*/ |
| 105 |
public function maybe_dispatch_async_request() { |
| 106 |
if ( is_admin() && ! ActionScheduler::lock()->is_locked( 'async-request-runner' ) ) { |
| 107 |
// Only start an async queue at most once every 60 seconds |
| 108 |
ActionScheduler::lock()->set( 'async-request-runner' ); |
| 109 |
$this->async_request->maybe_dispatch(); |
| 110 |
} |
| 111 |
} |
| 112 |
|
| 113 |
/** |
| 114 |
* Process actions in the queue. Attached to self::WP_CRON_HOOK i.e. 'action_scheduler_run_queue' |
| 115 |
* |
| 116 |
* The $context param of this method defaults to 'WP Cron', because prior to Action Scheduler 3.0.0 |
| 117 |
* that was the only context in which this method was run, and the self::WP_CRON_HOOK hook had no context |
| 118 |
* passed along with it. New code calling this method directly, or by triggering the self::WP_CRON_HOOK, |
| 119 |
* should set a context as the first parameter. For an example of this, refer to the code seen in |
| 120 |
* @see ActionScheduler_AsyncRequest_QueueRunner::handle() |
| 121 |
* |
| 122 |
* @param string $context Optional identifer for the context in which this action is being processed, e.g. 'WP CLI' or 'WP Cron' |
| 123 |
* Generally, this should be capitalised and not localised as it's a proper noun. |
| 124 |
* @return int The number of actions processed. |
| 125 |
*/ |
| 126 |
public function run( $context = 'WP Cron' ) { |
| 127 |
ActionScheduler_Compatibility::raise_memory_limit(); |
| 128 |
ActionScheduler_Compatibility::raise_time_limit( $this->get_time_limit() ); |
| 129 |
do_action( 'action_scheduler_before_process_queue' ); |
| 130 |
$this->run_cleanup(); |
| 131 |
|
| 132 |
$this->processed_actions_count = 0; |
| 133 |
if ( false === $this->has_maximum_concurrent_batches() ) { |
| 134 |
$batch_size = apply_filters( 'action_scheduler_queue_runner_batch_size', 25 ); |
| 135 |
do { |
| 136 |
$processed_actions_in_batch = $this->do_batch( $batch_size, $context ); |
| 137 |
$this->processed_actions_count += $processed_actions_in_batch; |
| 138 |
} while ( $processed_actions_in_batch > 0 && ! $this->batch_limits_exceeded( $this->processed_actions_count ) ); // keep going until we run out of actions, time, or memory |
| 139 |
} |
| 140 |
|
| 141 |
do_action( 'action_scheduler_after_process_queue' ); |
| 142 |
return $this->processed_actions_count; |
| 143 |
} |
| 144 |
|
| 145 |
/** |
| 146 |
* Process a batch of actions pending in the queue. |
| 147 |
* |
| 148 |
* Actions are processed by claiming a set of pending actions then processing each one until either the batch |
| 149 |
* size is completed, or memory or time limits are reached, defined by @see $this->batch_limits_exceeded(). |
| 150 |
* |
| 151 |
* @param int $size The maximum number of actions to process in the batch. |
| 152 |
* @param string $context Optional identifer for the context in which this action is being processed, e.g. 'WP CLI' or 'WP Cron' |
| 153 |
* Generally, this should be capitalised and not localised as it's a proper noun. |
| 154 |
* @return int The number of actions processed. |
| 155 |
*/ |
| 156 |
protected function do_batch( $size = 100, $context = '' ) { |
| 157 |
$claim = $this->store->stake_claim($size); |
| 158 |
$this->monitor->attach($claim); |
| 159 |
$processed_actions = 0; |
| 160 |
|
| 161 |
foreach ( $claim->get_actions() as $action_id ) { |
| 162 |
// bail if we lost the claim |
| 163 |
if ( ! in_array( $action_id, $this->store->find_actions_by_claim_id( $claim->get_id() ) ) ) { |
| 164 |
break; |
| 165 |
} |
| 166 |
$this->process_action( $action_id, $context ); |
| 167 |
$processed_actions++; |
| 168 |
|
| 169 |
if ( $this->batch_limits_exceeded( $processed_actions + $this->processed_actions_count ) ) { |
| 170 |
break; |
| 171 |
} |
| 172 |
} |
| 173 |
$this->store->release_claim($claim); |
| 174 |
$this->monitor->detach(); |
| 175 |
$this->clear_caches(); |
| 176 |
return $processed_actions; |
| 177 |
} |
| 178 |
|
| 179 |
/** |
| 180 |
* Flush the cache if possible (intended for use after a batch of actions has been processed). |
| 181 |
* |
| 182 |
* This is useful because running large batches can eat up memory and because invalid data can accrue in the |
| 183 |
* runtime cache, which may lead to unexpected results. |
| 184 |
*/ |
| 185 |
protected function clear_caches() { |
| 186 |
/* |
| 187 |
* Calling wp_cache_flush_runtime() lets us clear the runtime cache without invalidating the external object |
| 188 |
* cache, so we will always prefer this method (as compared to calling wp_cache_flush()) when it is available. |
| 189 |
* |
| 190 |
* However, this function was only introduced in WordPress 6.0. Additionally, the preferred way of detecting if |
| 191 |
* it is supported changed in WordPress 6.1 so we use two different methods to decide if we should utilize it. |
| 192 |
*/ |
| 193 |
$flushing_runtime_cache_explicitly_supported = function_exists( 'wp_cache_supports' ) && wp_cache_supports( 'flush_runtime' ); |
| 194 |
$flushing_runtime_cache_implicitly_supported = ! function_exists( 'wp_cache_supports' ) && function_exists( 'wp_cache_flush_runtime' ); |
| 195 |
|
| 196 |
if ( $flushing_runtime_cache_explicitly_supported || $flushing_runtime_cache_implicitly_supported ) { |
| 197 |
wp_cache_flush_runtime(); |
| 198 |
} elseif ( |
| 199 |
! wp_using_ext_object_cache() |
| 200 |
/** |
| 201 |
* When an external object cache is in use, and when wp_cache_flush_runtime() is not available, then |
| 202 |
* normally the cache will not be flushed after processing a batch of actions (to avoid a performance |
| 203 |
* penalty for other processes). |
| 204 |
* |
| 205 |
* This filter makes it possible to override this behavior and always flush the cache, even if an external |
| 206 |
* object cache is in use. |
| 207 |
* |
| 208 |
* @since 1.0 |
| 209 |
* |
| 210 |
* @param bool $flush_cache If the cache should be flushed. |
| 211 |
*/ |
| 212 |
|| apply_filters( 'action_scheduler_queue_runner_flush_cache', false ) |
| 213 |
) { |
| 214 |
wp_cache_flush(); |
| 215 |
} |
| 216 |
} |
| 217 |
|
| 218 |
public function add_wp_cron_schedule( $schedules ) { |
| 219 |
$schedules['every_minute'] = array( |
| 220 |
'interval' => 60, // in seconds |
| 221 |
'display' => __( 'Every minute', 'action-scheduler' ), |
| 222 |
); |
| 223 |
|
| 224 |
return $schedules; |
| 225 |
} |
| 226 |
} |
| 227 |
|