File "actions-cleanup.php"

Full Path: /home/adniftyx/public_html/wp-content/plugins/image-optimization/modules/optimization/components/actions-cleanup.php
File size: 4.05 KB
MIME-type: text/x-php
Charset: utf-8

<?php

namespace ImageOptimization\Modules\Optimization\Components;

use ImageOptimization\Classes\Async_Operation\{
	Async_Operation,
	Async_Operation_Hook,
	Async_Operation_Queue,
	Exceptions\Async_Operation_Exception,
	Queries\Image_Optimization_Operation_Query,
	Queries\Operation_Query
};

use ImageOptimization\Classes\Image\{
	Image_Meta,
	Image_Optimization_Error_Type,
	Image_Query_Builder,
	Image_Status,
};

use ImageOptimization\Classes\Logger;
use Throwable;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

class Actions_Cleanup {
	const FIVE_MINUTES_IN_SECONDS = 300;

	/**
	 * @async
	 * @return void
	 */
	public function cleanup_stuck_operations() {
		global $wpdb;

		$table_name = $wpdb->prefix . 'actionscheduler_actions';
		$now = time();
		$threshold = $now - self::FIVE_MINUTES_IN_SECONDS;

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$results = $wpdb->get_col(
			$wpdb->prepare(
				"
				SELECT action_id
				FROM {$table_name}
				WHERE last_attempt_gmt IS NOT NULL
				  AND UNIX_TIMESTAMP(last_attempt_gmt) < %d
				",
				$threshold
			)
		);
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		if ( empty( $results ) ) {
			Logger::debug( 'No stuck optimization operations found for cleanup.' );
			return;
		}

		foreach ( $results as $action_id ) {
			$action = Async_Operation::get_by_id( (int) $action_id );

			if (
				! $action ||
				Async_Operation_Queue::OPTIMIZE !== $action->get_queue() ||
				Async_Operation::OPERATION_STATUS_RUNNING !== $action->get_status()
			) {
				continue;
			}

			try {
				do_action(
					'action_scheduler_failed_action',
					$action_id,
					self::FIVE_MINUTES_IN_SECONDS
				);

				Logger::debug( "Triggered retry for stuck action ID {$action_id}." );
			} catch ( Throwable $t ) {
				Logger::warn( "Failed to handle stuck operation for action ID {$action_id}: " . $t->getMessage() );
			}
		}

		try {
			$this->cleanup_stuck_statuses();
		} catch ( Throwable $t ) {
			Logger::warn( 'Failed to run stuck statuses clearing job: ' . $t->getMessage() );
		}
	}

	/**
	 * The handler checks if there are any attachments that have the in-progress status, but no jobs are currently
	 * run or pending. Those attachment statuses will be updated to a generic error.
	 *
	 * @throws Async_Operation_Exception
	 */
	public function cleanup_stuck_statuses() {
		$operations_query = ( new Image_Optimization_Operation_Query() )
			->set_status( [ Async_Operation::OPERATION_STATUS_PENDING, Async_Operation::OPERATION_STATUS_RUNNING ] )
			->set_limit( 1 );

		$operations = Async_Operation::get( $operations_query );

		if ( ! empty( $operations ) ) {
			return;
		}

		$image_query = ( new Image_Query_Builder() )
			->return_optimization_in_progress_images()
			->set_paging_size( -1 )
			->execute();

		foreach ( $image_query->posts as $attachment_id ) {
			( new Image_Meta( $attachment_id ) )
				->set_status( Image_Status::OPTIMIZATION_FAILED )
				->set_error_type( Image_Optimization_Error_Type::GENERIC )
				->save();
		}
	}

	public function schedule_cleanup() {
		try {
			$cleanup_job_query = ( new Operation_Query() )
				->set_queue( Async_Operation_Queue::CLEANUP )
				->set_hook( Async_Operation_Hook::STUCK_OPERATION_CLEANUP )
				->set_status( [ Async_Operation::OPERATION_STATUS_PENDING, Async_Operation::OPERATION_STATUS_RUNNING ] )
				->set_limit( 1 );

			// Prevents job duplication. For some reason unique=true is not enough
			if ( ! empty( Async_Operation::get( $cleanup_job_query ) ) ) {
				return;
			}

			Async_Operation::create_recurring(
				time(),
				self::FIVE_MINUTES_IN_SECONDS,
				Async_Operation_Hook::STUCK_OPERATION_CLEANUP,
				[],
				Async_Operation_Queue::CLEANUP,
				10,
				true
			);
		} catch ( Async_Operation_Exception $aoe ) {
			Logger::warn( 'Failed to schedule recurring stuck operation cleanup: ' . $aoe->getMessage() );
		}
	}

	public function __construct() {
		add_action( 'action_scheduler_init', [ $this, 'schedule_cleanup' ] );
		add_action( Async_Operation_Hook::STUCK_OPERATION_CLEANUP, [ $this, 'cleanup_stuck_operations' ] );
	}
}