mirror of
https://github.com/pronamic/woocommerce-subscriptions.git
synced 2025-10-07 18:14:03 +00:00
573 lines
21 KiB
PHP
573 lines
21 KiB
PHP
<?php
|
|
/**
|
|
* This class is a helper intended to handle data processings that need to happen in batches in a deferred way.
|
|
* It abstracts away the nuances of (re)scheduling actions and dealing with errors.
|
|
*
|
|
* Usage:
|
|
*
|
|
* 1. Create a class that implements WCS_Batch_Processor.
|
|
*
|
|
* 2. Whenever there's data to be processed invoke the 'enqueue_processor' method in this class,
|
|
* passing the class name of the processor.
|
|
*
|
|
* That's it, processing will be performed in batches inside scheduled actions; enqueued processors will only
|
|
* be dequeued once they notify that no more items are left to process (or when `force_clear_all_processes` is invoked).
|
|
* Failed batches will be retried after a while.
|
|
*
|
|
* This is heavily inspired by core's version at Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessingController.
|
|
*
|
|
* @package WooCommerce Subscriptions
|
|
* @category Class
|
|
* @since 7.7.0
|
|
*/
|
|
class WCS_Batch_Processing_Controller {
|
|
/*
|
|
* Identifier of a "watchdog" action that will schedule a processing action
|
|
* for any processor that is enqueued but not yet scheduled
|
|
* (because it's been just enqueued or because it threw an error while processing a batch),
|
|
* that's one single action that reschedules itself continuously.
|
|
*/
|
|
const WATCHDOG_ACTION_NAME = 'wcs_schedule_pending_batch_processes';
|
|
|
|
/*
|
|
* Identifier of the action that will do the actual batch processing.
|
|
* There's one action per enqueued processor that will keep rescheduling itself
|
|
* as long as there are still pending items to process
|
|
* (except if there's an error that caused no items to be processed at all).
|
|
*/
|
|
const PROCESS_SINGLE_BATCH_ACTION_NAME = 'wcs_run_batch_process';
|
|
|
|
const ENQUEUED_PROCESSORS_OPTION_NAME = 'wcs_pending_batch_processes';
|
|
const ACTION_GROUP = 'wcs_batch_processes';
|
|
const LOGS_CONTEXT = 'wcs-batch-processing';
|
|
|
|
/**
|
|
* Maximum number of failures per processor before it gets dequeued.
|
|
*/
|
|
const FAILING_PROCESS_MAX_ATTEMPTS_DEFAULT = 5;
|
|
|
|
/**
|
|
* Instance of WC_Logger class.
|
|
*
|
|
* @var \WC_Logger_Interface
|
|
*/
|
|
private $logger;
|
|
|
|
/**
|
|
* Singleton instance.
|
|
*
|
|
* @var WCS_Batch_Processing_Controller
|
|
*/
|
|
private static $instance;
|
|
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* Schedules the necessary actions to process batches.
|
|
*/
|
|
private function __construct() {
|
|
add_action(
|
|
self::WATCHDOG_ACTION_NAME,
|
|
function () {
|
|
$this->handle_watchdog_action();
|
|
}
|
|
);
|
|
|
|
add_action(
|
|
self::PROCESS_SINGLE_BATCH_ACTION_NAME,
|
|
function ( $batch_process ) {
|
|
$this->process_next_batch_for_single_processor( $batch_process );
|
|
},
|
|
10,
|
|
2
|
|
);
|
|
|
|
add_action(
|
|
'shutdown',
|
|
function () {
|
|
$this->remove_or_retry_failed_processors();
|
|
}
|
|
);
|
|
|
|
$this->logger = wc_get_logger();
|
|
}
|
|
|
|
/**
|
|
* Get the singleton instance of this class.
|
|
*
|
|
* @return WCS_Batch_Processing_Controller
|
|
*/
|
|
final public static function instance(): WCS_Batch_Processing_Controller {
|
|
if ( ! isset( self::$instance ) ) {
|
|
self::$instance = new self();
|
|
}
|
|
return self::$instance;
|
|
}
|
|
|
|
/**
|
|
* Enqueue a processor so that it will get batch processing requests from within scheduled actions.
|
|
*
|
|
* @param string $processor_class_name Fully qualified class name of the processor, must implement `WCS_Batch_Processor`.
|
|
*/
|
|
public function enqueue_processor( string $processor_class_name ): void {
|
|
$pending_updates = $this->get_enqueued_processors();
|
|
if ( ! in_array( $processor_class_name, array_keys( $pending_updates ), true ) ) {
|
|
$pending_updates[] = $processor_class_name;
|
|
$this->set_enqueued_processors( $pending_updates );
|
|
}
|
|
$this->schedule_watchdog_action( false, true );
|
|
}
|
|
|
|
/**
|
|
* Schedule the watchdog action.
|
|
*
|
|
* @param bool $with_delay Whether to delay the action execution. Should be true when rescheduling, false when enqueueing.
|
|
* @param bool $unique Whether to make the action unique.
|
|
*/
|
|
private function schedule_watchdog_action( bool $with_delay = false, bool $unique = false ): void {
|
|
$time = time();
|
|
if ( $with_delay ) {
|
|
/**
|
|
* Modify the delay interval for the batch processor's watchdog events.
|
|
*
|
|
* @since 7.7.0
|
|
*
|
|
* @param int $delay Time, in seconds, before the watchdog process will run. Defaults to 3600 (1 hour).
|
|
*/
|
|
$time += apply_filters( 'wcs_batch_processor_watchdog_delay_seconds', HOUR_IN_SECONDS );
|
|
}
|
|
|
|
if ( ! as_has_scheduled_action( self::WATCHDOG_ACTION_NAME ) ) {
|
|
as_schedule_single_action(
|
|
$time,
|
|
self::WATCHDOG_ACTION_NAME,
|
|
array(),
|
|
self::ACTION_GROUP,
|
|
$unique
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Schedule a processing action for all the processors that are enqueued but not scheduled
|
|
* (because they have just been enqueued, or because the processing for a batch failed).
|
|
*/
|
|
private function handle_watchdog_action(): void {
|
|
$pending_processes = $this->get_enqueued_processors();
|
|
if ( empty( $pending_processes ) ) {
|
|
return;
|
|
}
|
|
foreach ( $pending_processes as $process_name ) {
|
|
if ( ! $this->is_scheduled( $process_name ) ) {
|
|
$this->schedule_batch_processing( $process_name );
|
|
}
|
|
}
|
|
$this->schedule_watchdog_action( true );
|
|
}
|
|
|
|
/**
|
|
* Process a batch for a single processor, and handle any required rescheduling or state cleanup.
|
|
*
|
|
* @param string $processor_class_name Fully qualified class name of the processor.
|
|
*
|
|
* @throws \Exception If error occurred during batch processing.
|
|
*/
|
|
private function process_next_batch_for_single_processor( string $processor_class_name ): void {
|
|
if ( ! $this->is_enqueued( $processor_class_name ) ) {
|
|
return;
|
|
}
|
|
|
|
$batch_processor = $this->get_processor_instance( $processor_class_name );
|
|
$error = $this->process_next_batch_for_single_processor_core( $batch_processor );
|
|
$still_pending = count( $batch_processor->get_next_batch_to_process( 1 ) ) > 0;
|
|
if ( ( $error instanceof \Exception ) ) {
|
|
// The batch processing failed and no items were processed:
|
|
// reschedule the processing with a delay, unless this is a repeatead failure.
|
|
if ( $this->is_consistently_failing( $batch_processor ) ) {
|
|
$this->log_consistent_failure( $batch_processor, $this->get_process_details( $batch_processor ) );
|
|
$this->remove_processor( $processor_class_name );
|
|
} else {
|
|
$this->schedule_batch_processing( $processor_class_name, true );
|
|
}
|
|
|
|
throw $error;
|
|
}
|
|
if ( $still_pending ) {
|
|
$this->schedule_batch_processing( $processor_class_name );
|
|
} else {
|
|
$this->dequeue_processor( $processor_class_name );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Process a batch for a single processor, updating state and logging any error.
|
|
*
|
|
* @param WCS_Batch_Processor $batch_processor Batch processor instance.
|
|
*
|
|
* @return null|\Exception Exception if error occurred, null otherwise.
|
|
*/
|
|
private function process_next_batch_for_single_processor_core( WCS_Batch_Processor $batch_processor ): ?\Exception {
|
|
$details = $this->get_process_details( $batch_processor );
|
|
$time_start = microtime( true );
|
|
$batch = $batch_processor->get_next_batch_to_process( $details['current_batch_size'] );
|
|
if ( empty( $batch ) ) {
|
|
return null;
|
|
}
|
|
try {
|
|
$batch_processor->process_batch( $batch );
|
|
$time_taken = microtime( true ) - $time_start;
|
|
$this->update_processor_state( $batch_processor, $time_taken );
|
|
} catch ( \Exception $exception ) {
|
|
$time_taken = microtime( true ) - $time_start;
|
|
$this->log_error( $exception, $batch_processor, $batch );
|
|
$this->update_processor_state( $batch_processor, $time_taken, $exception );
|
|
return $exception;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get the current state for a given enqueued processor.
|
|
*
|
|
* @param WCS_Batch_Processor $batch_processor Batch processor instance.
|
|
*
|
|
* @return array Current state for the processor, or a "blank" state if none exists yet.
|
|
*/
|
|
private function get_process_details( WCS_Batch_Processor $batch_processor ): array {
|
|
$defaults = array(
|
|
'total_time_spent' => 0,
|
|
'current_batch_size' => $batch_processor->get_default_batch_size(),
|
|
'last_error' => null,
|
|
'recent_failures' => 0,
|
|
'batch_first_failure' => null,
|
|
'batch_last_failure' => null,
|
|
);
|
|
|
|
$process_details = get_option( $this->get_processor_state_option_name( $batch_processor ) );
|
|
$process_details = wp_parse_args( is_array( $process_details ) ? $process_details : array(), $defaults );
|
|
|
|
return $process_details;
|
|
}
|
|
|
|
/**
|
|
* Get the name of the option where we will be saving state for a given processor.
|
|
*
|
|
* @param WCS_Batch_Processor|string $batch_processor Batch processor instance or class name.
|
|
*
|
|
* @return string Option name.
|
|
*/
|
|
private function get_processor_state_option_name( $batch_processor ): string {
|
|
$class_name = is_a( $batch_processor, WCS_Batch_Processor::class ) ? get_class( $batch_processor ) : $batch_processor;
|
|
$class_md5 = md5( $class_name );
|
|
// truncate the class name so we know that it will fit in the option name column along with md5 hash and prefix.
|
|
$class_name = substr( $class_name, 0, 140 );
|
|
return 'wcs_batch_' . $class_name . '_' . $class_md5;
|
|
}
|
|
|
|
/**
|
|
* Update the state for a processor after a batch has completed processing.
|
|
*
|
|
* @param WCS_Batch_Processor $batch_processor Batch processor instance.
|
|
* @param float $time_taken Time take by the batch to complete processing.
|
|
* @param \Exception|null $last_error Exception object in processing the batch, if there was one.
|
|
*/
|
|
private function update_processor_state( WCS_Batch_Processor $batch_processor, float $time_taken, \Exception $last_error = null ): void {
|
|
$current_status = $this->get_process_details( $batch_processor );
|
|
$current_status['total_time_spent'] += $time_taken;
|
|
$current_status['last_error'] = null !== $last_error ? $last_error->getMessage() : null;
|
|
|
|
if ( null !== $last_error ) {
|
|
$current_status['recent_failures'] = ( $current_status['recent_failures'] ?? 0 ) + 1;
|
|
$current_status['batch_last_failure'] = current_time( 'mysql' );
|
|
|
|
if ( is_null( $current_status['batch_first_failure'] ) ) {
|
|
$current_status['batch_first_failure'] = $current_status['batch_last_failure'];
|
|
}
|
|
} else {
|
|
$current_status['recent_failures'] = 0;
|
|
$current_status['batch_first_failure'] = null;
|
|
$current_status['batch_last_failure'] = null;
|
|
}
|
|
|
|
update_option( $this->get_processor_state_option_name( $batch_processor ), $current_status, false );
|
|
}
|
|
|
|
/**
|
|
* Removes the option where we store state for a given processor.
|
|
*
|
|
* @param string $processor_class_name Fully qualified class name of the processor.
|
|
*/
|
|
private function clear_processor_state( string $processor_class_name ): void {
|
|
delete_option( $this->get_processor_state_option_name( $processor_class_name ) );
|
|
}
|
|
|
|
/**
|
|
* Schedule a processing action for a single processor.
|
|
*
|
|
* @param string $processor_class_name Fully qualified class name of the processor.
|
|
* @param bool $with_delay Whether to schedule the action for immediate execution or for later.
|
|
*/
|
|
private function schedule_batch_processing( string $processor_class_name, bool $with_delay = false ): void {
|
|
$time = $with_delay ? time() + MINUTE_IN_SECONDS : time();
|
|
as_schedule_single_action( $time, self::PROCESS_SINGLE_BATCH_ACTION_NAME, array( $processor_class_name ) );
|
|
}
|
|
|
|
/**
|
|
* Check if a batch processing action is already scheduled for a given processor.
|
|
* Differs from `as_has_scheduled_action` in that this excludes actions in progress.
|
|
*
|
|
* @param string $processor_class_name Fully qualified class name of the batch processor.
|
|
*
|
|
* @return bool True if a batch processing action is already scheduled for the processor.
|
|
*/
|
|
public function is_scheduled( string $processor_class_name ): bool {
|
|
return as_has_scheduled_action( self::PROCESS_SINGLE_BATCH_ACTION_NAME, array( $processor_class_name ) );
|
|
}
|
|
|
|
/**
|
|
* Get an instance of a processor given its class name.
|
|
*
|
|
* @param string $processor_class_name Full class name of the batch processor.
|
|
*
|
|
* @return WCS_Batch_Processor Instance of batch processor for the given class.
|
|
* @throws \Exception If it's not possible to get an instance of the class.
|
|
*/
|
|
private function get_processor_instance( string $processor_class_name ): WCS_Batch_Processor {
|
|
if ( ! isset( $processor ) && class_exists( $processor_class_name ) ) {
|
|
// This is a fallback for when the batch processor is not registered in the container.
|
|
$processor = new $processor_class_name();
|
|
}
|
|
if ( ! is_a( $processor, WCS_Batch_Processor::class ) ) {
|
|
throw new \Exception( "Unable to initialize batch processor instance for $processor_class_name" );
|
|
}
|
|
return $processor;
|
|
}
|
|
|
|
/**
|
|
* Helper method to get list of all the enqueued processors.
|
|
*
|
|
* @return array List (of string) of the class names of the enqueued processors.
|
|
*/
|
|
public function get_enqueued_processors(): array {
|
|
$enqueued_processors = get_option( self::ENQUEUED_PROCESSORS_OPTION_NAME, array() );
|
|
if ( ! is_array( $enqueued_processors ) ) {
|
|
$this->logger->error( 'Could not fetch list of processors. Clearing up queue.', array( 'source' => self::LOGS_CONTEXT ) );
|
|
delete_option( self::ENQUEUED_PROCESSORS_OPTION_NAME );
|
|
$enqueued_processors = array();
|
|
}
|
|
|
|
return $enqueued_processors;
|
|
}
|
|
|
|
/**
|
|
* Dequeue a processor once it has no more items pending processing.
|
|
*
|
|
* @param string $processor_class_name Full processor class name.
|
|
*/
|
|
private function dequeue_processor( string $processor_class_name ): void {
|
|
$pending_processes = $this->get_enqueued_processors();
|
|
if ( in_array( $processor_class_name, $pending_processes, true ) ) {
|
|
$this->clear_processor_state( $processor_class_name );
|
|
$pending_processes = array_diff( $pending_processes, array( $processor_class_name ) );
|
|
$this->set_enqueued_processors( $pending_processes );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper method to set the enqueued processor class names.
|
|
*
|
|
* @param array $processors List of full processor class names.
|
|
*/
|
|
private function set_enqueued_processors( array $processors ): void {
|
|
update_option( self::ENQUEUED_PROCESSORS_OPTION_NAME, $processors, false );
|
|
}
|
|
|
|
/**
|
|
* Check if a particular processor is enqueued.
|
|
*
|
|
* @param string $processor_class_name Fully qualified class name of the processor.
|
|
*
|
|
* @return bool True if the processor is enqueued.
|
|
*/
|
|
public function is_enqueued( string $processor_class_name ): bool {
|
|
return in_array( $processor_class_name, $this->get_enqueued_processors(), true );
|
|
}
|
|
|
|
/**
|
|
* Dequeue and de-schedule a processor instance so that it won't be processed anymore.
|
|
*
|
|
* @param string $processor_class_name Fully qualified class name of the processor.
|
|
* @return bool True if the processor has been dequeued, false if the processor wasn't enqueued (so nothing has been done).
|
|
*/
|
|
public function remove_processor( string $processor_class_name ): bool {
|
|
$enqueued_processors = $this->get_enqueued_processors();
|
|
if ( ! in_array( $processor_class_name, $enqueued_processors, true ) ) {
|
|
return false;
|
|
}
|
|
|
|
$enqueued_processors = array_diff( $enqueued_processors, array( $processor_class_name ) );
|
|
if ( empty( $enqueued_processors ) ) {
|
|
$this->force_clear_all_processes();
|
|
} else {
|
|
update_option( self::ENQUEUED_PROCESSORS_OPTION_NAME, $enqueued_processors, false );
|
|
as_unschedule_all_actions( self::PROCESS_SINGLE_BATCH_ACTION_NAME, array( $processor_class_name ) );
|
|
$this->clear_processor_state( $processor_class_name );
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Dequeues and de-schedules all the processors.
|
|
*/
|
|
public function force_clear_all_processes(): void {
|
|
as_unschedule_all_actions( self::PROCESS_SINGLE_BATCH_ACTION_NAME );
|
|
as_unschedule_all_actions( self::WATCHDOG_ACTION_NAME );
|
|
|
|
foreach ( $this->get_enqueued_processors() as $processor ) {
|
|
$this->clear_processor_state( $processor );
|
|
}
|
|
|
|
update_option( self::ENQUEUED_PROCESSORS_OPTION_NAME, array(), false );
|
|
}
|
|
|
|
/**
|
|
* Log an error that happened while processing a batch.
|
|
*
|
|
* @param \Exception $error Exception object to log.
|
|
* @param WCS_Batch_Processor $batch_processor Batch processor instance.
|
|
* @param array $batch Batch that was being processed.
|
|
*/
|
|
protected function log_error( \Exception $error, WCS_Batch_Processor $batch_processor, array $batch ): void {
|
|
$error_message = "Error processing batch for {$batch_processor->get_name()}: {$error->getMessage()}";
|
|
$error_context = array(
|
|
'exception' => $error,
|
|
'source' => self::LOGS_CONTEXT,
|
|
);
|
|
|
|
// Log only first and last, as the entire batch may be too big.
|
|
if ( count( $batch ) > 0 ) {
|
|
$error_context = array_merge(
|
|
$error_context,
|
|
array(
|
|
'batch_start' => $batch[0],
|
|
'batch_end' => end( $batch ),
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Filters the error message for a batch processing.
|
|
*
|
|
* @param string $error_message The error message that will be logged.
|
|
* @param \Exception $error The exception that was thrown by the processor.
|
|
* @param WCS_Batch_Processor $batch_processor The processor that threw the exception.
|
|
* @param array $batch The batch that was being processed.
|
|
* @param array $error_context Context to be passed to the logging function.
|
|
* @return string The actual error message that will be logged.
|
|
*
|
|
* @since 7.7.0
|
|
*/
|
|
$error_message = apply_filters( 'wcs_batch_processing_log_message', $error_message, $error, $batch_processor, $batch, $error_context );
|
|
|
|
$this->logger->error( $error_message, $error_context );
|
|
}
|
|
|
|
/**
|
|
* Determines whether a given processor is consistently failing based on how many recent consecutive failures it has had.
|
|
*
|
|
* @param WCS_Batch_Processor $batch_processor The processor that we want to check.
|
|
* @return boolean TRUE if processor is consistently failing. FALSE otherwise.
|
|
*/
|
|
private function is_consistently_failing( WCS_Batch_Processor $batch_processor ): bool {
|
|
$process_details = $this->get_process_details( $batch_processor );
|
|
$max_attempts = absint(
|
|
/**
|
|
* Controls the failure threshold for batch processors. That is, the number of times we'll attempt to
|
|
* process a batch that has resulted in a failure. Once above this threshold, the processor won't be
|
|
* re-scheduled and will be removed from the queue.
|
|
*
|
|
* @since 7.7.0
|
|
*
|
|
* @param int $failure_threshold Maximum number of times for the processor to try processing a given batch.
|
|
* @param WCS_Batch_Processor $batch_processor The processor instance.
|
|
* @param array $process_details Array with batch processor state.
|
|
*/
|
|
apply_filters(
|
|
'wcs_batch_processing_max_attempts',
|
|
self::FAILING_PROCESS_MAX_ATTEMPTS_DEFAULT,
|
|
$batch_processor,
|
|
$process_details
|
|
)
|
|
);
|
|
|
|
return absint( $process_details['recent_failures'] ?? 0 ) >= max( $max_attempts, 1 );
|
|
}
|
|
|
|
/**
|
|
* Creates log entry with details about a batch processor that is consistently failing.
|
|
*
|
|
* @param WCS_Batch_Processor $batch_processor The batch processor instance.
|
|
* @param array $process_details Failing process details.
|
|
*/
|
|
private function log_consistent_failure( WCS_Batch_Processor $batch_processor, array $process_details ): void {
|
|
$this->logger->error(
|
|
"Batch processor {$batch_processor->get_name()} appears to be failing consistently: {$process_details['recent_failures']} unsuccessful attempt(s). No further attempts will be made.",
|
|
array(
|
|
'source' => self::LOGS_CONTEXT,
|
|
'failures' => $process_details['recent_failures'],
|
|
'first_failure' => $process_details['batch_first_failure'],
|
|
'last_failure' => $process_details['batch_last_failure'],
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Hooked onto 'shutdown'. This cleanup routine checks enqueued processors and whether they are scheduled or not to
|
|
* either re-eschedule them or remove them from the queue.
|
|
* This prevents stale states where Action Scheduler won't schedule any more attempts but we still report the
|
|
* processor as enqueued.
|
|
*
|
|
*/
|
|
private function remove_or_retry_failed_processors(): void {
|
|
if ( ! did_action( 'wp_loaded' ) ) {
|
|
return;
|
|
}
|
|
|
|
$last_error = error_get_last();
|
|
if ( ! is_null( $last_error ) && in_array( $last_error['type'], array( E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR ), true ) ) {
|
|
return;
|
|
}
|
|
|
|
// The most efficient way to check for an existing action is to use `as_has_scheduled_action`, but in unusual
|
|
// cases where another plugin has loaded a very old version of Action Scheduler, it may not be available to us.
|
|
$has_scheduled_action = function_exists( 'as_has_scheduled_action' ) ? 'as_has_scheduled_action' : 'as_next_scheduled_action';
|
|
|
|
if ( call_user_func( $has_scheduled_action, self::WATCHDOG_ACTION_NAME ) ) {
|
|
return;
|
|
}
|
|
|
|
$enqueued_processors = $this->get_enqueued_processors();
|
|
$unscheduled_processors = array_diff( $enqueued_processors, array_filter( $enqueued_processors, array( $this, 'is_scheduled' ) ) );
|
|
|
|
foreach ( $unscheduled_processors as $processor ) {
|
|
try {
|
|
$instance = $this->get_processor_instance( $processor );
|
|
} catch ( \Exception $e ) {
|
|
continue;
|
|
}
|
|
|
|
$exception = new \Exception( 'Processor is enqueued but not scheduled. Background job was probably killed or marked as failed. Reattempting execution.' );
|
|
$this->update_processor_state( $instance, 0, $exception );
|
|
$this->log_error( $exception, $instance, array() );
|
|
|
|
if ( $this->is_consistently_failing( $instance ) ) {
|
|
$this->log_consistent_failure( $instance, $this->get_process_details( $instance ) );
|
|
$this->remove_processor( $processor );
|
|
} else {
|
|
$this->schedule_batch_processing( $processor, true );
|
|
}
|
|
}
|
|
}
|
|
}
|