Commit 1baa7ea5 authored by Erick Hitter's avatar Erick Hitter
Browse files

Merge branch 'master' into add/cli-orchestration

parents 827fa450 cc626196
# Cron Control
Cron Control
============
Execute WordPress cron events in parallel, using a custom post type for event storage.
Using REST API endpoints (requires WordPress 4.4+), an event queue is produced and events are triggered.
## PHP Compatibility
Cron Control requires PHP 7 or greater to be able to catch fatal errors triggered by event callbacks. While the plugin may work with previous versions of PHP, internal locks may become deadlocked if callbacks fail.
## Event Concurrency
In some circumstances, multiple events with the same action can safely run in parallel. This is usually not the case, largely due to Core's alloptions, but sometimes an event is written in a way that we can support concurrent executions.
......
......@@ -17,6 +17,8 @@ class Events extends Singleton {
private $concurrent_action_whitelist = array();
private $running_event = null;
/**
* Register hooks
*/
......@@ -286,6 +288,10 @@ class Events extends Singleton {
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, ) );
}
// 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
......@@ -293,17 +299,38 @@ class Events extends Singleton {
$this->update_event_record( $event );
// Run the event
do_action_ref_array( $event->action, $event->args );
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()`
do_action( 'a8c_cron_control_event_threw_catchable_error', $event, $t );
$return = array(
'success' => false,
'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 process for the next event, unless it wasn'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
$this->running_event = null;
remove_action( 'shutdown', array( $this, 'do_lock_cleanup_on_shutdown' ) );
$this->do_lock_cleanup( $event );
}
return array(
'success' => true,
'message' => sprintf( __( 'Job with action `%1$s` and arguments `%2$s` executed.', 'automattic-cron-control' ), $event->action, maybe_serialize( $event->args ) ),
);
// Callback didn't trigger a Throwable, indicating it succeeded
if ( ! isset( $return ) ) {
$return = array(
'success' => true,
'message' => sprintf( __( 'Job with action `%1$s` and arguments `%2$s` executed.', 'automattic-cron-control' ), $event->action, maybe_serialize( $event->args ) ),
);
}
return $return;
}
/**
......@@ -311,7 +338,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 array Event data
* @param $event object 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 );
......@@ -448,6 +475,23 @@ class Events extends Singleton {
delete_event( $event->timestamp, $event->action, $event->instance );
}
/**
* If event execution throws uncatchable error, free locks
*
* Covers situations such as timeouts and memory exhaustion, which aren't \Throwable errors
*
* Under normal conditions, this callback isn't hooked to `shutdown`
*/
public function do_lock_cleanup_on_shutdown() {
if ( is_null( $this->running_event ) ) {
return;
}
do_action( 'a8c_cron_control_freeing_event_locks_after_uncaught_error', $this->running_event );
$this->do_lock_cleanup( $this->running_event );
}
/**
* Return status of automatic event execution
*
......
......@@ -31,7 +31,7 @@ class Lock extends \WP_CLI_Command {
\WP_CLI::error( sprintf( __( 'Specify an action', 'automattic-cron-control' ) ) );
}
$lock_name = \Automattic\WP\Cron_Control\Events::instance()->get_lock_key_for_event_action( array( 'action' => $args[0], ) );
$lock_name = \Automattic\WP\Cron_Control\Events::instance()->get_lock_key_for_event_action( (object) array( 'action' => $args[0], ) );
$lock_limit = 1;
$lock_description = __( "This lock prevents concurrent executions of events with the same action, regardless of the action's arguments.", 'automattic-cron-control' );
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment