rebase on oct-10-2023
This commit is contained in:
@@ -13,10 +13,14 @@ 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.
|
||||
* @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 +40,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 +235,86 @@ 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.
|
||||
* @throws Exception May be thrown if invalid options are passed.
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
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:
|
||||
throw new Exception( "Unknown action type '{$options['type']}' specified when trying to create an action for '{$options['hook']}'." );
|
||||
}
|
||||
|
||||
$action = new ActionScheduler_Action( $options['hook'], $options['arguments'], $schedule, $options['group'] );
|
||||
$action->set_priority( $options['priority'] );
|
||||
return $options['unique'] ? $this->store_unique_action( $action ) : $this->store( $action );
|
||||
}
|
||||
|
||||
/**
|
||||
* Save action to database.
|
||||
*
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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', 'woocommerce' );
|
||||
}
|
||||
|
||||
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', 'woocommerce' ),
|
||||
$id,
|
||||
$e->getMessage()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ) );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.', 'woocommerce' ),
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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, 'woocommerce' ),
|
||||
$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', 'woocommerce' ),
|
||||
$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, 'woocommerce' ),
|
||||
$actions_deleted
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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, 'woocommerce' ), number_format_i18n( $count ) ),
|
||||
sprintf( _n( 'Running %d action', 'Running %d actions', $count, 'woocommerce' ), $count ),
|
||||
$count
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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, 'woocommerce' ),
|
||||
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, 'woocommerce' ),
|
||||
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, 'woocommerce' ),
|
||||
number_format_i18n( $actions_completed )
|
||||
$actions_completed
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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', 'woocommerce' ), 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', 'woocommerce' ),
|
||||
esc_attr( $function_name )
|
||||
);
|
||||
error_log( $message );
|
||||
}
|
||||
|
||||
return self::$data_store_initialized;
|
||||
|
||||
@@ -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', 'woocommerce' );
|
||||
$status_labels['past-due'] = _x( 'Past-due', 'status labels', 'woocommerce' );
|
||||
|
||||
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 ) {
|
||||
|
||||
@@ -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->getPrevious() );
|
||||
}
|
||||
|
||||
$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
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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.', 'woocommerce' ), $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,
|
||||
'woocommerce'
|
||||
),
|
||||
$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.', 'woocommerce' ) );
|
||||
$error = empty( $wpdb->last_error )
|
||||
? _x( 'unknown', 'database error', 'woocommerce' )
|
||||
: $wpdb->last_error;
|
||||
|
||||
throw new \RuntimeException(
|
||||
sprintf(
|
||||
/* translators: %s database error. */
|
||||
__( 'Unable to claim actions. Database error: %s.', 'woocommerce' ),
|
||||
$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
|
||||
);
|
||||
|
||||
@@ -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.', 'woocommerce' ),
|
||||
$action_id,
|
||||
self::STATUS_RUNNING
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.', 'woocommerce' ),
|
||||
$action_id,
|
||||
self::STATUS_RUNNING
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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, 'woocommerce' ), number_format_i18n( $batch_size ) ) );
|
||||
$this->progress_bar->set_message( sprintf( _n( 'Migrating %d action', 'Migrating %d actions', $batch_size, 'woocommerce' ), $batch_size ) );
|
||||
$this->progress_bar->set_count( $batch_size );
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = [
|
||||
@@ -49,6 +49,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',
|
||||
|
||||
Reference in New Issue
Block a user