0, 'next_allowed_ts' => 0 ) ); if ( time() < (int) ( $backoff['next_allowed_ts'] ?? 0 ) ) { return array( 'skipped' => true, 'reason' => 'Backing off -- origin was slow on a recent warming pass.' ); } if ( self::origin_looks_unhealthy() ) { self::apply_backoff( $backoff ); return array( 'skipped' => true, 'reason' => 'Origin average response time is elevated; pausing warming this cycle.' ); } self::refill_queue(); $batch_size = max( 1, (int) Argus_Settings::get( 'cache_warm_batch_size', 20 ) ); $batch = self::claim_batch( $batch_size ); if ( empty( $batch ) ) { return array( 'processed' => 0 ); } $concurrency = max( 1, min( 10, (int) Argus_Settings::get( 'cache_warm_concurrency', 4 ) ) ); $min_interval = max( 0, (int) Argus_Settings::get( 'cache_warm_min_interval_secs', 1 ) ); $results = array( 'ok' => 0, 'failed' => 0 ); foreach ( array_chunk( $batch, $concurrency ) as $chunk ) { $chunk_results = self::fetch_concurrently( wp_list_pluck( $chunk, 'url' ) ); foreach ( $chunk as $item ) { $ok = ! empty( $chunk_results[ $item->url ] ); self::record_result( $item, $ok ); $ok ? ++$results['ok'] : ++$results['failed']; } if ( $min_interval > 0 ) { sleep( $min_interval ); } } if ( $results['failed'] > $results['ok'] ) { $backoff['consecutive_slow'] = (int) ( $backoff['consecutive_slow'] ?? 0 ) + 1; self::apply_backoff( $backoff ); } else { update_option( self::BACKOFF_OPTION, array( 'consecutive_slow' => 0, 'next_allowed_ts' => 0 ), false ); } return array( 'processed' => count( $batch ) ) + $results; } protected static function origin_looks_unhealthy() { $summary = Argus_Cache_Log::summary( 1 ); $miss_ms = $summary[ Argus_Static_Cache::STATUS_MISS ]['avg_ms'] ?? null; return null !== $miss_ms && $miss_ms > self::SLOW_ORIGIN_MS; } protected static function apply_backoff( array $backoff ) { $consecutive = max( 1, (int) ( $backoff['consecutive_slow'] ?? 1 ) ); $delay = min( HOUR_IN_SECONDS, 60 * ( 2 ** $consecutive ) ); update_option( self::BACKOFF_OPTION, array( 'consecutive_slow' => $consecutive, 'next_allowed_ts' => time() + $delay ), false ); } protected static function refill_queue() { global $wpdb; $discovered = Argus_DB::table( 'cache_discovered_urls' ); $queue = Argus_DB::table( 'cache_warm_queue' ); $home = home_url( '/' ); $now = current_time( 'mysql', true ); $candidates = $wpdb->get_results( $wpdb->prepare( "SELECT url, lastmod FROM {$discovered} WHERE eligibility = %s AND ( last_warmed_at IS NULL OR last_warmed_at < %s ) AND NOT EXISTS ( SELECT 1 FROM {$queue} q WHERE q.url = {$discovered}.url AND q.status = 'queued' ) ORDER BY (url = %s) DESC, lastmod DESC LIMIT %d", // phpcs:ignore Argus_Cache_Eligibility::SAFE_TO_CACHE, gmdate( 'Y-m-d H:i:s', strtotime( '-1 hour' ) ), $home, self::REFILL_LIMIT ) ); foreach ( $candidates as $i => $row ) { $wpdb->query( $wpdb->prepare( "INSERT INTO {$queue} (url, priority, status, queued_at) VALUES (%s, %d, 'queued', %s)", // phpcs:ignore $row->url, $row->url === $home ? 0 : ( 100 + $i ), $now ) ); } } protected static function claim_batch( $limit ) { global $wpdb; $table = Argus_DB::table( 'cache_warm_queue' ); $rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$table} WHERE status = 'queued' ORDER BY priority ASC, id ASC LIMIT %d", $limit ) // phpcs:ignore ); if ( empty( $rows ) ) { return array(); } $ids = wp_list_pluck( $rows, 'id' ); $wpdb->query( "UPDATE {$table} SET status = 'processing' WHERE id IN (" . implode( ',', array_map( 'absint', $ids ) ) . ')' ); // phpcs:ignore return $rows; } protected static function record_result( $item, $ok ) { global $wpdb; $queue = Argus_DB::table( 'cache_warm_queue' ); $discovered = Argus_DB::table( 'cache_discovered_urls' ); $now = current_time( 'mysql', true ); if ( $ok ) { $wpdb->update( $queue, array( 'status' => 'done', 'last_attempt_at' => $now, 'last_result' => 'warmed' ), array( 'id' => $item->id ) ); $wpdb->update( $discovered, array( 'last_warmed_at' => $now ), array( 'url' => $item->url ) ); Argus_Cache_Log::record( Argus_Static_Cache::STATUS_WARMED, $item->url, 'Preloaded by cache warmer.' ); return; } $attempts = (int) $item->attempts + 1; if ( $attempts >= self::MAX_ATTEMPTS ) { $wpdb->update( $queue, array( 'status' => 'failed', 'attempts' => $attempts, 'last_attempt_at' => $now, 'last_result' => 'gave up after ' . $attempts . ' attempts' ), array( 'id' => $item->id ) ); } else { $wpdb->update( $queue, array( 'status' => 'queued', 'attempts' => $attempts, 'last_attempt_at' => $now, 'last_result' => 'retrying' ), array( 'id' => $item->id ) ); } } protected static function fetch_concurrently( array $urls ) { if ( ! function_exists( 'curl_multi_init' ) ) { $out = array(); foreach ( $urls as $url ) { $response = wp_remote_get( $url, array( 'timeout' => 20, 'user-agent' => self::user_agent() ) ); $out[ $url ] = ! is_wp_error( $response ) && wp_remote_retrieve_response_code( $response ) < 500; } return $out; } $mh = curl_multi_init(); // phpcs:ignore WordPress.WP.AlternativeFunctions $handles = array(); foreach ( $urls as $url ) { $ch = curl_init( $url ); // phpcs:ignore WordPress.WP.AlternativeFunctions curl_setopt_array( // phpcs:ignore WordPress.WP.AlternativeFunctions $ch, array( CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 20, CURLOPT_USERAGENT => self::user_agent(), CURLOPT_NOBODY => false, CURLOPT_SSL_VERIFYPEER => true, ) ); curl_multi_add_handle( $mh, $ch ); // phpcs:ignore WordPress.WP.AlternativeFunctions $handles[ $url ] = $ch; } $running = null; do { curl_multi_exec( $mh, $running ); // phpcs:ignore WordPress.WP.AlternativeFunctions curl_multi_select( $mh ); // phpcs:ignore WordPress.WP.AlternativeFunctions } while ( $running > 0 ); $out = array(); foreach ( $handles as $url => $ch ) { $code = curl_getinfo( $ch, CURLINFO_HTTP_CODE ); // phpcs:ignore WordPress.WP.AlternativeFunctions $out[ $url ] = $code > 0 && $code < 500; curl_multi_remove_handle( $mh, $ch ); // phpcs:ignore WordPress.WP.AlternativeFunctions curl_close( $ch ); // phpcs:ignore WordPress.WP.AlternativeFunctions } curl_multi_close( $mh ); // phpcs:ignore WordPress.WP.AlternativeFunctions return $out; } protected static function user_agent() { return 'ARGUS-Defence-CacheWarmer/1.0 (+' . home_url( '/' ) . ')'; } public static function status() { global $wpdb; $table = Argus_DB::table( 'cache_warm_queue' ); $rows = $wpdb->get_results( "SELECT status, COUNT(*) AS cnt FROM {$table} GROUP BY status" ); // phpcs:ignore $out = array( 'queued' => 0, 'processing' => 0, 'done' => 0, 'failed' => 0 ); foreach ( $rows as $row ) { if ( isset( $out[ $row->status ] ) ) { $out[ $row->status ] = (int) $row->cnt; } } $backoff = get_option( self::BACKOFF_OPTION, array() ); $out['paused_until'] = ( ! empty( $backoff['next_allowed_ts'] ) && $backoff['next_allowed_ts'] > time() ) ? $backoff['next_allowed_ts'] : null; return $out; } }