| 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 |
/** |
| 12 |
* ActionScheduler_AsyncRequest_QueueRunner instance. |
| 13 |
* |
| 14 |
* @var ActionScheduler_AsyncRequest_QueueRunner |
| 15 |
*/ |
| 16 |
protected $async_request; |
| 17 |
|
| 18 |
/** |
| 19 |
* ActionScheduler_QueueRunner instance. |
| 20 |
* |
| 21 |
* @var ActionScheduler_QueueRunner |
| 22 |
*/ |
| 23 |
private static $runner = null; |
| 24 |
|
| 25 |
/** |
| 26 |
* Whether the cleaner instance is a non-default one. |
| 27 |
* |
| 28 |
* @var bool |
| 29 |
*/ |
| 30 |
private $is_custom_cleaner; |
| 31 |
|
| 32 |
/** |
| 33 |
* Number of processed actions. |
| 34 |
* |
| 35 |
* @var int |
| 36 |
*/ |
| 37 |
private $processed_actions_count = 0; |
| 38 |
|
| 39 |
/** |
| 40 |
* Get instance. |
| 41 |
* |
| 42 |
* @return ActionScheduler_QueueRunner |
| 43 |
* @codeCoverageIgnore |
| 44 |
*/ |
| 45 |
public static function instance() { |
| 46 |
if ( empty( self::$runner ) ) { |
| 47 |
$class = apply_filters( 'action_scheduler_queue_runner_class', 'ActionScheduler_QueueRunner' ); |
| 48 |
self::$runner = new $class(); |
| 49 |
} |
| 50 |
|
| 51 |
return self::$runner; |
| 52 |
} |
| 53 |
|
| 54 |
/** |
| 55 |
* ActionScheduler_QueueRunner constructor. |
| 56 |
* |
| 57 |
* @param ActionScheduler_Store|null $store Store object. |
| 58 |
* @param ActionScheduler_FatalErrorMonitor|null $monitor Monitor object. |
| 59 |
* @param ActionScheduler_QueueCleaner|null $cleaner Cleaner object. |
| 60 |
* @param ActionScheduler_AsyncRequest_QueueRunner|null $async_request Async request runner object. |
| 61 |
*/ |
| 62 |
public function __construct( ?ActionScheduler_Store $store = null, ?ActionScheduler_FatalErrorMonitor $monitor = null, ?ActionScheduler_QueueCleaner $cleaner = null, ?ActionScheduler_AsyncRequest_QueueRunner $async_request = null ) { |
| 63 |
parent::__construct( $store, $monitor, $cleaner ); |
| 64 |
|
| 65 |
if ( is_null( $async_request ) ) { |
| 66 |
$async_request = new ActionScheduler_AsyncRequest_QueueRunner( $this->store ); |
| 67 |
} |
| 68 |
|
| 69 |
$this->async_request = $async_request; |
| 70 |
$this->is_custom_cleaner = get_class( $this->cleaner ) !== ActionScheduler_QueueCleaner::class; |
| 71 |
} |
| 72 |
|
| 73 |
/** |
| 74 |
* Initialize. |
| 75 |
* |
| 76 |
* @codeCoverageIgnore |
| 77 |
*/ |
| 78 |
public function init() { |
| 79 |
|
| 80 |
add_filter( 'cron_schedules', array( self::instance(), 'add_wp_cron_schedule' ) ); // phpcs:ignore WordPress.WP.CronInterval.CronSchedulesInterval |
| 81 |
|
| 82 |
// Check for and remove any WP Cron hook scheduled by Action Scheduler < 3.0.0, which didn't include the $context param. |
| 83 |
$next_timestamp = wp_next_scheduled( self::WP_CRON_HOOK ); |
| 84 |
if ( $next_timestamp ) { |
| 85 |
wp_unschedule_event( $next_timestamp, self::WP_CRON_HOOK ); |
| 86 |
} |
| 87 |
|
| 88 |
$cron_context = array( 'WP Cron' ); |
| 89 |
|
| 90 |
if ( ! wp_next_scheduled( self::WP_CRON_HOOK, $cron_context ) ) { |
| 91 |
$schedule = apply_filters( 'action_scheduler_run_schedule', self::WP_CRON_SCHEDULE ); |
| 92 |
wp_schedule_event( time(), $schedule, self::WP_CRON_HOOK, $cron_context ); |
| 93 |
} |
| 94 |
|
| 95 |
add_action( self::WP_CRON_HOOK, array( self::instance(), 'run' ) ); |
| 96 |
$this->hook_dispatch_async_request(); |
| 97 |
|
| 98 |
// Backward compatibility: If the action cleaner is standard, cleaning will be performed as an action to improve throughput |
| 99 |
// and enable daily runs. If not, cleaning will occur explicitly before processing actions to ensure backward compatibility. |
| 100 |
// The cleaner was initially designed as a QueueRunner dependency, which is why the hooks are registered here. |
| 101 |
if ( ! $this->is_custom_cleaner ) { |
| 102 |
$this->cleaner->register_cleaner_hooks(); |
| 103 |
} |
| 104 |
} |
| 105 |
|
| 106 |
/** |
| 107 |
* Hook check for dispatching an async request. |
| 108 |
*/ |
| 109 |
public function hook_dispatch_async_request() { |
| 110 |
add_action( 'shutdown', array( $this, 'maybe_dispatch_async_request' ) ); |
| 111 |
} |
| 112 |
|
| 113 |
/** |
| 114 |
* Unhook check for dispatching an async request. |
| 115 |
*/ |
| 116 |
public function unhook_dispatch_async_request() { |
| 117 |
remove_action( 'shutdown', array( $this, 'maybe_dispatch_async_request' ) ); |
| 118 |
} |
| 119 |
|
| 120 |
/** |
| 121 |
* Check if we should dispatch an async request to process actions. |
| 122 |
* |
| 123 |
* This method is attached to 'shutdown', so is called frequently. To avoid slowing down |
| 124 |
* the site, it mitigates the work performed in each request by: |
| 125 |
* 1. checking if it's in the admin context and then |
| 126 |
* 2. haven't run on the 'shutdown' hook within the lock time (60 seconds by default) |
| 127 |
* 3. haven't exceeded the number of allowed batches. |
| 128 |
* |
| 129 |
* The order of these checks is important, because they run from a check on a value: |
| 130 |
* 1. in memory - is_admin() maps to $GLOBALS or the WP_ADMIN constant |
| 131 |
* 2. in memory - transients use autoloaded options by default |
| 132 |
* 3. from a database query - has_maximum_concurrent_batches() run the query |
| 133 |
* $this->store->get_claim_count() to find the current number of claims in the DB. |
| 134 |
* |
| 135 |
* If all of these conditions are met, then we request an async runner check whether it |
| 136 |
* should dispatch a request to process pending actions. |
| 137 |
*/ |
| 138 |
public function maybe_dispatch_async_request() { |
| 139 |
// Only start an async queue at most once every 60 seconds. |
| 140 |
if ( |
| 141 |
is_admin() |
| 142 |
&& ! ActionScheduler::lock()->is_locked( 'async-request-runner' ) |
| 143 |
&& ActionScheduler::lock()->set( 'async-request-runner' ) |
| 144 |
) { |
| 145 |
$this->async_request->maybe_dispatch(); |
| 146 |
} |
| 147 |
} |
| 148 |
|
| 149 |
/** |
| 150 |
* Process actions in the queue. Attached to self::WP_CRON_HOOK i.e. 'action_scheduler_run_queue' |
| 151 |
* |
| 152 |
* The $context param of this method defaults to 'WP Cron', because prior to Action Scheduler 3.0.0 |
| 153 |
* that was the only context in which this method was run, and the self::WP_CRON_HOOK hook had no context |
| 154 |
* passed along with it. New code calling this method directly, or by triggering the self::WP_CRON_HOOK, |
| 155 |
* should set a context as the first parameter. For an example of this, refer to the code seen in |
| 156 |
* |
| 157 |
* @see ActionScheduler_AsyncRequest_QueueRunner::handle() |
| 158 |
* |
| 159 |
* @param string $context Optional identifier for the context in which this action is being processed, e.g. 'WP CLI' or 'WP Cron' |
| 160 |
* Generally, this should be capitalised and not localised as it's a proper noun. |
| 161 |
* @return int The number of actions processed. |
| 162 |
*/ |
| 163 |
public function run( $context = 'WP Cron' ) { |
| 164 |
ActionScheduler_Compatibility::raise_memory_limit(); |
| 165 |
ActionScheduler_Compatibility::raise_time_limit( $this->get_time_limit() ); |
| 166 |
|
| 167 |
do_action( 'action_scheduler_before_process_queue' ); |
| 168 |
|
| 169 |
$cleanup_time_limit = 10 * $this->get_time_limit(); |
| 170 |
// Backward compatibility: If the action cleaner is standard, cleaning will be performed as an action to improve throughput |
| 171 |
// and enable daily runs. If not, cleaning will occur explicitly before processing actions to ensure backward compatibility. |
| 172 |
if ( $this->is_custom_cleaner ) { |
| 173 |
// Execute complete cleanup cycle, as in this logical branch deletion IS NOT executed via a separate action. |
| 174 |
$this->cleaner->clean( $cleanup_time_limit ); |
| 175 |
} else { |
| 176 |
// Execute partial cleanup cycle, as in this logical branch deletion IS executed via a separate action. |
| 177 |
$this->cleaner->reset_timeouts( $cleanup_time_limit ); |
| 178 |
$this->cleaner->mark_failures( $cleanup_time_limit ); |
| 179 |
} |
| 180 |
|
| 181 |
$this->processed_actions_count = 0; |
| 182 |
if ( false === $this->has_maximum_concurrent_batches() ) { |
| 183 |
$batch_size = apply_filters( 'action_scheduler_queue_runner_batch_size', 25 ); |
| 184 |
// Note: gc_collect_cycles() was considered here and in do_batch/clear_caches, but rejected: |
| 185 |
// upside is speculative, GC sweep cost scales with object count and can cause pauses. |
| 186 |
do { |
| 187 |
$processed_actions_in_batch = $this->do_batch( $batch_size, $context ); |
| 188 |
$this->processed_actions_count += $processed_actions_in_batch; |
| 189 |
} 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. |
| 190 |
} |
| 191 |
|
| 192 |
do_action( 'action_scheduler_after_process_queue' ); |
| 193 |
return $this->processed_actions_count; |
| 194 |
} |
| 195 |
|
| 196 |
/** |
| 197 |
* Process a batch of actions pending in the queue. |
| 198 |
* |
| 199 |
* Actions are processed by claiming a set of pending actions then processing each one until either the batch |
| 200 |
* size is completed, or memory or time limits are reached, defined by @see $this->batch_limits_exceeded(). |
| 201 |
* |
| 202 |
* @param int $size The maximum number of actions to process in the batch. |
| 203 |
* @param string $context Optional identifier for the context in which this action is being processed, e.g. 'WP CLI' or 'WP Cron' |
| 204 |
* Generally, this should be capitalised and not localised as it's a proper noun. |
| 205 |
* @return int The number of actions processed. |
| 206 |
*/ |
| 207 |
protected function do_batch( $size = 100, $context = '' ) { |
| 208 |
$claim = $this->store->stake_claim( $size ); |
| 209 |
$this->monitor->attach( $claim ); |
| 210 |
$processed_actions = 0; |
| 211 |
|
| 212 |
$claim_id = $claim->get_id(); |
| 213 |
foreach ( $claim->get_actions() as $action_id ) { |
| 214 |
// Bail if we lost the claim. |
| 215 |
if ( $claim_id !== $this->store->get_claim_id( $action_id ) ) { |
| 216 |
break; |
| 217 |
} |
| 218 |
|
| 219 |
$this->process_action( $action_id, $context ); |
| 220 |
$processed_actions++; |
| 221 |
|
| 222 |
if ( $this->batch_limits_exceeded( $processed_actions + $this->processed_actions_count ) ) { |
| 223 |
break; |
| 224 |
} |
| 225 |
} |
| 226 |
$this->store->release_claim( $claim ); |
| 227 |
$this->monitor->detach(); |
| 228 |
$this->clear_caches(); |
| 229 |
return $processed_actions; |
| 230 |
} |
| 231 |
|
| 232 |
/** |
| 233 |
* Flush the cache if possible (intended for use after a batch of actions has been processed). |
| 234 |
* |
| 235 |
* This is useful because running large batches can eat up memory and because invalid data can accrue in the |
| 236 |
* runtime cache, which may lead to unexpected results. |
| 237 |
*/ |
| 238 |
protected function clear_caches() { |
| 239 |
/* |
| 240 |
* Calling wp_cache_flush_runtime() lets us clear the runtime cache without invalidating the external object |
| 241 |
* cache, so we will always prefer this method (as compared to calling wp_cache_flush()) when it is available. |
| 242 |
* |
| 243 |
* However, this function was only introduced in WordPress 6.0. Additionally, the preferred way of detecting if |
| 244 |
* it is supported changed in WordPress 6.1 so we use two different methods to decide if we should utilize it. |
| 245 |
*/ |
| 246 |
$flushing_runtime_cache_explicitly_supported = function_exists( 'wp_cache_supports' ) && wp_cache_supports( 'flush_runtime' ); |
| 247 |
$flushing_runtime_cache_implicitly_supported = ! function_exists( 'wp_cache_supports' ) && function_exists( 'wp_cache_flush_runtime' ); |
| 248 |
|
| 249 |
if ( $flushing_runtime_cache_explicitly_supported || $flushing_runtime_cache_implicitly_supported ) { |
| 250 |
wp_cache_flush_runtime(); |
| 251 |
} elseif ( |
| 252 |
! wp_using_ext_object_cache() |
| 253 |
/** |
| 254 |
* When an external object cache is in use, and when wp_cache_flush_runtime() is not available, then |
| 255 |
* normally the cache will not be flushed after processing a batch of actions (to avoid a performance |
| 256 |
* penalty for other processes). |
| 257 |
* |
| 258 |
* This filter makes it possible to override this behavior and always flush the cache, even if an external |
| 259 |
* object cache is in use. |
| 260 |
* |
| 261 |
* @since 1.0 |
| 262 |
* |
| 263 |
* @param bool $flush_cache If the cache should be flushed. |
| 264 |
*/ |
| 265 |
|| apply_filters( 'action_scheduler_queue_runner_flush_cache', false ) |
| 266 |
) { |
| 267 |
wp_cache_flush(); |
| 268 |
} |
| 269 |
} |
| 270 |
|
| 271 |
/** |
| 272 |
* Add schedule to WP cron. |
| 273 |
* |
| 274 |
* @param array<string, array<string, int|string>> $schedules Schedules. |
| 275 |
* @return array<string, array<string, int|string>> |
| 276 |
*/ |
| 277 |
public function add_wp_cron_schedule( $schedules ) { |
| 278 |
$schedules['every_minute'] = array( |
| 279 |
'interval' => 60, // in seconds. |
| 280 |
'display' => __( 'Every minute', 'action-scheduler' ), |
| 281 |
); |
| 282 |
|
| 283 |
return $schedules; |
| 284 |
} |
| 285 |
} |
| 286 |
|