Commit 8d8aea49 authored by Erick Hitter's avatar Erick Hitter Committed by GitHub

Merge pull request #142 from Automattic/fix/phpcs

PHPCS fixes
parents 7928aaee 19c2336f
sudo: false
language: php
notifications:
......@@ -5,25 +7,59 @@ notifications:
on_success: never
on_failure: change
php:
- 5.6
- 7.0
cache:
directories:
- vendor
- $HOME/.composer/cache
env:
- WP_VERSION=latest WP_MULTISITE=0
- WP_VERSION=latest WP_MULTISITE=1
- WP_VERSION=trunk WP_MULTISITE=0
- WP_VERSION=trunk WP_MULTISITE=1
matrix:
include:
# PHPUnit
- php: 7.2
env: WP_VERSION=latest
- php: 7.2
env: WP_VERSION=trunk
- php: 7.1
env: WP_VERSION=latest
- php: 7.1
env: WP_VERSION=trunk
- php: 7.0
env: WP_VERSION=latest
- php: 7.0
env: WP_VERSION=trunk
- php: 5.6
env: WP_VERSION=latest
- php: 5.6
env: WP_VERSION=trunk
# PHPCS
- php: 7.1
env: WP_TRAVISCI=phpcs
before_script:
- phpenv config-rm xdebug.ini
- export PATH="$HOME/.composer/vendor/bin:$PATH"
- |
if [[ ${TRAVIS_PHP_VERSION:0:2} == "7." ]]; then
composer global require "phpunit/phpunit=5.7.*"
elif [[ ${TRAVIS_PHP_VERSION:0:3} != "5.2" ]]; then
composer global require "phpunit/phpunit=4.8.*"
if [[ ! -z "$WP_VERSION" ]] ; then
bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION
if [[ ${TRAVIS_PHP_VERSION:0:2} == "5." ]]; then
composer global require "phpunit/phpunit=4.8.*"
else
composer global require "phpunit/phpunit=5.7.*"
fi
fi
- |
if [[ "$WP_TRAVISCI" == "phpcs" ]] ; then
composer global require wp-coding-standards/wpcs
phpcs --config-set installed_paths $HOME/.composer/vendor/wp-coding-standards/wpcs
fi
- bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION
script: phpunit
script:
- |
if [[ ! -z "$WP_VERSION" ]] ; then
phpunit
WP_MULTISITE=1 phpunit
fi
- |
if [[ "$WP_TRAVISCI" == "phpcs" ]] ; then
phpcs
fi
<?php
/*
Plugin Name: Cron Control
Plugin URI:
Description: Execute WordPress cron events in parallel, using a custom post type for event storage.
Author: Erick Hitter, Automattic
Version: 1.5
Text Domain: automattic-cron-control
/**
* Plugin Name: Cron Control
* Plugin URI: https://vip.wordpress.com/
* Description: Execute WordPress cron events in parallel, using a custom post type for event storage.
* Author: Erick Hitter, Automattic
* Version: 1.5
* Text Domain: automattic-cron-control
* Domain Path: /languages
*
* @package a8c_Cron_Control
*/
namespace Automattic\WP\Cron_Control;
// Load basics needed to instantiate plugin
require __DIR__ . '/includes/abstract-class-singleton.php';
// Load basics needed to instantiate plugin.
require __DIR__ . '/includes/class-singleton.php';
// Instantiate main plugin class, which checks environment and loads remaining classes when appropriate
// Instantiate main plugin class, which checks environment and loads remaining classes when appropriate.
require __DIR__ . '/includes/class-main.php';
Main::instance();
This diff is collapsed.
<?php
/**
* Manage event execution
*
* @package a8c_Cron_Control
*/
namespace Automattic\WP\Cron_Control;
/**
* Events class
*/
class Events extends Singleton {
/**
* PLUGIN SETUP
*/
/**
* Class properties
* Class constants
*/
const LOCK = 'run-events';
const DISABLE_RUN_OPTION = 'a8c_cron_control_disable_run';
/**
* List of actions whitelisted for concurrent execution
*
* @var array
*/
private $concurrent_action_whitelist = array();
/**
* Name of action currently being executed
*
* @var mixed
*/
private $running_event = null;
/**
* Register hooks
*/
protected function class_init() {
// Prime lock cache if not present
// Prime lock cache if not present.
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';
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' ) );
}
......@@ -40,16 +58,16 @@ class Events extends Singleton {
* This also runs before Core has parsed the request and set the \REST_REQUEST constant
*/
public function prepare_environment() {
// Limit to plugin's endpoints
// Limit to plugin's endpoints.
$endpoint = get_endpoint_type();
if ( false === $endpoint ) {
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();
// 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 ) {
ignore_user_abort( true );
set_time_limit( JOB_TIMEOUT_IN_MINUTES * MINUTE_IN_SECONDS );
......@@ -76,40 +94,43 @@ class Events extends Singleton {
public function get_events() {
$events = get_option( 'cron' );
// That was easy
// That was easy.
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 );
// Select only those events to run in the next sixty seconds
// Will include missed events as well
$current_events = $internal_events = array();
$current_window = strtotime( sprintf( '+%d seconds', JOB_QUEUE_WINDOW_IN_SECONDS ) );
// Select only those events to run in the next sixty seconds.
// Will include missed events as well.
$current_events = array();
$internal_events = array();
$current_window = strtotime( sprintf( '+%d seconds', JOB_QUEUE_WINDOW_IN_SECONDS ) );
foreach ( $events as $event ) {
// Skip events whose time hasn't come
// Skip events whose time hasn't come.
if ( $event['timestamp'] > $current_window ) {
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 ) ) {
continue;
}
// Necessary data to identify an individual event
// `$event['action']` is hashed to avoid information disclosure
// Core hashes `$event['instance']` for us
// Necessary data to identify an individual event.
// `$event['action']` is hashed to avoid information disclosure.
// Core hashes `$event['instance']` for us.
$event_data_public = array(
'timestamp' => $event['timestamp'],
'action' => md5( $event['action'] ),
'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'] ) ) {
$internal_events[] = $event_data_public;
} else {
......@@ -117,13 +138,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 ) {
$current_events = $this->reduce_queue( $current_events );
}
// Combine with Internal Events
// TODO: un-nest array, which is nested for legacy reasons
// Combine with Internal Events.
// TODO: un-nest array, which is nested for legacy reasons.
return array(
'events' => array_merge( $current_events, $internal_events ),
);
......@@ -133,22 +154,21 @@ class Events extends Singleton {
* 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
*
* @param $event array Event data
*
* @param array $event Event data.
* @return bool
*/
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'] ) ) {
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 ) ) {
return true;
}
// Remove or reschedule the empty event
// Remove or reschedule the empty event.
if ( false === $event['args']['schedule'] ) {
wp_unschedule_event( $event['timestamp'], $event['action'], $event['args']['args'] );
} else {
......@@ -163,28 +183,28 @@ class Events extends Singleton {
/**
* 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
*/
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();
$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 {
// 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 ) {
$action = $event['action'];
// Prime the count
// Prime the count.
if ( ! isset( $action_counts[ $action ] ) ) {
$action_counts[ $action ] = 0;
}
// Check and do the move
// Check and do the move.
if ( $action_counts[ $action ] < $i ) {
$reduced_queue[] = $event;
$action_counts[ $action ]++;
......@@ -192,7 +212,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 ) ) {
break;
} else {
......@@ -201,7 +221,7 @@ class Events extends Singleton {
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.
......@@ -213,7 +233,7 @@ class Events extends Singleton {
* 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 ) {
$reduced_queue = array_slice( $reduced_queue, 0, JOB_QUEUE_SIZE );
}
......@@ -224,25 +244,29 @@ class Events extends Singleton {
/**
* Execute a specific event
*
* @param $timestamp int Unix timestamp
* @param $action string 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 $force bool Run event regardless of timestamp or lock status? eg, when executing jobs via wp-cli
*
* @param int $timestamp Unix timestamp.
* @param string $action md5 hash of the action used when the event is registered.
* @param string $instance md5 hash of the event's arguments array, which Core uses to index the `cron` option.
* @param bool $force Run event regardless of timestamp or lock status? eg, when executing jobs via wp-cli.
* @return array|\WP_Error
*/
public function run_event( $timestamp, $action, $instance, $force = false ) {
// Validate input data
// Validate input data.
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() ) {
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, ) );
/* translators: 1: Job identifier */
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(
'timestamp' => $timestamp,
'action_hashed' => $action,
......@@ -252,57 +276,67 @@ class Events extends Singleton {
// Nothing to do...
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, ) );
/* translators: 1: Job identifier */
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 );
// Limit how many events are processed concurrently, unless explicitly bypassed
// Limit how many events are processed concurrently, unless explicitly bypassed.
if ( ! $force ) {
// Prepare event-level lock
// Prepare event-level lock.
$this->prime_event_action_lock( $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, ) );
/* translators: 1: Event action, 2: Event arguments */
return new \WP_Error( 'no-free-threads', sprintf( __( 'No resources available to run the job with 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;
add_action( 'shutdown', array( $this, 'do_lock_cleanup_on_shutdown' ) );
}
// Mark the event completed, and reschedule if desired
// Core does this before running the job, so we respect that
// Mark the event completed, and reschedule if desired.
// Core does this before running the job, so we respect that.
$this->update_event_record( $event );
// Run the event
// Run the event.
try {
do_action_ref_array( $event->action, $event->args );
} 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 );
$return = array(
'success' => false,
/* translators: 1: Event action, 2: Event arguments, 3: Throwable error, 4: Line number that raised Throwable error */
'message' => sprintf( __( 'Callback for job with action `%1$s` and arguments `%2$s` raised a Throwable - %3$s in %4$s on line %5$d.', 'automattic-cron-control' ), $event->action, maybe_serialize( $event->args ), $t->getMessage(), $t->getFile(), $t->getLine() ),
);
}
// 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 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;
remove_action( 'shutdown', array( $this, 'do_lock_cleanup_on_shutdown' ) );
$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 ) ) {
$return = array(
'success' => true,
/* translators: 1: Event action, 2: Event arguments */
'message' => sprintf( __( 'Job with action `%1$s` and arguments `%2$s` executed.', 'automattic-cron-control' ), $event->action, maybe_serialize( $event->args ) ),
);
}
......@@ -315,7 +349,7 @@ class Events extends Singleton {
*
* 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 ) {
Lock::prime_lock( $this->get_lock_key_for_event_action( $event ), JOB_LOCK_EXPIRY_IN_MINUTES * \MINUTE_IN_SECONDS );
......@@ -324,12 +358,11 @@ class Events extends Singleton {
/**
* Are resources available to run this event?
*
* @param object $event Event data
*
* @param object $event Event data.
* @return bool
*/
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;
if ( isset( $this->concurrent_action_whitelist[ $event->action ] ) ) {
......@@ -341,13 +374,13 @@ class Events extends Singleton {
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 ) ) {
return true;
}
// 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
// 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 ( ! Lock::check_lock( self::LOCK, JOB_CONCURRENCY_LIMIT ) ) {
$this->reset_event_lock( $event );
return false;
......@@ -360,23 +393,22 @@ class Events extends Singleton {
/**
* Free locks after event completes
*
* @param object $event Event data
* @param object $event Event data.
*/
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 ) ) {
Lock::free_lock( self::LOCK );
}
// Reset individual event lock
// Reset individual event lock.
$this->reset_event_lock( $event );
}
/**
* Frees the lock for an individual event
*
* @param object $event Event data
*
* @param object $event Event data.
* @return bool
*/
private function reset_event_lock( $event ) {
......@@ -393,37 +425,38 @@ class Events extends Singleton {
/**
* 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
*/
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 );
}
/**
* Mark an event completed, and reschedule when requested
*
* @param object $event Event data.
*/
private function update_event_record( $event ) {
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();
$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 ] ) ) {
$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 ) {
$interval = $event->interval;
}
// If we have an interval, update the existing event entry
// If we have an interval, update the existing event entry.
if ( 0 != $interval ) {
// Determine new timestamp, according to how `wp_reschedule_event()` does
// Determine new timestamp, according to how `wp_reschedule_event()` does.
$now = time();
$new_timestamp = $event->timestamp;
......@@ -433,22 +466,22 @@ class Events extends Singleton {
$new_timestamp = $now + ( $interval - ( ( $now - $new_timestamp ) % $interval ) );
}
// Build the expected arguments format
// Build the expected arguments format.
$event_args = array(
'schedule' => $event->schedule,
'args' => $event->args,
'interval' => $interval,
);
// Update event store
// Update event store.
schedule_event( $new_timestamp, $event->action, $event_args, $event->ID );
// If the event could be rescheduled, don't then delete it :)
// If the event could be rescheduled, don't then delete it.
return;
}
}
// Either event doesn't recur, or the interval couldn't be determined
// Either event doesn't recur, or the interval couldn't be determined.
delete_event( $event->timestamp, $event->action, $event->instance );
}
......@@ -488,15 +521,13 @@ class Events extends Singleton {
/**
* Set automatic execution status
*
* 0 if run is enabled, 1 if run is disabled indefinitely, otherwise timestamp when execution will resume
*
* @param int $new_status
* @param int $new_status 0 if run is enabled, 1 if run is disabled indefinitely, otherwise timestamp when execution will resume.
* @return bool
*/
public function update_run_status( $new_status ) {
$new_status = absint( $new_status );
// Don't store a past timestamp
// Don't store a past timestamp.
if ( $new_status > 1 && $new_status < time() ) {
return false;
}
......