Plugin Updates

This commit is contained in:
Tony Volpe
2024-03-19 15:33:31 +00:00
parent ff5b56dc44
commit 3a70a6e4bf
317 changed files with 8178 additions and 2933 deletions

View File

@@ -13,10 +13,15 @@ class ActionScheduler_ActionFactory {
* @param array $args Args to pass to callbacks when the hook is triggered.
* @param ActionScheduler_Schedule $schedule The action's schedule.
* @param string $group A group to put the action in.
* phpcs:ignore Squiz.Commenting.FunctionComment.ExtraParamComment
* @param int $priority The action priority.
*
* @return ActionScheduler_Action An instance of the stored action.
*/
public function get_stored_action( $status, $hook, array $args = array(), ActionScheduler_Schedule $schedule = null, $group = '' ) {
// The 6th parameter ($priority) is not formally declared in the method signature to maintain compatibility with
// third-party subclasses created before this param was added.
$priority = func_num_args() >= 6 ? (int) func_get_arg( 5 ) : 10;
switch ( $status ) {
case ActionScheduler_Store::STATUS_PENDING:
@@ -36,17 +41,19 @@ class ActionScheduler_ActionFactory {
$action_class = apply_filters( 'action_scheduler_stored_action_class', $action_class, $status, $hook, $args, $schedule, $group );
$action = new $action_class( $hook, $args, $schedule, $group );
$action->set_priority( $priority );
/**
* Allow 3rd party code to change the instantiated action for a given hook, args, schedule and group.
*
* @param ActionScheduler_Action $action The instantiated action.
* @param string $hook The instantiated action's hook.
* @param array $args The instantiated action's args.
* @param ActionScheduler_Action $action The instantiated action.
* @param string $hook The instantiated action's hook.
* @param array $args The instantiated action's args.
* @param ActionScheduler_Schedule $schedule The instantiated action's schedule.
* @param string $group The instantiated action's group.
* @param string $group The instantiated action's group.
* @param int $priority The action priority.
*/
return apply_filters( 'action_scheduler_stored_action_instance', $action, $hook, $args, $schedule, $group );
return apply_filters( 'action_scheduler_stored_action_instance', $action, $hook, $args, $schedule, $group, $priority );
}
/**
@@ -229,9 +236,100 @@ class ActionScheduler_ActionFactory {
$schedule_class = get_class( $schedule );
$new_schedule = new $schedule( $next, $schedule->get_recurrence(), $schedule->get_first_date() );
$new_action = new ActionScheduler_Action( $action->get_hook(), $action->get_args(), $new_schedule, $action->get_group() );
$new_action->set_priority( $action->get_priority() );
return $this->store( $new_action );
}
/**
* Creates a scheduled action.
*
* This general purpose method can be used in place of specific methods such as async(),
* async_unique(), single() or single_unique(), etc.
*
* @internal Not intended for public use, should not be overriden by subclasses.
*
* @param array $options {
* Describes the action we wish to schedule.
*
* @type string $type Must be one of 'async', 'cron', 'recurring', or 'single'.
* @type string $hook The hook to be executed.
* @type array $arguments Arguments to be passed to the callback.
* @type string $group The action group.
* @type bool $unique If the action should be unique.
* @type int $when Timestamp. Indicates when the action, or first instance of the action in the case
* of recurring or cron actions, becomes due.
* @type int|string $pattern Recurrence pattern. This is either an interval in seconds for recurring actions
* or a cron expression for cron actions.
* @type int $priority Lower values means higher priority. Should be in the range 0-255.
* }
*
* @return int The action ID. Zero if there was an error scheduling the action.
*/
public function create( array $options = array() ) {
$defaults = array(
'type' => 'single',
'hook' => '',
'arguments' => array(),
'group' => '',
'unique' => false,
'when' => time(),
'pattern' => null,
'priority' => 10,
);
$options = array_merge( $defaults, $options );
// Cron/recurring actions without a pattern are treated as single actions (this gives calling code the ability
// to use functions like as_schedule_recurring_action() to schedule recurring as well as single actions).
if ( ( 'cron' === $options['type'] || 'recurring' === $options['type'] ) && empty( $options['pattern'] ) ) {
$options['type'] = 'single';
}
switch ( $options['type'] ) {
case 'async':
$schedule = new ActionScheduler_NullSchedule();
break;
case 'cron':
$date = as_get_datetime_object( $options['when'] );
$cron = CronExpression::factory( $options['pattern'] );
$schedule = new ActionScheduler_CronSchedule( $date, $cron );
break;
case 'recurring':
$date = as_get_datetime_object( $options['when'] );
$schedule = new ActionScheduler_IntervalSchedule( $date, $options['pattern'] );
break;
case 'single':
$date = as_get_datetime_object( $options['when'] );
$schedule = new ActionScheduler_SimpleSchedule( $date );
break;
default:
error_log( "Unknown action type '{$options['type']}' specified when trying to create an action for '{$options['hook']}'." );
return 0;
}
$action = new ActionScheduler_Action( $options['hook'], $options['arguments'], $schedule, $options['group'] );
$action->set_priority( $options['priority'] );
$action_id = 0;
try {
$action_id = $options['unique'] ? $this->store_unique_action( $action ) : $this->store( $action );
} catch ( Exception $e ) {
error_log(
sprintf(
/* translators: %1$s is the name of the hook to be enqueued, %2$s is the exception message. */
__( 'Caught exception while enqueuing action "%1$s": %2$s', 'action-scheduler' ),
$options['hook'],
$e->getMessage()
)
);
}
return $action_id;
}
/**
* Save action to database.
*

View File

@@ -4,7 +4,6 @@
* Class ActionScheduler_Compatibility
*/
class ActionScheduler_Compatibility {
/**
* Converts a shorthand byte value to an integer byte value.
*
@@ -89,21 +88,18 @@ class ActionScheduler_Compatibility {
$limit = (int) $limit;
$max_execution_time = (int) ini_get( 'max_execution_time' );
/*
* If the max execution time is already unlimited (zero), or if it exceeds or is equal to the proposed
* limit, there is no reason for us to make further changes (we never want to lower it).
*/
if (
0 === $max_execution_time
|| ( $max_execution_time >= $limit && $limit !== 0 )
) {
// If the max execution time is already set to zero (unlimited), there is no reason to make a further change.
if ( 0 === $max_execution_time ) {
return;
}
// Whichever of $max_execution_time or $limit is higher is the amount by which we raise the time limit.
$raise_by = 0 === $limit || $limit > $max_execution_time ? $limit : $max_execution_time;
if ( function_exists( 'wc_set_time_limit' ) ) {
wc_set_time_limit( $limit );
wc_set_time_limit( $raise_by );
} elseif ( function_exists( 'set_time_limit' ) && false === strpos( ini_get( 'disable_functions' ), 'set_time_limit' ) && ! ini_get( 'safe_mode' ) ) { // phpcs:ignore PHPCompatibility.IniDirectives.RemovedIniDirectives.safe_modeDeprecatedRemoved
@set_time_limit( $limit ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
@set_time_limit( $raise_by ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
}
}
}

View File

@@ -252,7 +252,7 @@ class ActionScheduler_ListTable extends ActionScheduler_Abstract_ListTable {
*/
protected function get_recurrence( $action ) {
$schedule = $action->get_schedule();
if ( $schedule->is_recurring() ) {
if ( $schedule->is_recurring() && method_exists( $schedule, 'get_recurrence' ) ) {
$recurrence = $schedule->get_recurrence();
if ( is_numeric( $recurrence ) ) {
@@ -471,7 +471,7 @@ class ActionScheduler_ListTable extends ActionScheduler_Abstract_ListTable {
return __( 'async', 'action-scheduler' );
}
if ( ! $schedule->get_date() ) {
if ( ! method_exists( $schedule, 'get_date' ) || ! $schedule->get_date() ) {
return '0000-00-00 00:00:00';
}
@@ -502,7 +502,20 @@ class ActionScheduler_ListTable extends ActionScheduler_Abstract_ListTable {
*/
protected function bulk_delete( array $ids, $ids_sql ) {
foreach ( $ids as $id ) {
$this->store->delete_action( $id );
try {
$this->store->delete_action( $id );
} catch ( Exception $e ) {
// A possible reason for an exception would include a scenario where the same action is deleted by a
// concurrent request.
error_log(
sprintf(
/* translators: 1: action ID 2: exception message. */
__( 'Action Scheduler was unable to delete action %1$d. Reason: %2$s', 'action-scheduler' ),
$id,
$e->getMessage()
)
);
}
}
}

View File

@@ -24,7 +24,37 @@ class ActionScheduler_OptionLock extends ActionScheduler_Lock {
* @bool True if lock value has changed, false if not or if set failed.
*/
public function set( $lock_type ) {
return update_option( $this->get_key( $lock_type ), time() + $this->get_duration( $lock_type ) );
global $wpdb;
$lock_key = $this->get_key( $lock_type );
$existing_lock_value = $this->get_existing_lock( $lock_type );
$new_lock_value = $this->new_lock_value( $lock_type );
// The lock may not exist yet, or may have been deleted.
if ( empty( $existing_lock_value ) ) {
return (bool) $wpdb->insert(
$wpdb->options,
array(
'option_name' => $lock_key,
'option_value' => $new_lock_value,
'autoload' => 'no',
)
);
}
if ( $this->get_expiration_from( $existing_lock_value ) >= time() ) {
return false;
}
// Otherwise, try to obtain the lock.
return (bool) $wpdb->update(
$wpdb->options,
array( 'option_value' => $new_lock_value ),
array(
'option_name' => $lock_key,
'option_value' => $existing_lock_value,
)
);
}
/**
@@ -34,7 +64,30 @@ class ActionScheduler_OptionLock extends ActionScheduler_Lock {
* @return bool|int False if no lock is set, otherwise the timestamp for when the lock is set to expire.
*/
public function get_expiration( $lock_type ) {
return get_option( $this->get_key( $lock_type ) );
return $this->get_expiration_from( $this->get_existing_lock( $lock_type ) );
}
/**
* Given the lock string, derives the lock expiration timestamp (or false if it cannot be determined).
*
* @param string $lock_value String containing a timestamp, or pipe-separated combination of unique value and timestamp.
*
* @return false|int
*/
private function get_expiration_from( $lock_value ) {
$lock_string = explode( '|', $lock_value );
// Old style lock?
if ( count( $lock_string ) === 1 && is_numeric( $lock_string[0] ) ) {
return (int) $lock_string[0];
}
// New style lock?
if ( count( $lock_string ) === 2 && is_numeric( $lock_string[1] ) ) {
return (int) $lock_string[1];
}
return false;
}
/**
@@ -46,4 +99,37 @@ class ActionScheduler_OptionLock extends ActionScheduler_Lock {
protected function get_key( $lock_type ) {
return sprintf( 'action_scheduler_lock_%s', $lock_type );
}
/**
* Supplies the existing lock value, or an empty string if not set.
*
* @param string $lock_type A string to identify different lock types.
*
* @return string
*/
private function get_existing_lock( $lock_type ) {
global $wpdb;
// Now grab the existing lock value, if there is one.
return (string) $wpdb->get_var(
$wpdb->prepare(
"SELECT option_value FROM $wpdb->options WHERE option_name = %s",
$this->get_key( $lock_type )
)
);
}
/**
* Supplies a lock value consisting of a unique value and the current timestamp, which are separated by a pipe
* character.
*
* Example: (string) "649de012e6b262.09774912|1688068114"
*
* @param string $lock_type A string to identify different lock types.
*
* @return string
*/
private function new_lock_value( $lock_type ) {
return uniqid( '', true ) . '|' . ( time() + $this->get_duration( $lock_type ) );
}
}

View File

@@ -18,6 +18,14 @@ class ActionScheduler_QueueCleaner {
*/
private $month_in_seconds = 2678400;
/**
* @var string[] Default list of statuses purged by the cleaner process.
*/
private $default_statuses_to_purge = [
ActionScheduler_Store::STATUS_COMPLETE,
ActionScheduler_Store::STATUS_CANCELED,
];
/**
* ActionScheduler_QueueCleaner constructor.
*
@@ -29,46 +37,113 @@ class ActionScheduler_QueueCleaner {
$this->batch_size = $batch_size;
}
/**
* Default queue cleaner process used by queue runner.
*
* @return array
*/
public function delete_old_actions() {
/**
* Filter the minimum scheduled date age for action deletion.
*
* @param int $retention_period Minimum scheduled age in seconds of the actions to be deleted.
*/
$lifespan = apply_filters( 'action_scheduler_retention_period', $this->month_in_seconds );
$cutoff = as_get_datetime_object($lifespan.' seconds ago');
$statuses_to_purge = array(
ActionScheduler_Store::STATUS_COMPLETE,
ActionScheduler_Store::STATUS_CANCELED,
);
try {
$cutoff = as_get_datetime_object( $lifespan . ' seconds ago' );
} catch ( Exception $e ) {
_doing_it_wrong(
__METHOD__,
sprintf(
/* Translators: %s is the exception message. */
esc_html__( 'It was not possible to determine a valid cut-off time: %s.', 'action-scheduler' ),
esc_html( $e->getMessage() )
),
'3.5.5'
);
return array();
}
/**
* Filter the statuses when cleaning the queue.
*
* @param string[] $default_statuses_to_purge Action statuses to clean.
*/
$statuses_to_purge = (array) apply_filters( 'action_scheduler_default_cleaner_statuses', $this->default_statuses_to_purge );
return $this->clean_actions( $statuses_to_purge, $cutoff, $this->get_batch_size() );
}
/**
* Delete selected actions limited by status and date.
*
* @param string[] $statuses_to_purge List of action statuses to purge. Defaults to canceled, complete.
* @param DateTime $cutoff_date Date limit for selecting actions. Defaults to 31 days ago.
* @param int|null $batch_size Maximum number of actions per status to delete. Defaults to 20.
* @param string $context Calling process context. Defaults to `old`.
* @return array Actions deleted.
*/
public function clean_actions( array $statuses_to_purge, DateTime $cutoff_date, $batch_size = null, $context = 'old' ) {
$batch_size = $batch_size !== null ? $batch_size : $this->batch_size;
$cutoff = $cutoff_date !== null ? $cutoff_date : as_get_datetime_object( $this->month_in_seconds . ' seconds ago' );
$lifespan = time() - $cutoff->getTimestamp();
if ( empty( $statuses_to_purge ) ) {
$statuses_to_purge = $this->default_statuses_to_purge;
}
$deleted_actions = [];
foreach ( $statuses_to_purge as $status ) {
$actions_to_delete = $this->store->query_actions( array(
'status' => $status,
'modified' => $cutoff,
'modified_compare' => '<=',
'per_page' => $this->get_batch_size(),
'per_page' => $batch_size,
'orderby' => 'none',
) );
foreach ( $actions_to_delete as $action_id ) {
try {
$this->store->delete_action( $action_id );
} catch ( Exception $e ) {
$deleted_actions = array_merge( $deleted_actions, $this->delete_actions( $actions_to_delete, $lifespan, $context ) );
}
/**
* Notify 3rd party code of exceptions when deleting a completed action older than the retention period
*
* This hook provides a way for 3rd party code to log or otherwise handle exceptions relating to their
* actions.
*
* @since 2.0.0
*
* @param int $action_id The scheduled actions ID in the data store
* @param Exception $e The exception thrown when attempting to delete the action from the data store
* @param int $lifespan The retention period, in seconds, for old actions
* @param int $count_of_actions_to_delete The number of old actions being deleted in this batch
*/
do_action( 'action_scheduler_failed_old_action_deletion', $action_id, $e, $lifespan, count( $actions_to_delete ) );
}
return $deleted_actions;
}
/**
* @param int[] $actions_to_delete List of action IDs to delete.
* @param int $lifespan Minimum scheduled age in seconds of the actions being deleted.
* @param string $context Context of the delete request.
* @return array Deleted action IDs.
*/
private function delete_actions( array $actions_to_delete, $lifespan = null, $context = 'old' ) {
$deleted_actions = [];
if ( $lifespan === null ) {
$lifespan = $this->month_in_seconds;
}
foreach ( $actions_to_delete as $action_id ) {
try {
$this->store->delete_action( $action_id );
$deleted_actions[] = $action_id;
} catch ( Exception $e ) {
/**
* Notify 3rd party code of exceptions when deleting a completed action older than the retention period
*
* This hook provides a way for 3rd party code to log or otherwise handle exceptions relating to their
* actions.
*
* @param int $action_id The scheduled actions ID in the data store
* @param Exception $e The exception thrown when attempting to delete the action from the data store
* @param int $lifespan The retention period, in seconds, for old actions
* @param int $count_of_actions_to_delete The number of old actions being deleted in this batch
* @since 2.0.0
*
*/
do_action( "action_scheduler_failed_{$context}_action_deletion", $action_id, $e, $lifespan, count( $actions_to_delete ) );
}
}
return $deleted_actions;
}
/**

View File

@@ -103,9 +103,12 @@ class ActionScheduler_QueueRunner extends ActionScheduler_Abstract_QueueRunner {
* should dispatch a request to process pending actions.
*/
public function maybe_dispatch_async_request() {
if ( is_admin() && ! ActionScheduler::lock()->is_locked( 'async-request-runner' ) ) {
// Only start an async queue at most once every 60 seconds
ActionScheduler::lock()->set( 'async-request-runner' );
// Only start an async queue at most once every 60 seconds.
if (
is_admin()
&& ! ActionScheduler::lock()->is_locked( 'async-request-runner' )
&& ActionScheduler::lock()->set( 'async-request-runner' )
) {
$this->async_request->maybe_dispatch();
}
}
@@ -185,9 +188,15 @@ class ActionScheduler_QueueRunner extends ActionScheduler_Abstract_QueueRunner {
protected function clear_caches() {
/*
* Calling wp_cache_flush_runtime() lets us clear the runtime cache without invalidating the external object
* cache, so we will always prefer this when it is available (but it was only introduced in WordPress 6.0).
* cache, so we will always prefer this method (as compared to calling wp_cache_flush()) when it is available.
*
* However, this function was only introduced in WordPress 6.0. Additionally, the preferred way of detecting if
* it is supported changed in WordPress 6.1 so we use two different methods to decide if we should utilize it.
*/
if ( function_exists( 'wp_cache_flush_runtime' ) ) {
$flushing_runtime_cache_explicitly_supported = function_exists( 'wp_cache_supports' ) && wp_cache_supports( 'flush_runtime' );
$flushing_runtime_cache_implicitly_supported = ! function_exists( 'wp_cache_supports' ) && function_exists( 'wp_cache_flush_runtime' );
if ( $flushing_runtime_cache_explicitly_supported || $flushing_runtime_cache_implicitly_supported ) {
wp_cache_flush_runtime();
} elseif (
! wp_using_ext_object_cache()

View File

@@ -0,0 +1,125 @@
<?php
/**
* Commands for Action Scheduler.
*/
class ActionScheduler_WPCLI_Clean_Command extends WP_CLI_Command {
/**
* Run the Action Scheduler Queue Cleaner
*
* ## OPTIONS
*
* [--batch-size=<size>]
* : The maximum number of actions to delete per batch. Defaults to 20.
*
* [--batches=<size>]
* : Limit execution to a number of batches. Defaults to 0, meaning batches will continue all eligible actions are deleted.
*
* [--status=<status>]
* : Only clean actions with the specified status. Defaults to Canceled, Completed. Define multiple statuses as a comma separated string (without spaces), e.g. `--status=complete,failed,canceled`
*
* [--before=<datestring>]
* : Only delete actions with scheduled date older than this. Defaults to 31 days. e.g `--before='7 days ago'`, `--before='02-Feb-2020 20:20:20'`
*
* [--pause=<seconds>]
* : The number of seconds to pause between batches. Default no pause.
*
* @param array $args Positional arguments.
* @param array $assoc_args Keyed arguments.
* @throws \WP_CLI\ExitException When an error occurs.
*
* @subcommand clean
*/
public function clean( $args, $assoc_args ) {
// Handle passed arguments.
$batch = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batch-size', 20 ) );
$batches = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batches', 0 ) );
$status = explode( ',', WP_CLI\Utils\get_flag_value( $assoc_args, 'status', '' ) );
$status = array_filter( array_map( 'trim', $status ) );
$before = \WP_CLI\Utils\get_flag_value( $assoc_args, 'before', '' );
$sleep = \WP_CLI\Utils\get_flag_value( $assoc_args, 'pause', 0 );
$batches_completed = 0;
$actions_deleted = 0;
$unlimited = $batches === 0;
try {
$lifespan = as_get_datetime_object( $before );
} catch ( Exception $e ) {
$lifespan = null;
}
try {
// Custom queue cleaner instance.
$cleaner = new ActionScheduler_QueueCleaner( null, $batch );
// Clean actions for as long as possible.
while ( $unlimited || $batches_completed < $batches ) {
if ( $sleep && $batches_completed > 0 ) {
sleep( $sleep );
}
$deleted = count( $cleaner->clean_actions( $status, $lifespan, null,'CLI' ) );
if ( $deleted <= 0 ) {
break;
}
$actions_deleted += $deleted;
$batches_completed++;
$this->print_success( $deleted );
}
} catch ( Exception $e ) {
$this->print_error( $e );
}
$this->print_total_batches( $batches_completed );
if ( $batches_completed > 1 ) {
$this->print_success( $actions_deleted );
}
}
/**
* Print WP CLI message about how many batches of actions were processed.
*
* @param int $batches_processed
*/
protected function print_total_batches( int $batches_processed ) {
WP_CLI::log(
sprintf(
/* translators: %d refers to the total number of batches processed */
_n( '%d batch processed.', '%d batches processed.', $batches_processed, 'action-scheduler' ),
$batches_processed
)
);
}
/**
* Convert an exception into a WP CLI error.
*
* @param Exception $e The error object.
*
* @throws \WP_CLI\ExitException
*/
protected function print_error( Exception $e ) {
WP_CLI::error(
sprintf(
/* translators: %s refers to the exception error message */
__( 'There was an error deleting an action: %s', 'action-scheduler' ),
$e->getMessage()
)
);
}
/**
* Print a success message with the number of completed actions.
*
* @param int $actions_deleted
*/
protected function print_success( int $actions_deleted ) {
WP_CLI::success(
sprintf(
/* translators: %d refers to the total number of actions deleted */
_n( '%d action deleted.', '%d actions deleted.', $actions_deleted, 'action-scheduler' ),
$actions_deleted
)
);
}
}

View File

@@ -90,7 +90,7 @@ class ActionScheduler_WPCLI_QueueRunner extends ActionScheduler_Abstract_QueueRu
$count = count( $this->actions );
$this->progress_bar = new ProgressBar(
/* translators: %d: amount of actions */
sprintf( _n( 'Running %d action', 'Running %d actions', $count, 'action-scheduler' ), number_format_i18n( $count ) ),
sprintf( _n( 'Running %d action', 'Running %d actions', $count, 'action-scheduler' ), $count ),
$count
);
}

View File

@@ -55,6 +55,9 @@ class ActionScheduler_WPCLI_Scheduler_command extends WP_CLI_Command {
* [--group=<group>]
* : Only run actions from the specified group. Omitting this option runs actions from all groups.
*
* [--exclude-groups=<groups>]
* : Run actions from all groups except the specified group(s). Define multiple groups as a comma separated string (without spaces), e.g. '--group_a,group_b'. This option is ignored when `--group` is used.
*
* [--free-memory-on=<count>]
* : The number of actions to process between freeing memory. 0 disables freeing memory. Default 50.
*
@@ -72,15 +75,16 @@ class ActionScheduler_WPCLI_Scheduler_command extends WP_CLI_Command {
*/
public function run( $args, $assoc_args ) {
// Handle passed arguments.
$batch = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batch-size', 100 ) );
$batches = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batches', 0 ) );
$clean = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'cleanup-batch-size', $batch ) );
$hooks = explode( ',', WP_CLI\Utils\get_flag_value( $assoc_args, 'hooks', '' ) );
$hooks = array_filter( array_map( 'trim', $hooks ) );
$group = \WP_CLI\Utils\get_flag_value( $assoc_args, 'group', '' );
$free_on = \WP_CLI\Utils\get_flag_value( $assoc_args, 'free-memory-on', 50 );
$sleep = \WP_CLI\Utils\get_flag_value( $assoc_args, 'pause', 0 );
$force = \WP_CLI\Utils\get_flag_value( $assoc_args, 'force', false );
$batch = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batch-size', 100 ) );
$batches = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batches', 0 ) );
$clean = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'cleanup-batch-size', $batch ) );
$hooks = explode( ',', WP_CLI\Utils\get_flag_value( $assoc_args, 'hooks', '' ) );
$hooks = array_filter( array_map( 'trim', $hooks ) );
$group = \WP_CLI\Utils\get_flag_value( $assoc_args, 'group', '' );
$exclude_groups = \WP_CLI\Utils\get_flag_value( $assoc_args, 'exclude-groups', '' );
$free_on = \WP_CLI\Utils\get_flag_value( $assoc_args, 'free-memory-on', 50 );
$sleep = \WP_CLI\Utils\get_flag_value( $assoc_args, 'pause', 0 );
$force = \WP_CLI\Utils\get_flag_value( $assoc_args, 'force', false );
ActionScheduler_DataController::set_free_ticks( $free_on );
ActionScheduler_DataController::set_sleep_time( $sleep );
@@ -88,6 +92,13 @@ class ActionScheduler_WPCLI_Scheduler_command extends WP_CLI_Command {
$batches_completed = 0;
$actions_completed = 0;
$unlimited = $batches === 0;
if ( is_callable( [ ActionScheduler::store(), 'set_claim_filter' ] ) ) {
$exclude_groups = $this->parse_comma_separated_string( $exclude_groups );
if ( ! empty( $exclude_groups ) ) {
ActionScheduler::store()->set_claim_filter('exclude-groups', $exclude_groups );
}
}
try {
// Custom queue cleaner instance.
@@ -116,6 +127,17 @@ class ActionScheduler_WPCLI_Scheduler_command extends WP_CLI_Command {
$this->print_success( $actions_completed );
}
/**
* Converts a string of comma-separated values into an array of those same values.
*
* @param string $string The string of one or more comma separated values.
*
* @return array
*/
private function parse_comma_separated_string( $string ): array {
return array_filter( str_getcsv( $string ) );
}
/**
* Print WP CLI message about how many actions are about to be processed.
*
@@ -126,9 +148,9 @@ class ActionScheduler_WPCLI_Scheduler_command extends WP_CLI_Command {
protected function print_total_actions( $total ) {
WP_CLI::log(
sprintf(
/* translators: %d refers to how many scheduled taks were found to run */
/* translators: %d refers to how many scheduled tasks were found to run */
_n( 'Found %d scheduled task', 'Found %d scheduled tasks', $total, 'action-scheduler' ),
number_format_i18n( $total )
$total
)
);
}
@@ -145,7 +167,7 @@ class ActionScheduler_WPCLI_Scheduler_command extends WP_CLI_Command {
sprintf(
/* translators: %d refers to the total number of batches executed */
_n( '%d batch executed.', '%d batches executed.', $batches_completed, 'action-scheduler' ),
number_format_i18n( $batches_completed )
$batches_completed
)
);
}
@@ -179,9 +201,9 @@ class ActionScheduler_WPCLI_Scheduler_command extends WP_CLI_Command {
protected function print_success( $actions_completed ) {
WP_CLI::success(
sprintf(
/* translators: %d refers to the total number of taskes completed */
/* translators: %d refers to the total number of tasks completed */
_n( '%d scheduled task completed.', '%d scheduled tasks completed.', $actions_completed, 'action-scheduler' ),
number_format_i18n( $actions_completed )
$actions_completed
)
);
}

View File

@@ -153,11 +153,41 @@ abstract class ActionScheduler {
add_action( 'init', array( $store, 'init' ), 1, 0 );
add_action( 'init', array( $logger, 'init' ), 1, 0 );
add_action( 'init', array( $runner, 'init' ), 1, 0 );
add_action(
'init',
/**
* Runs after the active store's init() method has been called.
*
* It would probably be preferable to have $store->init() (or it's parent method) set this itself,
* once it has initialized, however that would cause problems in cases where a custom data store is in
* use and it has not yet been updated to follow that same logic.
*/
function () {
self::$data_store_initialized = true;
/**
* Fires when Action Scheduler is ready: it is safe to use the procedural API after this point.
*
* @since 3.5.5
*/
do_action( 'action_scheduler_init' );
},
1
);
} else {
$admin_view->init();
$store->init();
$logger->init();
$runner->init();
self::$data_store_initialized = true;
/**
* Fires when Action Scheduler is ready: it is safe to use the procedural API after this point.
*
* @since 3.5.5
*/
do_action( 'action_scheduler_init' );
}
if ( apply_filters( 'action_scheduler_load_deprecated_functions', true ) ) {
@@ -166,14 +196,13 @@ abstract class ActionScheduler {
if ( defined( 'WP_CLI' ) && WP_CLI ) {
WP_CLI::add_command( 'action-scheduler', 'ActionScheduler_WPCLI_Scheduler_command' );
WP_CLI::add_command( 'action-scheduler', 'ActionScheduler_WPCLI_Clean_Command' );
if ( ! ActionScheduler_DataController::is_migration_complete() && Controller::instance()->allow_migration() ) {
$command = new Migration_Command();
$command->register();
}
}
self::$data_store_initialized = true;
/**
* Handle WP comment cleanup after migration.
*/
@@ -192,8 +221,12 @@ abstract class ActionScheduler {
*/
public static function is_initialized( $function_name = null ) {
if ( ! self::$data_store_initialized && ! empty( $function_name ) ) {
$message = sprintf( __( '%s() was called before the Action Scheduler data store was initialized', 'action-scheduler' ), esc_attr( $function_name ) );
error_log( $message, E_WARNING );
$message = sprintf(
/* translators: %s function name. */
__( '%s() was called before the Action Scheduler data store was initialized', 'action-scheduler' ),
esc_attr( $function_name )
);
_doing_it_wrong( $function_name, $message, '3.1.6' );
}
return self::$data_store_initialized;

View File

@@ -673,24 +673,34 @@ abstract class ActionScheduler_Abstract_ListTable extends WP_List_Table {
// Helper to set 'all' filter when not set on status counts passed in.
if ( ! isset( $this->status_counts['all'] ) ) {
$this->status_counts = array( 'all' => array_sum( $this->status_counts ) ) + $this->status_counts;
$all_count = array_sum( $this->status_counts );
if ( isset( $this->status_counts['past-due'] ) ) {
$all_count -= $this->status_counts['past-due'];
}
$this->status_counts = array( 'all' => $all_count ) + $this->status_counts;
}
foreach ( $this->status_counts as $status_name => $count ) {
// Translated status labels.
$status_labels = ActionScheduler_Store::instance()->get_status_labels();
$status_labels['all'] = _x( 'All', 'status labels', 'action-scheduler' );
$status_labels['past-due'] = _x( 'Past-due', 'status labels', 'action-scheduler' );
foreach ( $this->status_counts as $status_slug => $count ) {
if ( 0 === $count ) {
continue;
}
if ( $status_name === $request_status || ( empty( $request_status ) && 'all' === $status_name ) ) {
if ( $status_slug === $request_status || ( empty( $request_status ) && 'all' === $status_slug ) ) {
$status_list_item = '<li class="%1$s"><a href="%2$s" class="current">%3$s</a> (%4$d)</li>';
} else {
$status_list_item = '<li class="%1$s"><a href="%2$s">%3$s</a> (%4$d)</li>';
}
$status_filter_url = ( 'all' === $status_name ) ? remove_query_arg( 'status' ) : add_query_arg( 'status', $status_name );
$status_name = isset( $status_labels[ $status_slug ] ) ? $status_labels[ $status_slug ] : ucfirst( $status_slug );
$status_filter_url = ( 'all' === $status_slug ) ? remove_query_arg( 'status' ) : add_query_arg( 'status', $status_slug );
$status_filter_url = remove_query_arg( array( 'paged', 's' ), $status_filter_url );
$status_list_items[] = sprintf( $status_list_item, esc_attr( $status_name ), esc_url( $status_filter_url ), esc_html( ucfirst( $status_name ) ), absint( $count ) );
$status_list_items[] = sprintf( $status_list_item, esc_attr( $status_slug ), esc_url( $status_filter_url ), esc_html( $status_name ), absint( $count ) );
}
if ( $status_list_items ) {

View File

@@ -48,30 +48,56 @@ abstract class ActionScheduler_Abstract_QueueRunner extends ActionScheduler_Abst
* Generally, this should be capitalised and not localised as it's a proper noun.
*/
public function process_action( $action_id, $context = '' ) {
// Temporarily override the error handler while we process the current action.
set_error_handler(
/**
* Temporary error handler which can catch errors and convert them into exceptions. This faciliates more
* robust error handling across all supported PHP versions.
*
* @throws Exception
*
* @param int $type Error level expressed as an integer.
* @param string $message Error message.
*/
function ( $type, $message ) {
throw new Exception( $message );
},
E_USER_ERROR | E_RECOVERABLE_ERROR
);
/*
* The nested try/catch structure is required because we potentially need to convert thrown errors into
* exceptions (and an exception thrown from a catch block cannot be caught by a later catch block in the *same*
* structure).
*/
try {
$valid_action = false;
do_action( 'action_scheduler_before_execute', $action_id, $context );
try {
$valid_action = false;
do_action( 'action_scheduler_before_execute', $action_id, $context );
if ( ActionScheduler_Store::STATUS_PENDING !== $this->store->get_status( $action_id ) ) {
do_action( 'action_scheduler_execution_ignored', $action_id, $context );
return;
if ( ActionScheduler_Store::STATUS_PENDING !== $this->store->get_status( $action_id ) ) {
do_action( 'action_scheduler_execution_ignored', $action_id, $context );
return;
}
$valid_action = true;
do_action( 'action_scheduler_begin_execute', $action_id, $context );
$action = $this->store->fetch_action( $action_id );
$this->store->log_execution( $action_id );
$action->execute();
do_action( 'action_scheduler_after_execute', $action_id, $action, $context );
$this->store->mark_complete( $action_id );
} catch ( Throwable $e ) {
// Throwable is defined when executing under PHP 7.0 and up. We convert it to an exception, for
// compatibility with ActionScheduler_Logger.
throw new Exception( $e->getMessage(), $e->getCode(), $e );
}
$valid_action = true;
do_action( 'action_scheduler_begin_execute', $action_id, $context );
$action = $this->store->fetch_action( $action_id );
$this->store->log_execution( $action_id );
$action->execute();
do_action( 'action_scheduler_after_execute', $action_id, $action, $context );
$this->store->mark_complete( $action_id );
} catch ( Exception $e ) {
if ( $valid_action ) {
$this->store->mark_failure( $action_id );
do_action( 'action_scheduler_failed_execution', $action_id, $e, $context );
} else {
do_action( 'action_scheduler_failed_validation', $action_id, $e, $context );
}
// This catch block exists for compatibility with PHP 5.6.
$this->handle_action_error( $action_id, $e, $context, $valid_action );
} finally {
restore_error_handler();
}
if ( isset( $action ) && is_a( $action, 'ActionScheduler_Action' ) && $action->get_schedule()->is_recurring() ) {
@@ -79,6 +105,39 @@ abstract class ActionScheduler_Abstract_QueueRunner extends ActionScheduler_Abst
}
}
/**
* Marks actions as either having failed execution or failed validation, as appropriate.
*
* @param int $action_id Action ID.
* @param Exception $e Exception instance.
* @param string $context Execution context.
* @param bool $valid_action If the action is valid.
*
* @return void
*/
private function handle_action_error( $action_id, $e, $context, $valid_action ) {
if ( $valid_action ) {
$this->store->mark_failure( $action_id );
/**
* Runs when action execution fails.
*
* @param int $action_id Action ID.
* @param Exception $e Exception instance.
* @param string $context Execution context.
*/
do_action( 'action_scheduler_failed_execution', $action_id, $e, $context );
} else {
/**
* Runs when action validation fails.
*
* @param int $action_id Action ID.
* @param Exception $e Exception instance.
* @param string $context Execution context.
*/
do_action( 'action_scheduler_failed_validation', $action_id, $e, $context );
}
}
/**
* Schedule the next instance of the action if necessary.
*
@@ -143,12 +202,22 @@ abstract class ActionScheduler_Abstract_QueueRunner extends ActionScheduler_Abst
return false;
}
// Now let's fetch the first action (having the same hook) of *any status*ithin the same window.
// Now let's fetch the first action (having the same hook) of *any status* within the same window.
unset( $query_args['status'] );
$first_action_id_with_the_same_hook = $this->store->query_actions( $query_args );
// If the IDs match, then actions for this hook must be consistently failing.
return $first_action_id_with_the_same_hook === $first_failing_action_id;
/**
* If a recurring action is assessed as consistently failing, it will not be rescheduled. This hook provides a
* way to observe and optionally override that assessment.
*
* @param bool $is_consistently_failing If the action is considered to be consistently failing.
* @param ActionScheduler_Action $action The action being assessed.
*/
return (bool) apply_filters(
'action_scheduler_recurring_action_is_consistently_failing',
$first_action_id_with_the_same_hook === $first_failing_action_id,
$action
);
}
/**

View File

@@ -25,7 +25,7 @@ abstract class ActionScheduler_Abstract_Schema {
/**
* @var array Names of tables that will be registered by this class.
*/
protected $tables = [];
protected $tables = array();
/**
* Can optionally be used by concrete classes to carry out additional initialization work
@@ -90,10 +90,10 @@ abstract class ActionScheduler_Abstract_Schema {
$plugin_option_name = 'schema-';
switch ( static::class ) {
case 'ActionScheduler_StoreSchema' :
case 'ActionScheduler_StoreSchema':
$plugin_option_name .= 'Action_Scheduler\Custom_Tables\DB_Store_Table_Maker';
break;
case 'ActionScheduler_LoggerSchema' :
case 'ActionScheduler_LoggerSchema':
$plugin_option_name .= 'Action_Scheduler\Custom_Tables\DB_Logger_Table_Maker';
break;
}
@@ -129,7 +129,7 @@ abstract class ActionScheduler_Abstract_Schema {
* @return void
*/
private function update_table( $table ) {
require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
$definition = $this->get_table_definition( $table );
if ( $definition ) {
$updated = dbDelta( $definition );
@@ -148,7 +148,7 @@ abstract class ActionScheduler_Abstract_Schema {
* table prefix for the current blog
*/
protected function get_full_table_name( $table ) {
return $GLOBALS[ 'wpdb' ]->prefix . $table;
return $GLOBALS['wpdb']->prefix . $table;
}
/**
@@ -159,14 +159,19 @@ abstract class ActionScheduler_Abstract_Schema {
public function tables_exist() {
global $wpdb;
$existing_tables = $wpdb->get_col( 'SHOW TABLES' );
$expected_tables = array_map(
function ( $table_name ) use ( $wpdb ) {
return $wpdb->prefix . $table_name;
},
$this->tables
);
$tables_exist = true;
return count( array_intersect( $existing_tables, $expected_tables ) ) === count( $expected_tables );
foreach ( $this->tables as $table_name ) {
$table_name = $wpdb->prefix . $table_name;
$pattern = str_replace( '_', '\\_', $table_name );
$existing_table = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $pattern ) );
if ( $existing_table !== $table_name ) {
$tables_exist = false;
break;
}
}
return $tables_exist;
}
}

View File

@@ -26,6 +26,8 @@ abstract class ActionScheduler_Lock {
/**
* Set a lock.
*
* To prevent race conditions, implementations should avoid setting the lock if the lock is already held.
*
* @param string $lock_type A string to identify different lock types.
* @return bool
*/

View File

@@ -347,7 +347,7 @@ abstract class ActionScheduler_Store extends ActionScheduler_Store_Deprecated {
'hook' => $hook,
'status' => self::STATUS_PENDING,
'per_page' => 1000,
'orderby' => 'action_id',
'orderby' => 'none',
)
);
@@ -372,7 +372,7 @@ abstract class ActionScheduler_Store extends ActionScheduler_Store_Deprecated {
'group' => $group,
'status' => self::STATUS_PENDING,
'per_page' => 1000,
'orderby' => 'action_id',
'orderby' => 'none',
)
);

View File

@@ -10,6 +10,19 @@ class ActionScheduler_Action {
protected $schedule = NULL;
protected $group = '';
/**
* Priorities are conceptually similar to those used for regular WordPress actions.
* Like those, a lower priority takes precedence over a higher priority and the default
* is 10.
*
* Unlike regular WordPress actions, the priority of a scheduled action is strictly an
* integer and should be kept within the bounds 0-255 (anything outside the bounds will
* be brought back into the acceptable range).
*
* @var int
*/
protected $priority = 10;
public function __construct( $hook, array $args = array(), ActionScheduler_Schedule $schedule = NULL, $group = '' ) {
$schedule = empty( $schedule ) ? new ActionScheduler_NullSchedule() : $schedule;
$this->set_hook($hook);
@@ -93,4 +106,30 @@ class ActionScheduler_Action {
public function is_finished() {
return FALSE;
}
/**
* Sets the priority of the action.
*
* @param int $priority Priority level (lower is higher priority). Should be in the range 0-255.
*
* @return void
*/
public function set_priority( $priority ) {
if ( $priority < 0 ) {
$priority = 0;
} elseif ( $priority > 255 ) {
$priority = 255;
}
$this->priority = (int) $priority;
}
/**
* Gets the action priority.
*
* @return int
*/
public function get_priority() {
return $this->priority;
}
}

View File

@@ -82,7 +82,7 @@ class ActionScheduler_DBLogger extends ActionScheduler_Logger {
}
/**
* Retrieve the an action's log entries from the database.
* Retrieve an action's log entries from the database.
*
* @param int $action_id Action ID.
*

View File

@@ -25,6 +25,13 @@ class ActionScheduler_DBStore extends ActionScheduler_Store {
/** @var int */
protected static $max_index_length = 191;
/** @var array List of claim filters. */
protected $claim_filters = [
'group' => '',
'hooks' => '',
'exclude-groups' => '',
];
/**
* Initialize the data store
*
@@ -84,7 +91,8 @@ class ActionScheduler_DBStore extends ActionScheduler_Store {
'scheduled_date_gmt' => $this->get_scheduled_date_string( $action, $date ),
'scheduled_date_local' => $this->get_scheduled_date_string_local( $action, $date ),
'schedule' => serialize( $action->get_schedule() ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
'group_id' => $this->get_group_id( $action->get_group() ),
'group_id' => current( $this->get_group_ids( $action->get_group() ) ),
'priority' => $action->get_priority(),
);
$args = wp_json_encode( $action->get_args() );
@@ -172,6 +180,7 @@ WHERE ( $where_clause ) IS NULL",
ActionScheduler_Store::STATUS_RUNNING,
);
$pending_status_placeholders = implode( ', ', array_fill( 0, count( $pending_statuses ), '%s' ) );
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $pending_status_placeholders is hardcoded.
$where_clause = $wpdb->prepare(
"
@@ -242,23 +251,35 @@ AND `group_id` = %d
/**
* Get a group's ID based on its name/slug.
*
* @param string $slug The string name of a group.
* @param bool $create_if_not_exists Whether to create the group if it does not already exist. Default, true - create the group.
* @param string|array $slugs The string name of a group, or names for several groups.
* @param bool $create_if_not_exists Whether to create the group if it does not already exist. Default, true - create the group.
*
* @return int The group's ID, if it exists or is created, or 0 if it does not exist and is not created.
* @return array The group IDs, if they exist or were successfully created. May be empty.
*/
protected function get_group_id( $slug, $create_if_not_exists = true ) {
if ( empty( $slug ) ) {
return 0;
}
/** @var \wpdb $wpdb */
global $wpdb;
$group_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT group_id FROM {$wpdb->actionscheduler_groups} WHERE slug=%s", $slug ) );
if ( empty( $group_id ) && $create_if_not_exists ) {
$group_id = $this->create_group( $slug );
protected function get_group_ids( $slugs, $create_if_not_exists = true ) {
$slugs = (array) $slugs;
$group_ids = array();
if ( empty( $slugs ) ) {
return array();
}
return $group_id;
/** @var \wpdb $wpdb */
global $wpdb;
foreach ( $slugs as $slug ) {
$group_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT group_id FROM {$wpdb->actionscheduler_groups} WHERE slug=%s", $slug ) );
if ( empty( $group_id ) && $create_if_not_exists ) {
$group_id = $this->create_group( $slug );
}
if ( $group_id ) {
$group_ids[] = $group_id;
}
}
return $group_ids;
}
/**
@@ -355,7 +376,7 @@ AND `group_id` = %d
}
$group = $data->group ? $data->group : '';
return ActionScheduler::factory()->get_stored_action( $data->status, $data->hook, $args, $schedule, $group );
return ActionScheduler::factory()->get_stored_action( $data->status, $data->hook, $args, $schedule, $group, $data->priority );
}
/**
@@ -684,7 +705,7 @@ AND `group_id` = %d
array(
'per_page' => 1000,
'status' => self::STATUS_PENDING,
'orderby' => 'action_id',
'orderby' => 'none',
)
);
@@ -796,6 +817,33 @@ AND `group_id` = %d
return $wpdb->insert_id;
}
/**
* Set a claim filter.
*
* @param string $filter_name Claim filter name.
* @param mixed $filter_values Values to filter.
* @return void
*/
public function set_claim_filter( $filter_name, $filter_values ) {
if ( isset( $this->claim_filters[ $filter_name ] ) ) {
$this->claim_filters[ $filter_name ] = $filter_values;
}
}
/**
* Get the claim filter value.
*
* @param string $filter_name Claim filter name.
* @return mixed
*/
public function get_claim_filter( $filter_name ) {
if ( isset( $this->claim_filters[ $filter_name ] ) ) {
return $this->claim_filters[ $filter_name ];
}
return '';
}
/**
* Mark actions claimed.
*
@@ -813,9 +861,8 @@ AND `group_id` = %d
/** @var \wpdb $wpdb */
global $wpdb;
$now = as_get_datetime_object();
$date = is_null( $before_date ) ? $now : clone $before_date;
$now = as_get_datetime_object();
$date = is_null( $before_date ) ? $now : clone $before_date;
// can't use $wpdb->update() because of the <= condition.
$update = "UPDATE {$wpdb->actionscheduler_actions} SET claim_id=%d, last_attempt_gmt=%s, last_attempt_local=%s";
$params = array(
@@ -824,6 +871,18 @@ AND `group_id` = %d
current_time( 'mysql' ),
);
// Set claim filters.
if ( ! empty( $hooks ) ) {
$this->set_claim_filter( 'hooks', $hooks );
} else {
$hooks = $this->get_claim_filter( 'hooks' );
}
if ( ! empty( $group ) ) {
$this->set_claim_filter( 'group', $group );
} else {
$group = $this->get_claim_filter( 'group' );
}
$where = 'WHERE claim_id = 0 AND scheduled_date_gmt <= %s AND status=%s';
$params[] = $date->format( 'Y-m-d H:i:s' );
$params[] = self::STATUS_PENDING;
@@ -834,18 +893,33 @@ AND `group_id` = %d
$params = array_merge( $params, array_values( $hooks ) );
}
$group_operator = 'IN';
if ( empty( $group ) ) {
$group = $this->get_claim_filter( 'exclude-groups' );
$group_operator = 'NOT IN';
}
if ( ! empty( $group ) ) {
$group_ids = $this->get_group_ids( $group, false );
$group_id = $this->get_group_id( $group, false );
// throw exception if no matching group found, this matches ActionScheduler_wpPostStore's behaviour.
if ( empty( $group_id ) ) {
/* translators: %s: group name */
throw new InvalidArgumentException( sprintf( __( 'The group "%s" does not exist.', 'action-scheduler' ), $group ) );
// throw exception if no matching group(s) found, this matches ActionScheduler_wpPostStore's behaviour.
if ( empty( $group_ids ) ) {
throw new InvalidArgumentException(
sprintf(
/* translators: %s: group name(s) */
_n(
'The group "%s" does not exist.',
'The groups "%s" do not exist.',
is_array( $group ) ? count( $group ) : 1,
'action-scheduler'
),
$group
)
);
}
$where .= ' AND group_id = %d';
$params[] = $group_id;
$id_list = implode( ',', array_map( 'intval', $group_ids ) );
$where .= " AND group_id {$group_operator} ( $id_list )";
}
/**
@@ -855,13 +929,23 @@ AND `group_id` = %d
*
* @param string $order_by_sql
*/
$order = apply_filters( 'action_scheduler_claim_actions_order_by', 'ORDER BY attempts ASC, scheduled_date_gmt ASC, action_id ASC' );
$order = apply_filters( 'action_scheduler_claim_actions_order_by', 'ORDER BY priority ASC, attempts ASC, scheduled_date_gmt ASC, action_id ASC' );
$params[] = $limit;
$sql = $wpdb->prepare( "{$update} {$where} {$order} LIMIT %d", $params ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders
$rows_affected = $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
if ( false === $rows_affected ) {
throw new \RuntimeException( __( 'Unable to claim actions. Database error.', 'action-scheduler' ) );
$error = empty( $wpdb->last_error )
? _x( 'unknown', 'database error', 'action-scheduler' )
: $wpdb->last_error;
throw new \RuntimeException(
sprintf(
/* translators: %s database error. */
__( 'Unable to claim actions. Database error: %s.', 'action-scheduler' ),
$error
)
);
}
return (int) $rows_affected;
@@ -912,7 +996,7 @@ AND `group_id` = %d
$cut_off = $before_date->format( 'Y-m-d H:i:s' );
$sql = $wpdb->prepare(
"SELECT action_id, scheduled_date_gmt FROM {$wpdb->actionscheduler_actions} WHERE claim_id = %d",
"SELECT action_id, scheduled_date_gmt FROM {$wpdb->actionscheduler_actions} WHERE claim_id = %d ORDER BY priority ASC, attempts ASC, scheduled_date_gmt ASC, action_id ASC",
$claim_id
);
@@ -955,7 +1039,7 @@ AND `group_id` = %d
if ( $row_updates < count( $action_ids ) ) {
throw new RuntimeException(
sprintf(
__( 'Unable to release actions from claim id %d.', 'woocommerce' ),
__( 'Unable to release actions from claim id %d.', 'action-scheduler' ),
$claim->get_id()
)
);
@@ -1005,6 +1089,8 @@ AND `group_id` = %d
/**
* Add execution message to action log.
*
* @throws Exception If the action status cannot be updated to self::STATUS_RUNNING ('in-progress').
*
* @param int $action_id Action ID.
*
* @return void
@@ -1015,7 +1101,20 @@ AND `group_id` = %d
$sql = "UPDATE {$wpdb->actionscheduler_actions} SET attempts = attempts+1, status=%s, last_attempt_gmt = %s, last_attempt_local = %s WHERE action_id = %d";
$sql = $wpdb->prepare( $sql, self::STATUS_RUNNING, current_time( 'mysql', true ), current_time( 'mysql' ), $action_id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$status_updated = $wpdb->query( $sql );
if ( ! $status_updated ) {
throw new Exception(
sprintf(
/* translators: 1: action ID. 2: status slug. */
__( 'Unable to update the status of action %1$d to %2$s.', 'action-scheduler' ),
$action_id,
self::STATUS_RUNNING
)
);
}
}
/**

View File

@@ -936,6 +936,8 @@ class ActionScheduler_wpPostStore extends ActionScheduler_Store {
/**
* Log Execution.
*
* @throws Exception If the action status cannot be updated to self::STATUS_RUNNING ('in-progress').
*
* @param string $action_id Action ID.
*/
public function log_execution( $action_id ) {
@@ -947,7 +949,7 @@ class ActionScheduler_wpPostStore extends ActionScheduler_Store {
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->query(
$status_updated = $wpdb->query(
$wpdb->prepare(
"UPDATE {$wpdb->posts} SET menu_order = menu_order+1, post_status=%s, post_modified_gmt = %s, post_modified = %s WHERE ID = %d AND post_type = %s",
self::STATUS_RUNNING,
@@ -957,6 +959,17 @@ class ActionScheduler_wpPostStore extends ActionScheduler_Store {
self::POST_TYPE
)
);
if ( ! $status_updated ) {
throw new Exception(
sprintf(
/* translators: 1: action ID. 2: status slug. */
__( 'Unable to update the status of action %1$d to %2$s.', 'action-scheduler' ),
$action_id,
self::STATUS_RUNNING
)
);
}
}
/**

View File

@@ -79,7 +79,7 @@ class Runner {
if ( $this->progress_bar ) {
/* translators: %d: amount of actions */
$this->progress_bar->set_message( sprintf( _n( 'Migrating %d action', 'Migrating %d actions', $batch_size, 'action-scheduler' ), number_format_i18n( $batch_size ) ) );
$this->progress_bar->set_message( sprintf( _n( 'Migrating %d action', 'Migrating %d actions', $batch_size, 'action-scheduler' ), $batch_size ) );
$this->progress_bar->set_count( $batch_size );
}

View File

@@ -16,7 +16,7 @@ class ActionScheduler_StoreSchema extends ActionScheduler_Abstract_Schema {
/**
* @var int Increment this value to trigger a schema update.
*/
protected $schema_version = 6;
protected $schema_version = 7;
public function __construct() {
$this->tables = [
@@ -38,6 +38,7 @@ class ActionScheduler_StoreSchema extends ActionScheduler_Abstract_Schema {
$table_name = $wpdb->$table;
$charset_collate = $wpdb->get_charset_collate();
$max_index_length = 191; // @see wp_get_db_schema()
$hook_status_scheduled_date_gmt_max_index_length = $max_index_length - 20 - 8; // - status, - scheduled_date_gmt
$default_date = self::DEFAULT_DATE;
switch ( $table ) {
@@ -49,6 +50,7 @@ class ActionScheduler_StoreSchema extends ActionScheduler_Abstract_Schema {
status varchar(20) NOT NULL,
scheduled_date_gmt datetime NULL default '{$default_date}',
scheduled_date_local datetime NULL default '{$default_date}',
priority tinyint unsigned NOT NULL default '10',
args varchar($max_index_length),
schedule longtext,
group_id bigint(20) unsigned NOT NULL default '0',
@@ -58,8 +60,8 @@ class ActionScheduler_StoreSchema extends ActionScheduler_Abstract_Schema {
claim_id bigint(20) unsigned NOT NULL default '0',
extended_args varchar(8000) DEFAULT NULL,
PRIMARY KEY (action_id),
KEY hook (hook($max_index_length)),
KEY status (status),
KEY hook_status_scheduled_date_gmt (hook($hook_status_scheduled_date_gmt_max_index_length), status, scheduled_date_gmt),
KEY status_scheduled_date_gmt (status, scheduled_date_gmt),
KEY scheduled_date_gmt (scheduled_date_gmt),
KEY args (args($max_index_length)),
KEY group_id (group_id),