tions( 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 BatchProcessorInterface $batch_processor Batch processor instance.
* @param array $batch Batch that was being processed.
*/
protected function log_error( \Exception $error, BatchProcessorInterface $batch_processor, array $batch ): void {
$error_message = "Error processing batch for {$batch_processor->get_name()}: {$error->getMessage()}";
$error_context = array(
'exception' => $error,
'source' => 'batch-processing',
);
// 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 BatchProcessorInterface $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 6.8.0
*/
$error_message = apply_filters( 'wc_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.
*
* @since 9.1.0
*
* @param BatchProcessorInterface $batch_processor The processor that we want to check.
* @return boolean TRUE if processor is consistently failing. FALSE otherwise.
*/
private function is_consistently_failing( BatchProcessorInterface $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 9.1.0
*
* @param int $failure_threshold Maximum number of times for the processor to try processing a given batch.
* @param BatchProcessorInterface $batch_processor The processor instance.
* @param array $process_details Array with batch processor state.
*/
apply_filters(
'wc_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.
*
* @since 9.1.0
*
* @param BatchProcessorInterface $batch_processor The batch processor instance.
* @param array $process_details Failing process details.
*/
private function log_consistent_failure( BatchProcessorInterface $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' => 'batch-processing',
'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.
*
* @since 9.1.0
*/
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 );
}
}
}
}