374 lines
9.7 KiB
PHP
374 lines
9.7 KiB
PHP
|
<?php
|
||
|
/**
|
||
|
* Traits for scheduling actions and dependencies.
|
||
|
*/
|
||
|
|
||
|
namespace Automattic\WooCommerce\Admin\Schedulers;
|
||
|
|
||
|
defined( 'ABSPATH' ) || exit;
|
||
|
|
||
|
/**
|
||
|
* SchedulerTraits class.
|
||
|
*/
|
||
|
trait SchedulerTraits {
|
||
|
/**
|
||
|
* Action scheduler group.
|
||
|
*
|
||
|
* @var string|null
|
||
|
*/
|
||
|
public static $group = 'wc-admin-data';
|
||
|
|
||
|
/**
|
||
|
* Queue instance.
|
||
|
*
|
||
|
* @var WC_Queue_Interface
|
||
|
*/
|
||
|
protected static $queue = null;
|
||
|
|
||
|
/**
|
||
|
* Add all actions as hooks.
|
||
|
*/
|
||
|
public static function init() {
|
||
|
foreach ( self::get_actions() as $action_name => $action_hook ) {
|
||
|
$method = new \ReflectionMethod( static::class, $action_name );
|
||
|
add_action( $action_hook, array( static::class, 'do_action_or_reschedule' ), 10, $method->getNumberOfParameters() );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get queue instance.
|
||
|
*
|
||
|
* @return WC_Queue_Interface
|
||
|
*/
|
||
|
public static function queue() {
|
||
|
if ( is_null( self::$queue ) ) {
|
||
|
self::$queue = WC()->queue();
|
||
|
}
|
||
|
|
||
|
return self::$queue;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Set queue instance.
|
||
|
*
|
||
|
* @param WC_Queue_Interface $queue Queue instance.
|
||
|
*/
|
||
|
public static function set_queue( $queue ) {
|
||
|
self::$queue = $queue;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets the default scheduler actions for batching and scheduling actions.
|
||
|
*/
|
||
|
public static function get_default_scheduler_actions() {
|
||
|
return array(
|
||
|
'schedule_action' => 'wc-admin_schedule_action_' . static::$name,
|
||
|
'queue_batches' => 'wc-admin_queue_batches_' . static::$name,
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets the actions for this specific scheduler.
|
||
|
*
|
||
|
* @return array
|
||
|
*/
|
||
|
public static function get_scheduler_actions() {
|
||
|
return array();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get all available scheduling actions.
|
||
|
* Used to determine action hook names and clear events.
|
||
|
*/
|
||
|
public static function get_actions() {
|
||
|
return array_merge(
|
||
|
static::get_default_scheduler_actions(),
|
||
|
static::get_scheduler_actions()
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get an action tag name from the action name.
|
||
|
*
|
||
|
* @param string $action_name The action name.
|
||
|
* @return string|null
|
||
|
*/
|
||
|
public static function get_action( $action_name ) {
|
||
|
$actions = static::get_actions();
|
||
|
return isset( $actions[ $action_name ] ) ? $actions[ $action_name ] : null;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns an array of actions and dependencies as key => value pairs.
|
||
|
*
|
||
|
* @return array
|
||
|
*/
|
||
|
public static function get_dependencies() {
|
||
|
return array();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get dependencies associated with an action.
|
||
|
*
|
||
|
* @param string $action_name The action slug.
|
||
|
* @return string|null
|
||
|
*/
|
||
|
public static function get_dependency( $action_name ) {
|
||
|
$dependencies = static::get_dependencies();
|
||
|
return isset( $dependencies[ $action_name ] ) ? $dependencies[ $action_name ] : null;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Batch action size.
|
||
|
*/
|
||
|
public static function get_batch_sizes() {
|
||
|
return array(
|
||
|
'queue_batches' => 100,
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the batch size for an action.
|
||
|
*
|
||
|
* @param string $action Single batch action name.
|
||
|
* @return int Batch size.
|
||
|
*/
|
||
|
public static function get_batch_size( $action ) {
|
||
|
$batch_sizes = static::get_batch_sizes();
|
||
|
$batch_size = isset( $batch_sizes[ $action ] ) ? $batch_sizes[ $action ] : 25;
|
||
|
|
||
|
/**
|
||
|
* Filter the batch size for regenerating a report table.
|
||
|
*
|
||
|
* @param int $batch_size Batch size.
|
||
|
* @param string $action Batch action name.
|
||
|
*/
|
||
|
return apply_filters( 'woocommerce_analytics_regenerate_batch_size', $batch_size, static::$name, $action );
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Flatten multidimensional arrays to store for scheduling.
|
||
|
*
|
||
|
* @param array $args Argument array.
|
||
|
* @return string
|
||
|
*/
|
||
|
public static function flatten_args( $args ) {
|
||
|
$flattened = array();
|
||
|
|
||
|
foreach ( $args as $arg ) {
|
||
|
if ( is_array( $arg ) ) {
|
||
|
$flattened[] = self::flatten_args( $arg );
|
||
|
} else {
|
||
|
$flattened[] = $arg;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$string = '[' . implode( ',', $flattened ) . ']';
|
||
|
return $string;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Check if existing jobs exist for an action and arguments.
|
||
|
*
|
||
|
* @param string $action_name Action name.
|
||
|
* @param array $args Array of arguments to pass to action.
|
||
|
* @return bool
|
||
|
*/
|
||
|
public static function has_existing_jobs( $action_name, $args ) {
|
||
|
$existing_jobs = self::queue()->search(
|
||
|
array(
|
||
|
'status' => 'pending',
|
||
|
'per_page' => 1,
|
||
|
'claimed' => false,
|
||
|
'hook' => static::get_action( $action_name ),
|
||
|
'search' => self::flatten_args( $args ),
|
||
|
'group' => self::$group,
|
||
|
)
|
||
|
);
|
||
|
|
||
|
if ( $existing_jobs ) {
|
||
|
$existing_job = current( $existing_jobs );
|
||
|
|
||
|
// Bail out if there's a pending single action, or a pending scheduled actions.
|
||
|
if (
|
||
|
( static::get_action( $action_name ) === $existing_job->get_hook() ) ||
|
||
|
(
|
||
|
static::get_action( 'schedule_action' ) === $existing_job->get_hook() &&
|
||
|
in_array( self::get_action( $action_name ), $existing_job->get_args(), true )
|
||
|
)
|
||
|
) {
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the next blocking job for an action.
|
||
|
*
|
||
|
* @param string $action_name Action name.
|
||
|
* @return false|ActionScheduler_Action
|
||
|
*/
|
||
|
public static function get_next_blocking_job( $action_name ) {
|
||
|
$dependency = self::get_dependency( $action_name );
|
||
|
|
||
|
if ( ! $dependency ) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
$blocking_jobs = self::queue()->search(
|
||
|
array(
|
||
|
'status' => 'pending',
|
||
|
'orderby' => 'date',
|
||
|
'order' => 'DESC',
|
||
|
'per_page' => 1,
|
||
|
'search' => $dependency, // search is used instead of hook to find queued batch creation.
|
||
|
'group' => static::$group,
|
||
|
)
|
||
|
);
|
||
|
|
||
|
$next_job_schedule = null;
|
||
|
|
||
|
if ( is_array( $blocking_jobs ) ) {
|
||
|
foreach ( $blocking_jobs as $blocking_job ) {
|
||
|
$next_job_schedule = self::get_next_action_time( $blocking_job );
|
||
|
|
||
|
// Ensure that the next schedule is a DateTime (it can be null).
|
||
|
if ( is_a( $next_job_schedule, 'DateTime' ) ) {
|
||
|
return $blocking_job;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Check for blocking jobs and reschedule if any exist.
|
||
|
*/
|
||
|
public static function do_action_or_reschedule() {
|
||
|
$action_hook = current_action();
|
||
|
$action_name = array_search( $action_hook, static::get_actions(), true );
|
||
|
$args = func_get_args();
|
||
|
|
||
|
// Check if any blocking jobs exist and schedule after they've completed
|
||
|
// or schedule to run now if no blocking jobs exist.
|
||
|
$blocking_job = static::get_next_blocking_job( $action_name );
|
||
|
if ( $blocking_job ) {
|
||
|
$after = new \DateTime();
|
||
|
self::queue()->schedule_single(
|
||
|
self::get_next_action_time( $blocking_job )->getTimestamp() + 5,
|
||
|
$action_hook,
|
||
|
$args,
|
||
|
static::$group
|
||
|
);
|
||
|
} else {
|
||
|
call_user_func_array( array( static::class, $action_name ), $args );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the DateTime for the next scheduled time an action should run.
|
||
|
* This function allows backwards compatibility with Action Scheduler < v3.0.
|
||
|
*
|
||
|
* @param \ActionScheduler_Action $action Action.
|
||
|
* @return DateTime|null
|
||
|
*/
|
||
|
public static function get_next_action_time( $action ) {
|
||
|
if ( method_exists( $action->get_schedule(), 'get_next' ) ) {
|
||
|
$after = new \DateTime();
|
||
|
$next_job_schedule = $action->get_schedule()->get_next( $after );
|
||
|
} else {
|
||
|
$next_job_schedule = $action->get_schedule()->next();
|
||
|
}
|
||
|
|
||
|
return $next_job_schedule;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Schedule an action to run and check for dependencies.
|
||
|
*
|
||
|
* @param string $action_name Action name.
|
||
|
* @param array $args Array of arguments to pass to action.
|
||
|
*/
|
||
|
public static function schedule_action( $action_name, $args = array() ) {
|
||
|
// Check for existing jobs and bail if they already exist.
|
||
|
if ( static::has_existing_jobs( $action_name, $args ) ) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
$action_hook = static::get_action( $action_name );
|
||
|
if ( ! $action_hook ) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (
|
||
|
// Skip scheduling if Action Scheduler tables have not been initialized.
|
||
|
! get_option( 'schema-ActionScheduler_StoreSchema' ) ||
|
||
|
apply_filters( 'woocommerce_analytics_disable_action_scheduling', false )
|
||
|
) {
|
||
|
call_user_func_array( array( static::class, $action_name ), $args );
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
self::queue()->schedule_single( time() + 5, $action_hook, $args, static::$group );
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Queue a large number of batch jobs, respecting the batch size limit.
|
||
|
* Reduces a range of batches down to "single batch" jobs.
|
||
|
*
|
||
|
* @param int $range_start Starting batch number.
|
||
|
* @param int $range_end Ending batch number.
|
||
|
* @param string $single_batch_action Action to schedule for a single batch.
|
||
|
* @param array $action_args Action arguments.
|
||
|
* @return void
|
||
|
*/
|
||
|
public static function queue_batches( $range_start, $range_end, $single_batch_action, $action_args = array() ) {
|
||
|
$batch_size = static::get_batch_size( 'queue_batches' );
|
||
|
$range_size = 1 + ( $range_end - $range_start );
|
||
|
$action_timestamp = time() + 5;
|
||
|
|
||
|
if ( $range_size > $batch_size ) {
|
||
|
// If the current batch range is larger than a single batch,
|
||
|
// split the range into $queue_batch_size chunks.
|
||
|
$chunk_size = (int) ceil( $range_size / $batch_size );
|
||
|
|
||
|
for ( $i = 0; $i < $batch_size; $i++ ) {
|
||
|
$batch_start = (int) ( $range_start + ( $i * $chunk_size ) );
|
||
|
$batch_end = (int) min( $range_end, $range_start + ( $chunk_size * ( $i + 1 ) ) - 1 );
|
||
|
|
||
|
if ( $batch_start > $range_end ) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
self::schedule_action(
|
||
|
'queue_batches',
|
||
|
array( $batch_start, $batch_end, $single_batch_action, $action_args )
|
||
|
);
|
||
|
}
|
||
|
} else {
|
||
|
// Otherwise, queue the single batches.
|
||
|
for ( $i = $range_start; $i <= $range_end; $i++ ) {
|
||
|
$batch_action_args = array_merge( array( $i ), $action_args );
|
||
|
self::schedule_action( $single_batch_action, $batch_action_args );
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Clears all queued actions.
|
||
|
*/
|
||
|
public static function clear_queued_actions() {
|
||
|
if ( version_compare( \ActionScheduler_Versions::instance()->latest_version(), '3.0', '>=' ) ) {
|
||
|
\ActionScheduler::store()->cancel_actions_by_group( static::$group );
|
||
|
} else {
|
||
|
$actions = static::get_actions();
|
||
|
foreach ( $actions as $action ) {
|
||
|
self::queue()->cancel_all( $action, null, static::$group );
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|