Commit 2ec37a99 authored by Erick Hitter's avatar Erick Hitter
Browse files

PHPCS fixes

parent e651fa5a
<?php <?php
/**
* Manage event execution
*
* @package a8c_Cron_Control
*/
namespace Automattic\WP\Cron_Control; namespace Automattic\WP\Cron_Control;
/**
* Events class
*/
class Events extends Singleton { class Events extends Singleton {
/** /**
* PLUGIN SETUP * PLUGIN SETUP
*/ */
/** /**
* Class properties * Class constants
*/ */
const LOCK = 'run-events'; const LOCK = 'run-events';
const DISABLE_RUN_OPTION = 'a8c_cron_control_disable_run'; const DISABLE_RUN_OPTION = 'a8c_cron_control_disable_run';
/**
* List of actions whitelisted for concurrent execution
*
* @var array
*/
private $concurrent_action_whitelist = array(); private $concurrent_action_whitelist = array();
/**
* Name of action currently being executed
*
* @var mixed
*/
private $running_event = null; private $running_event = null;
/** /**
* Register hooks * Register hooks
*/ */
protected function class_init() { protected function class_init() {
// Prime lock cache if not present // Prime lock cache if not present.
Lock::prime_lock( self::LOCK ); Lock::prime_lock( self::LOCK );
// Prepare environment as early as possible // Prepare environment as early as possible.
$earliest_action = did_action( 'muplugins_loaded' ) ? 'plugins_loaded' : 'muplugins_loaded'; $earliest_action = did_action( 'muplugins_loaded' ) ? 'plugins_loaded' : 'muplugins_loaded';
add_action( $earliest_action, array( $this, 'prepare_environment' ) ); add_action( $earliest_action, array( $this, 'prepare_environment' ) );
// Allow code loaded as late as the theme to modify the whitelist // Allow code loaded as late as the theme to modify the whitelist.
add_action( 'after_setup_theme', array( $this, 'populate_concurrent_action_whitelist' ) ); add_action( 'after_setup_theme', array( $this, 'populate_concurrent_action_whitelist' ) );
} }
...@@ -40,16 +58,16 @@ class Events extends Singleton { ...@@ -40,16 +58,16 @@ class Events extends Singleton {
* This also runs before Core has parsed the request and set the \REST_REQUEST constant * This also runs before Core has parsed the request and set the \REST_REQUEST constant
*/ */
public function prepare_environment() { public function prepare_environment() {
// Limit to plugin's endpoints // Limit to plugin's endpoints.
$endpoint = get_endpoint_type(); $endpoint = get_endpoint_type();
if ( false === $endpoint ) { if ( false === $endpoint ) {
return; return;
} }
// Flag is used in many contexts, so should be set for all of our requests, regardless of the action // Flag is used in many contexts, so should be set for all of our requests, regardless of the action.
set_doing_cron(); set_doing_cron();
// When running events, allow for long-running ones, and non-blocking trigger requests // When running events, allow for long-running ones, and non-blocking trigger requests.
if ( REST_API::ENDPOINT_RUN === $endpoint ) { if ( REST_API::ENDPOINT_RUN === $endpoint ) {
ignore_user_abort( true ); ignore_user_abort( true );
set_time_limit( JOB_TIMEOUT_IN_MINUTES * MINUTE_IN_SECONDS ); set_time_limit( JOB_TIMEOUT_IN_MINUTES * MINUTE_IN_SECONDS );
...@@ -76,40 +94,40 @@ class Events extends Singleton { ...@@ -76,40 +94,40 @@ class Events extends Singleton {
public function get_events() { public function get_events() {
$events = get_option( 'cron' ); $events = get_option( 'cron' );
// That was easy // That was easy.
if ( ! is_array( $events ) || empty( $events ) ) { if ( ! is_array( $events ) || empty( $events ) ) {
return array( 'events' => null, ); return array( 'events' => null );
} }
// Simplify array format for further processing // Simplify array format for further processing.
$events = collapse_events_array( $events ); $events = collapse_events_array( $events );
// Select only those events to run in the next sixty seconds // Select only those events to run in the next sixty seconds.
// Will include missed events as well // Will include missed events as well.
$current_events = $internal_events = array(); $current_events = $internal_events = array();
$current_window = strtotime( sprintf( '+%d seconds', JOB_QUEUE_WINDOW_IN_SECONDS ) ); $current_window = strtotime( sprintf( '+%d seconds', JOB_QUEUE_WINDOW_IN_SECONDS ) );
foreach ( $events as $event ) { foreach ( $events as $event ) {
// Skip events whose time hasn't come // Skip events whose time hasn't come.
if ( $event['timestamp'] > $current_window ) { if ( $event['timestamp'] > $current_window ) {
continue; continue;
} }
// Skip events that don't have any callbacks hooked to their actions, unless their execution is requested // Skip events that don't have any callbacks hooked to their actions, unless their execution is requested.
if ( ! $this->action_has_callback_or_should_run_anyway( $event ) ) { if ( ! $this->action_has_callback_or_should_run_anyway( $event ) ) {
continue; continue;
} }
// Necessary data to identify an individual event // Necessary data to identify an individual event.
// `$event['action']` is hashed to avoid information disclosure // `$event['action']` is hashed to avoid information disclosure.
// Core hashes `$event['instance']` for us // Core hashes `$event['instance']` for us.
$event_data_public = array( $event_data_public = array(
'timestamp' => $event['timestamp'], 'timestamp' => $event['timestamp'],
'action' => md5( $event['action'] ), 'action' => md5( $event['action'] ),
'instance' => $event['instance'], 'instance' => $event['instance'],
); );
// Queue internal events separately to avoid them being blocked // Queue internal events separately to avoid them being blocked.
if ( is_internal_event( $event['action'] ) ) { if ( is_internal_event( $event['action'] ) ) {
$internal_events[] = $event_data_public; $internal_events[] = $event_data_public;
} else { } else {
...@@ -117,13 +135,13 @@ class Events extends Singleton { ...@@ -117,13 +135,13 @@ class Events extends Singleton {
} }
} }
// Limit batch size to avoid resource exhaustion // Limit batch size to avoid resource exhaustion.
if ( count( $current_events ) > JOB_QUEUE_SIZE ) { if ( count( $current_events ) > JOB_QUEUE_SIZE ) {
$current_events = $this->reduce_queue( $current_events ); $current_events = $this->reduce_queue( $current_events );
} }
// Combine with Internal Events // Combine with Internal Events.
// TODO: un-nest array, which is nested for legacy reasons // TODO: un-nest array, which is nested for legacy reasons.
return array( return array(
'events' => array_merge( $current_events, $internal_events ), 'events' => array_merge( $current_events, $internal_events ),
); );
...@@ -133,22 +151,21 @@ class Events extends Singleton { ...@@ -133,22 +151,21 @@ class Events extends Singleton {
* Check that an event has a callback to run, and allow the check to be overridden * Check that an event has a callback to run, and allow the check to be overridden
* Empty events are, by default, skipped and removed/rescheduled * Empty events are, by default, skipped and removed/rescheduled
* *
* @param $event array Event data * @param array $event Event data.
*
* @return bool * @return bool
*/ */
private function action_has_callback_or_should_run_anyway( $event ) { private function action_has_callback_or_should_run_anyway( $event ) {
// Event has a callback, so let's get on with it // Event has a callback, so let's get on with it.
if ( false !== has_action( $event['action'] ) ) { if ( false !== has_action( $event['action'] ) ) {
return true; return true;
} }
// Run the event anyway, perhaps because callbacks are added using the `all` action // Run the event anyway, perhaps because callbacks are added using the `all` action.
if ( apply_filters( 'a8c_cron_control_run_event_with_no_callbacks', false, $event ) ) { if ( apply_filters( 'a8c_cron_control_run_event_with_no_callbacks', false, $event ) ) {
return true; return true;
} }
// Remove or reschedule the empty event // Remove or reschedule the empty event.
if ( false === $event['args']['schedule'] ) { if ( false === $event['args']['schedule'] ) {
wp_unschedule_event( $event['timestamp'], $event['action'], $event['args']['args'] ); wp_unschedule_event( $event['timestamp'], $event['action'], $event['args']['args'] );
} else { } else {
...@@ -163,28 +180,28 @@ class Events extends Singleton { ...@@ -163,28 +180,28 @@ class Events extends Singleton {
/** /**
* Trim events queue down to the limit set by JOB_QUEUE_SIZE * Trim events queue down to the limit set by JOB_QUEUE_SIZE
* *
* @param $events array List of events to be run in the current period * @param array $events List of events to be run in the current period.
* *
* @return array * @return array
*/ */
private function reduce_queue( $events ) { private function reduce_queue( $events ) {
// Loop through events, adding one of each action during each iteration // Loop through events, adding one of each action during each iteration.
$reduced_queue = array(); $reduced_queue = array();
$action_counts = array(); $action_counts = array();
$i = 1; // Intentionally not zero-indexed to facilitate comparisons against $action_counts members $i = 1; // Intentionally not zero-indexed to facilitate comparisons against $action_counts members.
do { do {
// Each time the events array is iterated over, move one instance of an action to the current queue // Each time the events array is iterated over, move one instance of an action to the current queue.
foreach ( $events as $key => $event ) { foreach ( $events as $key => $event ) {
$action = $event['action']; $action = $event['action'];
// Prime the count // Prime the count.
if ( ! isset( $action_counts[ $action ] ) ) { if ( ! isset( $action_counts[ $action ] ) ) {
$action_counts[ $action ] = 0; $action_counts[ $action ] = 0;
} }
// Check and do the move // Check and do the move.
if ( $action_counts[ $action ] < $i ) { if ( $action_counts[ $action ] < $i ) {
$reduced_queue[] = $event; $reduced_queue[] = $event;
$action_counts[ $action ]++; $action_counts[ $action ]++;
...@@ -192,7 +209,7 @@ class Events extends Singleton { ...@@ -192,7 +209,7 @@ class Events extends Singleton {
} }
} }
// When done with an iteration and events remain, start again from the beginning of the $events array // When done with an iteration and events remain, start again from the beginning of the $events array.
if ( empty( $events ) ) { if ( empty( $events ) ) {
break; break;
} else { } else {
...@@ -201,7 +218,7 @@ class Events extends Singleton { ...@@ -201,7 +218,7 @@ class Events extends Singleton {
continue; continue;
} }
} while( $i <= 15 && count( $reduced_queue ) < JOB_QUEUE_SIZE && ! empty( $events ) ); } while ( $i <= 15 && count( $reduced_queue ) < JOB_QUEUE_SIZE && ! empty( $events ) );
/** /**
* IMPORTANT: DO NOT re-sort the $reduced_queue array from this point forward. * IMPORTANT: DO NOT re-sort the $reduced_queue array from this point forward.
...@@ -213,7 +230,7 @@ class Events extends Singleton { ...@@ -213,7 +230,7 @@ class Events extends Singleton {
* for the current JOB_QUEUE_WINDOW_IN_SECONDS. * for the current JOB_QUEUE_WINDOW_IN_SECONDS.
*/ */
// Finally, ensure that we don't have more than we need // Finally, ensure that we don't have more than we need.
if ( count( $reduced_queue ) > JOB_QUEUE_SIZE ) { if ( count( $reduced_queue ) > JOB_QUEUE_SIZE ) {
$reduced_queue = array_slice( $reduced_queue, 0, JOB_QUEUE_SIZE ); $reduced_queue = array_slice( $reduced_queue, 0, JOB_QUEUE_SIZE );
} }
...@@ -224,25 +241,24 @@ class Events extends Singleton { ...@@ -224,25 +241,24 @@ class Events extends Singleton {
/** /**
* Execute a specific event * Execute a specific event
* *
* @param $timestamp int Unix timestamp * @param int $timestamp Unix timestamp.
* @param $action string md5 hash of the action used when the event is registered * @param string $action md5 hash of the action used when the event is registered.
* @param $instance string md5 hash of the event's arguments array, which Core uses to index the `cron` option * @param string $instance md5 hash of the event's arguments array, which Core uses to index the `cron` option.
* @param $force bool Run event regardless of timestamp or lock status? eg, when executing jobs via wp-cli * @param bool $force Run event regardless of timestamp or lock status? eg, when executing jobs via wp-cli.
*
* @return array|\WP_Error * @return array|\WP_Error
*/ */
public function run_event( $timestamp, $action, $instance, $force = false ) { public function run_event( $timestamp, $action, $instance, $force = false ) {
// Validate input data // Validate input data.
if ( empty( $timestamp ) || empty( $action ) || empty( $instance ) ) { if ( empty( $timestamp ) || empty( $action ) || empty( $instance ) ) {
return new \WP_Error( 'missing-data', __( 'Invalid or incomplete request data.', 'automattic-cron-control' ), array( 'status' => 400, ) ); return new \WP_Error( 'missing-data', __( 'Invalid or incomplete request data.', 'automattic-cron-control' ), array( 'status' => 400 ) );
} }
// Ensure we don't run jobs ahead of time // Ensure we don't run jobs ahead of time.
if ( ! $force && $timestamp > time() ) { if ( ! $force && $timestamp > time() ) {
return new \WP_Error( 'premature', sprintf( __( 'Job with identifier `%1$s` is not scheduled to run yet.', 'automattic-cron-control' ), "$timestamp-$action-$instance" ), array( 'status' => 403, ) ); return new \WP_Error( 'premature', sprintf( __( 'Job with identifier `%1$s` is not scheduled to run yet.', 'automattic-cron-control' ), "$timestamp-$action-$instance" ), array( 'status' => 403 ) );
} }
// Find the event to retrieve the full arguments // Find the event to retrieve the full arguments.
$event = get_event_by_attributes( array( $event = get_event_by_attributes( array(
'timestamp' => $timestamp, 'timestamp' => $timestamp,
'action_hashed' => $action, 'action_hashed' => $action,
...@@ -252,35 +268,37 @@ class Events extends Singleton { ...@@ -252,35 +268,37 @@ class Events extends Singleton {
// Nothing to do... // Nothing to do...
if ( ! is_object( $event ) ) { if ( ! is_object( $event ) ) {
return new \WP_Error( 'no-event', sprintf( __( 'Job with identifier `%1$s` could not be found.', 'automattic-cron-control' ), "$timestamp-$action-$instance" ), array( 'status' => 404, ) ); return new \WP_Error( 'no-event', sprintf( __( 'Job with identifier `%1$s` could not be found.', 'automattic-cron-control' ), "$timestamp-$action-$instance" ), array( 'status' => 404 ) );
} }
unset( $timestamp, $action, $instance ); unset( $timestamp, $action, $instance );
// Limit how many events are processed concurrently, unless explicitly bypassed // Limit how many events are processed concurrently, unless explicitly bypassed.
if ( ! $force ) { if ( ! $force ) {
// Prepare event-level lock // Prepare event-level lock.
$this->prime_event_action_lock( $event ); $this->prime_event_action_lock( $event );
if ( ! $this->can_run_event( $event ) ) { if ( ! $this->can_run_event( $event ) ) {
return new \WP_Error( 'no-free-threads', sprintf( __( 'No resources available to run the job with action action `%1$s` and arguments `%2$s`.', 'automattic-cron-control' ), $event->action, maybe_serialize( $event->args ) ), array( 'status' => 429, ) ); return new \WP_Error( 'no-free-threads', sprintf( __( 'No resources available to run the job with action action `%1$s` and arguments `%2$s`.', 'automattic-cron-control' ), $event->action, maybe_serialize( $event->args ) ), array( 'status' => 429 ) );
} }
// Free locks should event throw uncatchable error // Free locks should event throw uncatchable error.
$this->running_event = $event; $this->running_event = $event;
add_action( 'shutdown', array( $this, 'do_lock_cleanup_on_shutdown' ) ); add_action( 'shutdown', array( $this, 'do_lock_cleanup_on_shutdown' ) );
} }
// Mark the event completed, and reschedule if desired // Mark the event completed, and reschedule if desired.
// Core does this before running the job, so we respect that // Core does this before running the job, so we respect that.
$this->update_event_record( $event ); $this->update_event_record( $event );
// Run the event // Run the event.
try { try {
do_action_ref_array( $event->action, $event->args ); do_action_ref_array( $event->action, $event->args );
} catch ( \Throwable $t ) { } catch ( \Throwable $t ) {
// Note that timeouts and memory exhaustion do not invoke this block /**
// Instead, those locks are freed in `do_lock_cleanup_on_shutdown()` * Note that timeouts and memory exhaustion do not invoke this block.
* Instead, those locks are freed in `do_lock_cleanup_on_shutdown()`.
*/
do_action( 'a8c_cron_control_event_threw_catchable_error', $event, $t ); do_action( 'a8c_cron_control_event_threw_catchable_error', $event, $t );
...@@ -290,16 +308,16 @@ class Events extends Singleton { ...@@ -290,16 +308,16 @@ class Events extends Singleton {
); );
} }
// Free locks for the next event, unless they weren't set to begin with // Free locks for the next event, unless they weren't set to begin with.
if ( ! $force ) { if ( ! $force ) {
// If we got this far, there's no uncaught error to handle // If we got this far, there's no uncaught error to handle.
$this->running_event = null; $this->running_event = null;
remove_action( 'shutdown', array( $this, 'do_lock_cleanup_on_shutdown' ) ); remove_action( 'shutdown', array( $this, 'do_lock_cleanup_on_shutdown' ) );
$this->do_lock_cleanup( $event ); $this->do_lock_cleanup( $event );
} }
// Callback didn't trigger a Throwable, indicating it succeeded // Callback didn't trigger a Throwable, indicating it succeeded.
if ( ! isset( $return ) ) { if ( ! isset( $return ) ) {
$return = array( $return = array(
'success' => true, 'success' => true,
...@@ -315,7 +333,7 @@ class Events extends Singleton { ...@@ -315,7 +333,7 @@ class Events extends Singleton {
* *
* Used to ensure only one instance of a particular event, such as `wp_version_check` runs at one time * Used to ensure only one instance of a particular event, such as `wp_version_check` runs at one time
* *
* @param $event object Event data * @param object $event Event data.
*/ */
private function prime_event_action_lock( $event ) { private function prime_event_action_lock( $event ) {
Lock::prime_lock( $this->get_lock_key_for_event_action( $event ), JOB_LOCK_EXPIRY_IN_MINUTES * \MINUTE_IN_SECONDS ); Lock::prime_lock( $this->get_lock_key_for_event_action( $event ), JOB_LOCK_EXPIRY_IN_MINUTES * \MINUTE_IN_SECONDS );
...@@ -324,12 +342,11 @@ class Events extends Singleton { ...@@ -324,12 +342,11 @@ class Events extends Singleton {
/** /**
* Are resources available to run this event? * Are resources available to run this event?
* *
* @param object $event Event data * @param object $event Event data.
*
* @return bool * @return bool
*/ */
private function can_run_event( $event ) { private function can_run_event( $event ) {
// Limit to one concurrent execution of a specific action by default // Limit to one concurrent execution of a specific action by default.
$limit = 1; $limit = 1;
if ( isset( $this->concurrent_action_whitelist[ $event->action ] ) ) { if ( isset( $this->concurrent_action_whitelist[ $event->action ] ) ) {
...@@ -341,13 +358,13 @@ class Events extends Singleton { ...@@ -341,13 +358,13 @@ class Events extends Singleton {
return false; return false;
} }
// Internal Events aren't subject to the global lock // Internal Events aren't subject to the global lock.
if ( is_internal_event( $event->action ) ) { if ( is_internal_event( $event->action ) ) {
return true; return true;
} }
// Check if any resources are available to execute this job // Check if any resources are available to execute this job.
// If not, the individual-event lock must be freed, otherwise it's deadlocked until it times out // If not, the individual-event lock must be freed, otherwise it's deadlocked until it times out.
if ( ! Lock::check_lock( self::LOCK, JOB_CONCURRENCY_LIMIT ) ) { if ( ! Lock::check_lock( self::LOCK, JOB_CONCURRENCY_LIMIT ) ) {
$this->reset_event_lock( $event ); $this->reset_event_lock( $event );
return false; return false;
...@@ -360,23 +377,22 @@ class Events extends Singleton { ...@@ -360,23 +377,22 @@ class Events extends Singleton {
/** /**
* Free locks after event completes * Free locks after event completes
* *
* @param object $event Event data * @param object $event Event data.
*/ */
private function do_lock_cleanup( $event ) { private function do_lock_cleanup( $event ) {
// Lock isn't set when event is Internal, so we don't want to alter it // Lock isn't set when event is Internal, so we don't want to alter it.
if ( ! is_internal_event( $event->action ) ) { if ( ! is_internal_event( $event->action ) ) {
Lock::free_lock( self::LOCK ); Lock::free_lock( self::LOCK );
} }
// Reset individual event lock // Reset individual event lock.
$this->reset_event_lock( $event ); $this->reset_event_lock( $event );
} }
/** /**
* Frees the lock for an individual event * Frees the lock for an individual event
* *
* @param object $event Event data * @param object $event Event data.
*
* @return bool * @return bool
*/ */
private function reset_event_lock( $event ) { private function reset_event_lock( $event ) {
...@@ -393,37 +409,38 @@ class Events extends Singleton { ...@@ -393,37 +409,38 @@ class Events extends Singleton {
/** /**
* Turn the event action into a string that can be used with a lock * Turn the event action into a string that can be used with a lock
* *
* @param object $event Event data * @param object $event Event data.
*
* @return string * @return string
*/ */
public function get_lock_key_for_event_action( $event ) { public function get_lock_key_for_event_action( $event ) {
// Hashed solely to constrain overall length // Hashed solely to constrain overall length.
return md5( 'ev-' . $event->action ); return md5( 'ev-' . $event->action );
} }
/** /**
* Mark an event completed, and reschedule when requested * Mark an event completed, and reschedule when requested
*
* @param object $event Event data.
*/ */
private function update_event_record( $event ) { private function update_event_record( $event ) {
if ( false !== $event->schedule ) { if ( false !== $event->schedule ) {
// Re-implements much of the logic from `wp_reschedule_event()` // Re-implements much of the logic from `wp_reschedule_event()`.
$schedules = wp_get_schedules(); $schedules = wp_get_schedules();
$interval = 0; $interval = 0;
// First, we try to get it from the schedule // First, we try to get it from the schedule.
if ( isset( $schedules[ $event->schedule ] ) ) { if ( isset( $schedules[ $event->schedule ] ) ) {
$interval = $schedules[ $event->schedule ]['interval']; $interval = $schedules[ $event->schedule ]['interval'];
} }
// Now we try to get it from the saved interval, in case the schedule disappears // Now we try to get it from the saved interval, in case the schedule disappears.
if ( 0 == $interval ) { if ( 0 == $interval ) {
$interval = $event->interval;