false, 'streams' => array(), 'reason' => 'no_streams' ); } $claim = 'aieo_' . static::RUNNER_ID . '_start'; if ( 1 !== (int) $wpdb->get_var( $wpdb->prepare( 'SELECT GET_LOCK(%s, 2)', $claim ) ) ) { return array( 'started' => false, 'streams' => $keys, 'reason' => 'already_running' ); } try { $running = get_option( static::opt_running() ); if ( is_array( $running ) && ! empty( $running['active'] ) && ! static::run_is_stale( $running ) ) { return array( 'started' => false, 'streams' => $keys, 'reason' => 'already_running' ); } update_option( static::opt_running(), array( 'active' => 1, 'streams' => $keys, 'started_at' => gmdate( 'Y-m-d H:i:s' ), ), false ); update_option( static::opt_progress(), array(), false ); } finally { $wpdb->query( $wpdb->prepare( 'SELECT RELEASE_LOCK(%s)', $claim ) ); } if ( $enqueue ) { foreach ( $streams as $args ) { static::enqueue_tick( (array) $args, 0 ); } } return array( 'started' => true, 'streams' => $keys, 'reason' => $enqueue ? 'queued' : 'claimed' ); } protected static function run_is_stale( array $running ) { $latest = strtotime( (string) ( $running['started_at'] ?? '' ) . ' UTC' ); $prog = get_option( static::opt_progress() ); if ( is_array( $prog ) ) { foreach ( $prog as $row ) { $t = isset( $row['updated'] ) ? strtotime( (string) $row['updated'] . ' UTC' ) : false; if ( false !== $t && $t > (int) $latest ) { $latest = $t; } } } if ( false === $latest || $latest <= 0 ) { return true; } return ( time() - $latest ) > static::STALE_AFTER; } public static function tick_action() { static::run_tick( func_get_args() ); } protected static function run_tick( array $args ) { $res = static::execute_tick( $args ); if ( 'continue' === $res['status'] ) { static::enqueue_tick( $args, 0 ); } elseif ( 'retry' === $res['status'] ) { static::enqueue_tick( $args, (int) $res['retry_in'] ); } } public static function tick_once( array $args ) { return static::execute_tick( $args ); } protected static function execute_tick( array $args ) { global $wpdb; $stream = static::stream_key( $args ); $lock = 'aieo_' . static::RUNNER_ID . '_' . md5( $stream ); if ( 1 !== (int) $wpdb->get_var( $wpdb->prepare( 'SELECT GET_LOCK(%s, 0)', $lock ) ) ) { return array( 'status' => 'locked', 'retry_in' => 0, 'stream' => $stream ); } try { $res = static::do_batch( $args ); $remaining = max( 0, (int) ( $res['remaining'] ?? 0 ) ); $total = (int) ( $res['total'] ?? 0 ); $row = array( 'remaining' => $remaining, 'total' => $total, 'done' => max( 0, $total - $remaining ), 'updated' => gmdate( 'Y-m-d H:i:s' ), ); if ( isset( $res['extra'] ) && is_array( $res['extra'] ) ) { $row = array_merge( $row, $res['extra'] ); } if ( ! empty( $res['success'] ) && $remaining > 0 ) { $row['retries'] = 0; static::update_progress( $stream, $row ); return array( 'status' => 'continue', 'retry_in' => 0, 'stream' => $stream ); } if ( empty( $res['success'] ) && (int) ( $res['retry_in'] ?? 0 ) > 0 ) { $retries = static::stream_retries( $stream ) + 1; $row['retries'] = $retries; static::update_progress( $stream, $row ); if ( $retries <= static::MAX_RETRIES ) { return array( 'status' => 'retry', 'retry_in' => (int) $res['retry_in'], 'stream' => $stream ); } static::mark_stream_done( $stream, 'retries_exhausted' ); return array( 'status' => 'failed', 'retry_in' => 0, 'stream' => $stream ); } static::update_progress( $stream, $row ); $reason = empty( $res['success'] ) ? 'failed' : ( $remaining > 0 ? 'stopped' : '' ); static::mark_stream_done( $stream, $reason ); return array( 'status' => empty( $res['success'] ) ? 'failed' : 'done', 'retry_in' => 0, 'stream' => $stream, ); } catch ( \Throwable $e ) { if ( function_exists( 'aieo_debug_log' ) ) { aieo_debug_log( get_called_class() . ' tick failed (' . $stream . '): ' . $e->getMessage() ); } static::mark_stream_done( $stream, 'exception' ); return array( 'status' => 'failed', 'retry_in' => 0, 'stream' => $stream ); } finally { $wpdb->query( $wpdb->prepare( 'SELECT RELEASE_LOCK(%s)', $lock ) ); } } protected static function enqueue_tick( array $args, $delay = 0 ) { $delay = max( 0, (int) $delay ); if ( 0 === $delay && function_exists( 'as_enqueue_async_action' ) ) { as_enqueue_async_action( static::hook(), $args, 'aieo' ); } elseif ( function_exists( 'as_schedule_single_action' ) ) { as_schedule_single_action( time() + max( 5, $delay ), static::hook(), $args, 'aieo' ); } else { wp_schedule_single_event( time() + max( 5, $delay ), static::hook(), $args ); } } protected static function with_progress_lock( $fn ) { global $wpdb; $lock = 'aieo_' . static::RUNNER_ID . '_progress'; $got = ( 1 === (int) $wpdb->get_var( $wpdb->prepare( 'SELECT GET_LOCK(%s, 3)', $lock ) ) ); try { return $fn(); } finally { if ( $got ) { $wpdb->query( $wpdb->prepare( 'SELECT RELEASE_LOCK(%s)', $lock ) ); } } } protected static function update_progress( $stream, array $data ) { static::with_progress_lock( static function () use ( $stream, $data ) { $prog = get_option( static::opt_progress() ); $prog = is_array( $prog ) ? $prog : array(); $prev = ( isset( $prog[ $stream ] ) && is_array( $prog[ $stream ] ) ) ? $prog[ $stream ] : array(); $prog[ $stream ] = array_merge( $prev, $data ); update_option( static::opt_progress(), $prog, false ); } ); } protected static function stream_retries( $stream ) { $prog = get_option( static::opt_progress() ); return ( is_array( $prog ) && isset( $prog[ $stream ]['retries'] ) ) ? (int) $prog[ $stream ]['retries'] : 0; } protected static function mark_stream_done( $stream, $reason = '' ) { static::with_progress_lock( static function () use ( $stream, $reason ) { $prog = get_option( static::opt_progress() ); $prog = is_array( $prog ) ? $prog : array(); if ( ! isset( $prog[ $stream ] ) ) { $prog[ $stream ] = array(); } $prog[ $stream ]['remaining'] = isset( $prog[ $stream ]['remaining'] ) ? (int) $prog[ $stream ]['remaining'] : 0; $prog[ $stream ]['finished'] = 1; if ( '' !== $reason ) { $prog[ $stream ]['stopped_reason'] = $reason; } $prog[ $stream ]['updated'] = gmdate( 'Y-m-d H:i:s' ); update_option( static::opt_progress(), $prog, false ); $running = get_option( static::opt_running() ); if ( is_array( $running ) && ! empty( $running['active'] ) ) { $stream_keys = array(); if ( ! empty( $running['streams'] ) && is_array( $running['streams'] ) ) { $stream_keys = $running['streams']; } elseif ( ! empty( $running['post_types'] ) && is_array( $running['post_types'] ) ) { $stream_keys = $running['post_types']; } else { $stream_keys = array_keys( $prog ); } $all_done = true; foreach ( $stream_keys as $key ) { if ( empty( $prog[ $key ]['finished'] ) ) { $all_done = false; break; } } if ( $all_done ) { $running['active'] = 0; $running['ended_at'] = gmdate( 'Y-m-d H:i:s' ); update_option( static::opt_running(), $running, false ); } } } ); } public static function runner_status() { $running = get_option( static::opt_running() ); return array( 'running' => (bool) ( is_array( $running ) ? ( $running['active'] ?? 0 ) : 0 ), 'progress' => (array) get_option( static::opt_progress(), array() ), 'started_at' => (string) ( is_array( $running ) ? ( $running['started_at'] ?? '' ) : '' ), 'ended_at' => (string) ( is_array( $running ) ? ( $running['ended_at'] ?? '' ) : '' ), ); } }