Commit 16d4501a authored by Erick Hitter's avatar Erick Hitter Committed by GitHub
Browse files

Merge pull request #12 from Automattic/develop

Improve caching and event CPT handling
parents c4642708 31ec80a0
......@@ -16,6 +16,8 @@ class Cron_Options_CPT extends Singleton {
const POST_STATUS_PENDING = 'inherit';
const POST_STATUS_COMPLETED = 'trash';
const CACHE_KEY = 'a8c_cron_ctrl_option';
private $posts_to_clean = array();
private $option_before_unscheduling = null;
......@@ -64,6 +66,16 @@ class Cron_Options_CPT extends Singleton {
* Override cron option requests with data from CPT
*/
public function get_option() {
// Use cached value for reads, except when we're unscheduling and state is important
if ( ! $this->is_unscheduling() ) {
$cached_option = wp_cache_get( self::CACHE_KEY, null, true );
if ( false !== $cached_option ) {
return $cached_option;
}
}
// Start building a new cron option
$cron_array = array(
'version' => 2, // Core versions the cron array; without this, events will continually requeue
);
......@@ -133,6 +145,9 @@ class Cron_Options_CPT extends Singleton {
$this->option_before_unscheduling = null;
}
// Cache the results, bearing in mind that they won't be used during unscheduling events
wp_cache_set( self::CACHE_KEY, $cron_array, null, 1 * \HOUR_IN_SECONDS );
return $cron_array;
}
......@@ -173,22 +188,7 @@ class Cron_Options_CPT extends Singleton {
$job_exists = $this->job_exists( $event['timestamp'], $event['action'], $event['instance'] );
if ( ! $job_exists ) {
// Build minimum information needed to create a post
$job_post = array(
'post_title' => $this->event_title( $event['timestamp'], $event['action'], $event['instance'] ),
'post_name' => $this->event_name( $event['timestamp'], $event['action'], $event['instance'] ),
'post_content_filtered' => maybe_serialize( array(
'action' => $event['action'],
'instance' => $event['instance'],
'args' => $event['args'],
) ),
'post_date' => date( 'Y-m-d H:i:s', $event['timestamp'] ),
'post_date_gmt' => date( 'Y-m-d H:i:s', $event['timestamp'] ),
'post_type' => self::POST_TYPE,
'post_status' => self::POST_STATUS_PENDING,
);
$this->create_job( $job_post );
$this->create_job( $event['timestamp'], $event['action'], $event['args'] );
}
}
}
......@@ -201,26 +201,21 @@ class Cron_Options_CPT extends Singleton {
/**
* Retrieve list of jobs, respecting whether or not the CPT is registered
*
* `WP_Query` also can't be used before `init` due to capabilities checks
* Uses a direct query to avoid stale caches that result in duplicate events
*/
private function get_jobs( $args ) {
// If called before `init`, we need to query directly because post types aren't registered earlier
if ( did_action( 'init' ) ) {
return get_posts( $args );
} else {
global $wpdb;
$orderby = 'date' === $args['orderby'] ? 'post_date' : $args['orderby'];
global $wpdb;
if ( isset( $args['paged'] ) ) {
$paged = max( 0, $args['paged'] - 1 );
$offset = $paged * $args['posts_per_page'];
} else {
$offset = 0;
}
$orderby = 'date' === $args['orderby'] ? 'post_date' : $args['orderby'];
return $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->posts} WHERE post_type = %s AND post_status = %s ORDER BY %s %s LIMIT %d,%d;", $args['post_type'], $args['post_status'], $orderby, $args['order'], $offset, $args['posts_per_page'] ), 'OBJECT' );
if ( isset( $args['paged'] ) ) {
$paged = max( 0, $args['paged'] - 1 );
$offset = $paged * $args['posts_per_page'];
} else {
$offset = 0;
}
return $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->posts} WHERE post_type = %s AND post_status = %s ORDER BY %s %s LIMIT %d,%d;", $args['post_type'], $args['post_status'], $orderby, $args['order'], $offset, $args['posts_per_page'] ), 'OBJECT' );
}
/**
......@@ -241,57 +236,69 @@ class Cron_Options_CPT extends Singleton {
}
/**
* Create a job post, respecting whether or not Core is ready for CPTs
* Create a post object for a given event
*
* `wp_insert_post()` can't be called as early as we need, in part because of the capabilities checks Core performs
* Can't call `wp_insert_post()` because `wp_unique_post_slug()` breaks the plugin's expectations
* Also doesn't call `wp_insert_post()` because this function is needed before post types and capabilities are ready.
*/
private function create_job( $job_post ) {
public function create_job( $timestamp, $action, $args ) {
// Limit how many events to insert at once
if ( ! Lock::check_lock( self::LOCK, 5 ) ) {
return false;
}
// If called before `init`, we need to insert directly because post types aren't registered earlier
if ( did_action( 'init' ) ) {
wp_insert_post( $job_post );
} else {
global $wpdb;
global $wpdb;
// Additional data needed to manually create a post
$job_post = wp_parse_args( $job_post, array(
'post_author' => 0,
'comment_status' => 'closed',
'ping_status' => 'closed',
'post_parent' => 0,
'post_modified' => current_time( 'mysql' ),
'post_modified_gmt' => current_time( 'mysql', true ),
) );
// Build minimum information needed to create a post
$instance = md5( serialize( $args['args'] ) );
$job_post = array(
'post_title' => $this->event_title( $timestamp, $action, $instance ),
'post_name' => $this->event_name( $timestamp, $action, $instance ),
'post_content_filtered' => maybe_serialize( array(
'action' => $action,
'instance' => $instance,
'args' => $args,
) ),
'post_date' => date( 'Y-m-d H:i:s', $timestamp ),
'post_date_gmt' => date( 'Y-m-d H:i:s', $timestamp ),
'post_modified' => current_time( 'mysql' ),
'post_modified_gmt' => current_time( 'mysql', true ),
'post_type' => self::POST_TYPE,
'post_status' => self::POST_STATUS_PENDING,
'post_author' => 0,
'post_parent' => 0,
'comment_status' => 'closed',
'ping_status' => 'closed',
);
// Some sanitization in place of `sanitize_post()`, which we can't use this early
foreach ( array( 'post_title', 'post_name', 'post_content_filtered' ) as $field ) {
$job_post[ $field ] = sanitize_text_field( $job_post[ $field ] );
}
// Some sanitization in place of `sanitize_post()`, which we can't use this early
foreach ( array( 'post_title', 'post_name', 'post_content_filtered' ) as $field ) {
$job_post[ $field ] = sanitize_text_field( $job_post[ $field ] );
}
// Duplicate some processing performed in `wp_insert_post()`
$charset = $wpdb->get_col_charset( $wpdb->posts, 'post_title' );
if ( 'utf8' === $charset ) {
$job_post['post_title'] = wp_encode_emoji( $job_post['post_title'] );
}
// Duplicate some processing performed in `wp_insert_post()`
$charset = $wpdb->get_col_charset( $wpdb->posts, 'post_title' );
if ( 'utf8' === $charset ) {
$job_post['post_title'] = wp_encode_emoji( $job_post['post_title'] );
}
$job_post = wp_unslash( $job_post );
$job_post = wp_unslash( $job_post );
// Set this so it isn't empty, even though it serves us no purpose
$job_post['guid'] = esc_url( add_query_arg( self::POST_TYPE, $job_post['post_name'], home_url( '/' ) ) );
// Set this so it isn't empty, even though it serves us no purpose
$job_post['guid'] = esc_url( add_query_arg( self::POST_TYPE, $job_post['post_name'], home_url( '/' ) ) );
// Create the post
$inserted = $wpdb->insert( $wpdb->posts, $job_post );
// Create the post
$inserted = $wpdb->insert( $wpdb->posts, $job_post );
// Clear caches for new posts once the post type is registered
if ( $inserted ) {
$this->posts_to_clean[] = $wpdb->insert_id;
}
// Clear caches for new posts once the post type is registered
if ( $inserted ) {
$this->posts_to_clean[] = $wpdb->insert_id;
}
// Delete internal cache
wp_cache_delete( self::CACHE_KEY );
// Allow more events to be created
Lock::free_lock( self::LOCK );
}
......@@ -307,7 +314,7 @@ class Cron_Options_CPT extends Singleton {
*
* @return bool
*/
private function mark_job_completed( $timestamp, $action, $instance ) {
public function mark_job_completed( $timestamp, $action, $instance ) {
$job_post_id = $this->job_exists( $timestamp, $action, $instance, true );
if ( ! $job_post_id ) {
......@@ -334,6 +341,9 @@ class Cron_Options_CPT extends Singleton {
$this->posts_to_clean[] = $job_post_id;
}
// Delete internal cache
wp_cache_delete( self::CACHE_KEY );
return true;
}
......
......@@ -135,20 +135,14 @@ class Events extends Singleton {
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, ) );
}
// Mark the event completed, and reschedule if desired
$this->update_event_record( $event );
// Prepare environment to run job
ignore_user_abort( true );
set_time_limit( JOB_TIMEOUT_IN_MINUTES * MINUTE_IN_SECONDS );
define( 'DOING_CRON', true );
// Remove the event, and reschedule if desired
// Follows pattern Core uses in wp-cron.php
if ( false !== $event['schedule'] ) {
$reschedule_args = array( $event['timestamp'], $event['schedule'], $event['action'], $event['args'] );
call_user_func_array( 'wp_reschedule_event', $reschedule_args );
}
wp_unschedule_event( $event['timestamp'], $event['action'], $event['args'] );
// Run the event
do_action_ref_array( $event['action'], $event['args'] );
......@@ -164,6 +158,51 @@ class Events extends Singleton {
'message' => sprintf( __( 'Job with action `%1$s` and arguments `%2$s` completed in %3$d seconds.', 'automattic-cron-control' ), $event['action'], maybe_serialize( $event['args'] ), $time_end - $time_start ),
);
}
/**
* Mark an event completed, and reschedule when requested
*/
private function update_event_record( $event ) {
if ( false !== $event['schedule'] ) {
// 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
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
if ( 0 == $interval ) {
$interval = $event['interval'];
}
// If we have an interval, create a new event entry
if ( 0 != $interval ) {
// Determine new timestamp, according to how `wp_reschedule_event()` does
$now = time();
$new_timestamp = $event['timestamp'];
if ( $new_timestamp >= $now ) {
$new_timestamp = $now + $interval;
} else {
$new_timestamp = $now + ( $interval - ( ( $now - $new_timestamp ) % $interval ) );
}
// Build the expected arguments format
$event_args = array(
'schedule' => $event['schedule'],
'args' => $event['args'],
'interval' => $interval,
);
Cron_Options_CPT::instance()->create_job( $new_timestamp, $event['action'], $event_args );
}
}
Cron_Options_CPT::instance()->mark_job_completed( $event['timestamp'], $event['action'], $event['instance'] );
}
}
Events::instance();
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