stuck_count() ); } /** * Pure evaluation. * * @param int|null $stuck Stuck job count, null if tables are missing. * @return Result[] */ public static function evaluate( ?int $stuck ): array { if ( null === $stuck ) { return array(); } if ( 0 === $stuck ) { return array( new Result( 'woocommerce.stuck', Result::GOOD, __( 'No overdue or stuck background jobs', 'keepwatch' ) ) ); } $status = ( $stuck >= self::CRITICAL_COUNT ) ? Result::CRITICAL : Result::RECOMMENDED; return array( new Result( 'woocommerce.stuck', $status, /* translators: %d: number of stuck jobs */ sprintf( _n( '%d background job is overdue or stuck', '%d background jobs are overdue or stuck', $stuck, 'keepwatch' ), $stuck ), __( 'Pending jobs past their run time or long in-progress claims usually mean WP-Cron or Action Scheduler runners are not keeping up.', 'keepwatch' ), admin_url( 'admin.php?page=wc-status&tab=action-scheduler&status=pending' ), __( 'View scheduled actions', 'keepwatch' ) ), ); } /** * Count overdue pending and aged in-progress actions. */ private function stuck_count(): ?int { global $wpdb; $table = $wpdb->prefix . 'actionscheduler_actions'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- single read-only COUNT. if ( $table !== $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table ) ) ) ) { return null; } $cutoff = gmdate( 'Y-m-d H:i:s', time() - self::OVERDUE_SECONDS ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- see above. $count = $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE ( status = %s AND scheduled_date_gmt < %s ) OR ( status = %s AND last_attempt_gmt < %s )', $table, 'pending', $cutoff, 'in-progress', $cutoff ) ); return null === $count ? null : (int) $count; } }